├── .gitignore ├── src ├── fonts_9 │ ├── icomoon.ttf │ ├── icomoon.woff │ └── icomoon.svg ├── Config.css ├── Bifrost.css ├── PressureHelper.css ├── index.js ├── App.css ├── color_picker.js ├── ErrorBoundary.js ├── UserAction.css ├── Alerts.js ├── Sidebar.js ├── Message.js ├── Title.css ├── Welcome.css ├── icomoon.css ├── index.css ├── Common.css ├── text_splitter.js ├── PressureHelper.js ├── Sidebar.css ├── Welcome.js ├── serviceWorker.js ├── flows_api.js ├── Title.js ├── cache.js ├── Flows.css ├── App.js ├── Config.js ├── Bifrost.js ├── Common.js └── UserAction.js ├── public ├── static │ ├── bg │ │ └── default.png │ ├── favicon │ │ ├── 180.png │ │ ├── 192.png │ │ └── 256.png │ └── manifest.json └── index.html ├── .gitmodules ├── package.json ├── README.md └── LICENSE.txt /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | node_modules/ 3 | /build* 4 | .env 5 | -------------------------------------------------------------------------------- /src/fonts_9/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xmcp/webhole/HEAD/src/fonts_9/icomoon.ttf -------------------------------------------------------------------------------- /src/fonts_9/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xmcp/webhole/HEAD/src/fonts_9/icomoon.woff -------------------------------------------------------------------------------- /public/static/bg/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xmcp/webhole/HEAD/public/static/bg/default.png -------------------------------------------------------------------------------- /public/static/favicon/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xmcp/webhole/HEAD/public/static/favicon/180.png -------------------------------------------------------------------------------- /public/static/favicon/192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xmcp/webhole/HEAD/public/static/favicon/192.png -------------------------------------------------------------------------------- /public/static/favicon/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xmcp/webhole/HEAD/public/static/favicon/256.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/react-lazyload"] 2 | path = src/react-lazyload 3 | url = https://github.com/xmcp/react-lazyload 4 | -------------------------------------------------------------------------------- /src/Config.css: -------------------------------------------------------------------------------- 1 | .config-ui-header { 2 | text-align: center; 3 | top: 1em; 4 | position: sticky; 5 | } 6 | 7 | .bg-preview { 8 | height: 18em; 9 | width: 32em; 10 | max-height: 60vh; 11 | max-width: 100%; 12 | margin: .5em auto 1em; 13 | box-shadow: 0 1px 5px rgba(0,0,0,.4); 14 | } -------------------------------------------------------------------------------- /src/Bifrost.css: -------------------------------------------------------------------------------- 1 | .flow-item-toolbar { 2 | border: 1px solid black; 3 | border-radius: .5em; 4 | padding: .5em; 5 | margin-top: .5rem; 6 | } 7 | .bifrost-portlet-selector button { 8 | margin: 0 .2em; 9 | } 10 | .bifrost-toolbar>:nth-child(2) { 11 | margin-top: .5em; 12 | } 13 | .bifrost-toolbar textarea { 14 | width: 100%; 15 | height: 4em; 16 | } -------------------------------------------------------------------------------- /src/PressureHelper.css: -------------------------------------------------------------------------------- 1 | .pressure-box { 2 | border: 500px /* also change js! */ solid orange; 3 | position: fixed; 4 | margin: auto; 5 | z-index: 100; 6 | pointer-events: none; 7 | } 8 | 9 | .pressure-box-empty { 10 | visibility: hidden; 11 | } 12 | 13 | .pressure-box-fired { 14 | border-color: orangered; 15 | pointer-events: initial !important; 16 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import {ErrorBoundary} from './ErrorBoundary'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | import './index.css'; 8 | import './icomoon.css'; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | 14 | , document.getElementById('root')); 15 | 16 | serviceWorker.register(); 17 | -------------------------------------------------------------------------------- /public/static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "_BRAND_NAME", 3 | "name": "_BRAND_NAME", 4 | "icons": [ 5 | { 6 | "src": "favicon/256.png", 7 | "sizes": "256x256", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "favicon/192.png", 12 | "sizes": "192x192", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": "../", 17 | "display": "standalone", 18 | "theme_color": "#333333", 19 | "background_color": "#333333" 20 | } 21 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .flows-anim-exit { 2 | opacity: 1; 3 | transform: unset; 4 | } 5 | .flows-anim-exit-active { 6 | opacity: 0; 7 | transform: translateY(1.5em) scaleX(.9); 8 | transition: opacity .1s ease-out, transform .1s ease-out; 9 | } 10 | .flows-anim-enter, .flows-anim-appear { 11 | opacity: 0; 12 | transform: translateY(-1em); 13 | } 14 | .flows-anim-enter-active, .flows-anim-appear-active { 15 | opacity: 1; 16 | transform: unset; 17 | transition: opacity .1s ease-out, transform .1s ease-out; 18 | } -------------------------------------------------------------------------------- /src/color_picker.js: -------------------------------------------------------------------------------- 1 | // https://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/ 2 | 3 | const golden_ratio_conjugate=0.618033988749895; 4 | 5 | export class ColorPicker { 6 | constructor() { 7 | this.names={}; 8 | this.current_h=Math.random(); 9 | } 10 | 11 | get(name) { 12 | name=name.toLowerCase(); 13 | if(name==='洞主') 14 | return ['hsl(0,0%,97%)','hsl(0,0%,16%)']; 15 | 16 | if(!this.names[name]) { 17 | this.current_h+=golden_ratio_conjugate; 18 | this.current_h%=1; 19 | this.names[name]=[ 20 | `hsl(${this.current_h*360}, 50%, 90%)`, 21 | `hsl(${this.current_h*360}, 60%, 20%)`, 22 | ]; 23 | } 24 | return this.names[name]; 25 | } 26 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webhole", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "copy-to-clipboard": "^3.0.8", 7 | "fix-orientation": "^1.1.0", 8 | "load-script": "^1.0.0", 9 | "pressure": "^2.1.2", 10 | "react": "^16.14.0", 11 | "react-dom": "^16.14.0", 12 | "react-scripts": "^3.4.4", 13 | "react-timeago": "^4.1.9", 14 | "react-transition-group": "^4.4.1" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test --env=jsdom", 20 | "eject": "react-scripts eject" 21 | }, 22 | "homepage": ".", 23 | "browserslist": { 24 | "production": [ 25 | ">0.2%", 26 | "not dead", 27 | "not op_mini all" 28 | ], 29 | "development": [ 30 | "last 1 chrome version", 31 | "last 1 firefox version", 32 | "last 1 safari version" 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | export class ErrorBoundary extends Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { error: null }; 7 | } 8 | 9 | static getDerivedStateFromError(error) { 10 | console.log(error); 11 | return { error: error }; 12 | } 13 | 14 | render() { 15 | if(this.state.error) 16 | return ( 17 |
18 |

Frontend Exception

19 |

Please report this bug to developers.

20 |
21 |
{''+this.state.error}
22 |
23 |
{this.state.error.stack||'(no stack info)'}
24 |
25 | ); 26 | else 27 | return this.props.children; 28 | } 29 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | _BRAND_NAME 24 | 30 | 31 | 32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /src/UserAction.css: -------------------------------------------------------------------------------- 1 | .login-form p { 2 | margin: 1em 0; 3 | text-align: center; 4 | } 5 | .login-form button { 6 | width: 6rem; 7 | } 8 | 9 | .reply-sticky { 10 | position: sticky; 11 | bottom: 0; 12 | top: .5em; 13 | } 14 | 15 | .post-form-bar { 16 | line-height: 1.75em; 17 | display: flex; 18 | margin-bottom: .25em; 19 | } 20 | .post-form-bar .post-form-switcher { 21 | flex: 1; 22 | font-size: .9em; 23 | } 24 | .post-form-switch { 25 | padding: .1em .5em; 26 | cursor: pointer; 27 | opacity: .6; 28 | } 29 | .post-form-switch:not(.post-form-switch-cur) { 30 | color: var(--var-link-color); 31 | } 32 | .post-form-switch-cur { 33 | background-color: #fcfcfc; 34 | box-shadow: 0 0 3px rgba(0,0,0,.5); 35 | border-radius: 1em; 36 | } 37 | .root-dark-mode .post-form-switch-cur { 38 | background-color: #333; 39 | color: var(--foreground-dark); 40 | box-shadow: 0 0 2px white; 41 | } 42 | .post-form-switch-cur, .post-form-switch:hover { 43 | opacity: 1; 44 | } 45 | .post-form-bar .post-btn { 46 | flex: 0 0 8em; 47 | margin-right: 0; 48 | } 49 | .post-form-img-tip { 50 | font-size: .9em; 51 | } 52 | .post-form textarea { 53 | resize: vertical; 54 | width: 100%; 55 | min-height: 2em; 56 | height: 8em; 57 | } 58 | .post-form-reply.box { 59 | 60 | margin: 0 -.15em; 61 | width: calc(100% + .3em) !important; 62 | } 63 | .post-form.post-form-reply textarea { 64 | height: 4em; 65 | } 66 | 67 | .invite-code { 68 | margin: .5rem; 69 | font-size: 1.5em; 70 | } 71 | .invite-code a { 72 | font-size: 1rem; 73 | vertical-align: .2rem; 74 | } -------------------------------------------------------------------------------- /src/Alerts.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react'; 2 | 3 | export function Alerts(props) { 4 | let [external_alerts, set_external_alerts]=useState([]); 5 | 6 | useEffect(()=>{ 7 | window._webhole_show_alert=(key,msg)=>{ 8 | let new_alerts=external_alerts.slice().filter((x)=>x[0]!==key); 9 | if(msg) 10 | new_alerts.push([key,msg]); 11 | set_external_alerts(new_alerts); 12 | }; 13 | },[external_alerts]); 14 | 15 | let alerts_to_show=props.info.alerts.concat(external_alerts.map(([_key,msg])=>msg)); 16 | 17 | return ( 18 |
19 |
20 | {!!window.__WEBHOLE_DEV_SERVER_FLAG && 21 |
22 |
23 | CONNECTED TO DEV SERVER 24 |
25 |
26 | } 27 | {alerts_to_show.map((al,idx)=> { 28 | let clsname='flow-item box'+(al.type==='danger' ? ' box-danger' : al.type==='warning' ? ' box-warning' : ''); 29 | if(al.is_html) 30 | return ( 31 |
32 |
33 |
34 | ); 35 | else 36 | return ( 37 |
38 |
{al.message}
39 |
40 | ); 41 | })} 42 |
43 | ); 44 | } -------------------------------------------------------------------------------- /src/Sidebar.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PureComponent} from 'react'; 2 | import './Sidebar.css'; 3 | 4 | export class Sidebar extends PureComponent { 5 | constructor(props) { 6 | super(props); 7 | this.sidebar_ref=React.createRef(); 8 | this.do_close_bound=this.do_close.bind(this); 9 | this.do_back_bound=this.do_back.bind(this); 10 | } 11 | 12 | componentDidUpdate(nextProps) { 13 | if(this.props.stack!==nextProps.stack) { 14 | //console.log('sidebar top'); 15 | if(this.sidebar_ref.current) 16 | this.sidebar_ref.current.scrollTop=0; 17 | } 18 | } 19 | 20 | do_close() { 21 | this.props.show_sidebar(null,null,'clear'); 22 | } 23 | do_back() { 24 | this.props.show_sidebar(null,null,'pop'); 25 | } 26 | 27 | render() { 28 | let [cur_title,cur_content]=this.props.stack[this.props.stack.length-1]; 29 | return ( 30 |
31 |
{e.preventDefault();e.target.click();}} /> 32 |
33 | {cur_content} 34 |
35 |
36 |    37 | {this.props.stack.length>2 && 38 |    39 | } 40 | {cur_title} 41 |
42 |
43 | ); 44 | } 45 | } -------------------------------------------------------------------------------- /src/Message.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react'; 2 | import {get_json, token_param} from './flows_api'; 3 | import {Time, HAPI_DOMAIN} from './Common'; 4 | 5 | const FOLD_LENGTH=100; 6 | function FoldedPre(props) { 7 | let [folded,set_folded]=useState(()=>props.text.length>FOLD_LENGTH); 8 | 9 | let showing_text=folded ? props.text.substr(0,FOLD_LENGTH) : props.text; 10 | return ( 11 |
12 |             {showing_text}
13 |             {folded &&
14 |                 <>
15 |                     … 
16 |                     set_folded(false)}>展开
17 |                 
18 |             }
19 |         
20 | ); 21 | } 22 | 23 | export function MessageViewer(props) { 24 | let [res,set_res]=useState(null); 25 | 26 | useEffect(()=>{ 27 | fetch(HAPI_DOMAIN+'/api/messages/list?role=msg'+token_param(props.token)) 28 | .then(get_json) 29 | .then((json)=>{ 30 | if(json.error) 31 | throw new Error(json.error_msg||json.error); 32 | 33 | set_res(json.data); 34 | }) 35 | .catch((e)=>{ 36 | alert('加载失败。'+e); 37 | }); 38 | },[]); 39 | 40 | if(res===null) 41 | return ( 42 |
43 | 正在获取…… 44 |
45 | ); 46 | 47 | return ( 48 |
49 | {res.map((msg)=>( 50 |
51 |
52 |
55 |
56 | 57 |
58 |
59 | ))} 60 | {res.length===0 && 61 |
62 | 暂无系统消息 63 |
64 | } 65 |
66 | ); 67 | } -------------------------------------------------------------------------------- /src/Title.css: -------------------------------------------------------------------------------- 1 | .title-bar { 2 | z-index: 10; 3 | position: sticky; 4 | top: -6em; 5 | left: 0; 6 | width: 100%; 7 | height: 9em; 8 | background-color: rgba(255,255,255,.85); 9 | color: black; 10 | box-shadow: 0 0 25px rgba(0,0,0,.4); 11 | margin-bottom: 1em; 12 | backdrop-filter: blur(5px); 13 | } 14 | .root-no-blur .title-bar { 15 | backdrop-filter: unset; 16 | } 17 | 18 | .root-dark-mode .title-bar { 19 | background-color: hsla(0,0%,12%,.85); 20 | color: var(--foreground-dark); 21 | box-shadow: 0 0 5px rgba(255,255,255,.1); 22 | } 23 | 24 | .control-bar { 25 | display: flex; 26 | margin-top: .5em; 27 | line-height: 2em; 28 | } 29 | 30 | .control-btn { 31 | flex: 0 0 4.5em; 32 | text-align: center; 33 | color: black; 34 | border-radius: 5px; 35 | } 36 | .control-btn:hover { 37 | background-color: #666666; 38 | color: white; 39 | } 40 | .control-btn-label { 41 | margin-left: .25rem; 42 | font-size: .9em; 43 | vertical-align: .05em; 44 | } 45 | @media screen and (max-width: 900px) { 46 | .control-btn { 47 | flex: 0 0 2.5em; 48 | } 49 | .control-btn-label { 50 | display: none; 51 | } 52 | .control-search { 53 | padding: 0 .5em; 54 | } 55 | } 56 | 57 | .root-dark-mode .control-btn { 58 | color: var(--foreground-dark); 59 | opacity: .9; 60 | } 61 | .root-dark-mode .control-btn:hover { 62 | color: var(--foreground-dark); 63 | opacity: 1; 64 | } 65 | 66 | .control-search { 67 | flex: auto; 68 | color: black; 69 | background-color: rgba(255,255,255,.3) !important; 70 | margin: 0 .5em; 71 | min-width: 8em; 72 | } 73 | 74 | .control-search:focus { 75 | background-color: white !important; 76 | } 77 | 78 | .root-dark-mode .control-search { 79 | background-color: hsla(0,0%,35%,.6) !important; 80 | color: var(--foreground-dark); 81 | } 82 | .root-dark-mode .control-search:focus { 83 | background-color: hsl(0,0%,80%) !important; 84 | color: black !important; 85 | } 86 | 87 | .list-menu { 88 | text-align: center; 89 | } 90 | 91 | .help-desc-box p { 92 | margin: .5em; 93 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Seed 2 | 3 | A refined version of the front-end code used at PKU Helper. 4 | 5 | **Note that this repository does not belong to any website. Developers of this repository take no responsibility of contents on websites using it.** 6 | 7 | We do not provide any kind of tech support. Use at your own risk. 8 | 9 | ## Installation 10 | 11 | A corresponding backend server is required to host APIs for the front-end code. 12 | Visit [jimmielin/the-light](https://github.com/jimmielin/the-light) for setup instructions for backend server. 13 | 14 | Install `nodejs` and run `npm install` to install requirements. 15 | 16 | As a normal Create-React-App project, run `npm start` to start local dev server, 17 | and run `npm run build` to build production HTML files into `build` directory. 18 | 19 | You may need to run `git submodule init && git submodule update --remote` if submodule is not cloned. 20 | 21 | ## Customization and Configuration 22 | 23 | All branding information (e.g. website title, API domain, slogan) has been removed, and it is up to you to customize them. 24 | 25 | Those customizable variables start with `_BRAND` in their name. 26 | You can search for `_BRAND_` in `src` and `public` directory and replace all occurrences to the value you want. 27 | 28 | Moreover, `index.html` includes these vital parameters that you would mostly like to change: 29 | 30 | - `__WEBHOLE_HAPI_DOMAIN`: hole backend API domain, e.g. `https://hapi.your_domain.com` 31 | - `__WEBHOLE_GATEWAY_DOMAIN`: gateway domain for user management, e.g. `https://gateway.your_domain.com` 32 | - `__WEBHOLE_DEV_SERVER_FLAG`: set to `true` if it is a development environment (will show an alert on the page) 33 | - `__WEBHOLE_DISABLE_WEBP`: set to `true` if hole backend API does not support webp image format 34 | 35 | ## License 36 | 37 | This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. 38 | 39 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the [GNU General Public License](https://www.gnu.org/licenses/gpl-3.0.zh-cn.html) for more details. 40 | -------------------------------------------------------------------------------- /src/Welcome.css: -------------------------------------------------------------------------------- 1 | .box.login-box { 2 | font-size: 1rem; 3 | text-align: center; 4 | padding: 1em .25em; 5 | border-radius: 10px; 6 | } 7 | 8 | .login-box-background { 9 | color: black; 10 | background-color: rgba(255,255,255,.85); 11 | box-shadow: 0 0 30px rgba(255,255,255,.6); 12 | backdrop-filter: blur(5px); 13 | } 14 | 15 | .root-dark-mode .box.login-box-background { 16 | color: var(--foreground-dark); 17 | background-color: rgba(0,0,0,.7); 18 | box-shadow: 0 0 200px rgba(255,255,255,.4); 19 | } 20 | .login-box-btn { 21 | min-width: 12em; 22 | } 23 | .login-box p { 24 | margin: .5rem 0; 25 | } 26 | 27 | .login-box .login-box-title { 28 | font-size: 1.1em; 29 | margin-bottom: 1rem; 30 | } 31 | 32 | .title-bar-landing { 33 | height: 7em; 34 | position: unset; 35 | margin-bottom: 2em; 36 | } 37 | 38 | .landing-container { 39 | margin: 0 auto 4em auto; 40 | padding: 0 2em; 41 | max-width: 1100px; 42 | display: flex; 43 | flex-direction: row-reverse; 44 | } 45 | 46 | .landing-container-side { 47 | flex: 0 0 350px; 48 | margin-left: 2em; 49 | } 50 | 51 | .landing-container-content { 52 | flex: 1 1; 53 | } 54 | 55 | .landing-content { 56 | margin-top: 1rem; 57 | text-shadow: 0 0 6px black, 0 0 3px black, 0 0 2px black; 58 | } 59 | .landing-content h1 { 60 | font-size: 1.35em; 61 | } 62 | .landing-content p { 63 | font-size: 1.05em; 64 | } 65 | .landing-content a { 66 | color: unset; 67 | border-bottom: 1px solid white; 68 | margin-bottom: -1px; 69 | } 70 | .landing-content strong { 71 | font-weight: normal; 72 | color: #ffff88; 73 | } 74 | 75 | .bg-img-landing { 76 | filter: contrast(.9) brightness(0.75); 77 | } 78 | 79 | @media screen and (max-width: 950px) { 80 | .landing-container { 81 | padding: 0 .5em; 82 | } 83 | .landing-container-side { 84 | flex: 0 0 310px; 85 | margin-left: 1em; 86 | } 87 | .landing-content h1 { 88 | font-size: 1.2em; 89 | } 90 | .landing-content p { 91 | font-size: 1em; 92 | } 93 | .title-bar-landing { 94 | margin-bottom: .5em; 95 | } 96 | } 97 | 98 | @media screen and (max-width: 650px) { 99 | .landing-container { 100 | padding: 0 1.5em; 101 | } 102 | .landing-container { 103 | display: block; 104 | } 105 | .landing-container-side { 106 | margin-left: 0; 107 | } 108 | .title-bar-landing { 109 | margin-bottom: 1.5em; 110 | } 111 | .landing-content { 112 | margin-top: 1em; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/icomoon.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'icomoon'; 3 | src: 4 | url('fonts_9/icomoon.ttf?tud302') format('truetype'), 5 | url('fonts_9/icomoon.woff?tud302') format('woff'), 6 | url('fonts_9/icomoon.svg?tud302#icomoon') format('svg'); 7 | font-weight: normal; 8 | font-style: normal; 9 | font-display: block; 10 | } 11 | 12 | .icon { 13 | /* use !important to prevent issues with browser extensions that change fonts */ 14 | font-family: 'icomoon' !important; 15 | speak: never; 16 | font-style: normal; 17 | font-weight: normal; 18 | font-variant: normal; 19 | text-transform: none; 20 | line-height: 1; 21 | vertical-align: -.0625em; 22 | 23 | /* Better Font Rendering =========== */ 24 | -webkit-font-smoothing: antialiased; 25 | -moz-osx-font-smoothing: grayscale; 26 | } 27 | 28 | .icon-send:before { 29 | content: "\e900"; 30 | } 31 | .icon-pen:before { 32 | content: "\e908"; 33 | } 34 | .icon-image:before { 35 | content: "\e90d"; 36 | } 37 | .icon-mic:before { 38 | content: "\e91e"; 39 | } 40 | .icon-textfile:before { 41 | content: "\e926"; 42 | } 43 | .icon-history:before { 44 | content: "\e94d"; 45 | } 46 | .icon-reply:before { 47 | content: "\e96b"; 48 | } 49 | .icon-quote:before { 50 | content: "\e977"; 51 | } 52 | .icon-loading:before { 53 | content: "\e979"; 54 | } 55 | .icon-login:before { 56 | content: "\e98d"; 57 | } 58 | .icon-settings:before { 59 | content: "\e994"; 60 | } 61 | .icon-stats:before { 62 | content: "\e99b"; 63 | } 64 | .icon-fire:before { 65 | content: "\e9a9"; 66 | } 67 | .icon-locate:before { 68 | content: "\e9b3"; 69 | } 70 | .icon-upload:before { 71 | content: "\e9c3"; 72 | } 73 | .icon-flag:before { 74 | content: "\e9cc"; 75 | } 76 | .icon-attention:before { 77 | content: "\e9d3"; 78 | } 79 | .icon-star:before { 80 | content: "\e9d7"; 81 | } 82 | .icon-star-ok:before { 83 | content: "\e9d9"; 84 | } 85 | .icon-plus:before { 86 | content: "\ea0a"; 87 | } 88 | .icon-about:before { 89 | content: "\ea0c"; 90 | } 91 | .icon-close:before { 92 | content: "\ea0d"; 93 | } 94 | .icon-logout:before { 95 | content: "\ea14"; 96 | } 97 | .icon-play:before { 98 | content: "\ea15"; 99 | } 100 | .icon-pause:before { 101 | content: "\ea16"; 102 | } 103 | .icon-refresh:before { 104 | content: "\ea2e"; 105 | } 106 | .icon-forward:before { 107 | content: "\ea42"; 108 | } 109 | .icon-back:before { 110 | content: "\ea44"; 111 | } 112 | .icon-order-rev:before { 113 | content: "\ea46"; 114 | font-size: 1.2em; 115 | } 116 | .icon-order-rev-down:before { 117 | content: "\ea47"; 118 | font-size: 1.2em; 119 | } 120 | .icon-github:before { 121 | content: "\eab0"; 122 | } 123 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --foreground-dark: hsl(0,0%,93%); 3 | } 4 | 5 | body, textarea, pre { 6 | font-family: 'Segoe UI', '微软雅黑', 'Microsoft YaHei', sans-serif; 7 | } 8 | 9 | * { 10 | box-sizing: border-box; 11 | word-wrap: break-word; 12 | -webkit-overflow-scrolling: touch; 13 | } 14 | 15 | p, pre { 16 | margin: 0; 17 | } 18 | 19 | a { 20 | text-decoration: none; 21 | cursor: pointer; 22 | } 23 | 24 | pre { 25 | white-space: pre-line; 26 | } 27 | 28 | code { 29 | font-family: Consolas, Courier, monospace; 30 | } 31 | 32 | body { 33 | background-size: cover; 34 | user-select: none; 35 | background-color: #333; 36 | color: white; 37 | margin: 0; 38 | padding: 0; 39 | overflow-x: hidden; 40 | text-size-adjust: 100%; 41 | } 42 | 43 | body.root-dark-mode { 44 | background-color: black; 45 | } 46 | 47 | html::-webkit-scrollbar { 48 | display: none; 49 | } 50 | html { 51 | scrollbar-width: none; 52 | -ms-overflow-style: none; 53 | } 54 | 55 | :root { 56 | --var-link-color: #00c; 57 | } 58 | .root-dark-mode .box, .root-dark-mode .sidebar, .root-dark-mode .sidebar-title { 59 | --var-link-color: #9bf; 60 | } 61 | 62 | a, .link-color { 63 | color: var(--var-link-color); 64 | } 65 | a:not(.no-underline):hover { 66 | border-bottom: 1px solid var(--var-link-color); 67 | margin-bottom: -1px; 68 | } 69 | 70 | input, textarea { 71 | border-radius: 5px; 72 | border: 1px solid black; 73 | outline: none; 74 | margin: 0; 75 | } 76 | input { 77 | padding: 0 1em; 78 | line-height: 2em; 79 | } 80 | 81 | audio { 82 | vertical-align: middle; 83 | } 84 | 85 | button, .button { 86 | color: black; 87 | background-color: rgba(235,235,235,.5); 88 | border-radius: 5px; 89 | text-align: center; 90 | border: 1px solid black; 91 | line-height: 2em; 92 | margin: 0 .5rem; 93 | } 94 | 95 | .root-dark-mode button, .root-dark-mode .button { 96 | background-color: hsl(0,0%,30%); 97 | color: var(--foreground-dark); 98 | } 99 | 100 | button:hover, .button:hover { 101 | background-color: rgba(255,255,255,.7); 102 | } 103 | 104 | .root-dark-mode button:hover, .root-dark-mode .button:hover { 105 | background-color: hsl(0,0%,40%); 106 | } 107 | 108 | button:disabled, .button:disabled { 109 | background-color: rgba(128,128,128,.5); 110 | } 111 | 112 | .root-dark-mode button:disabled, .root-dark-mode .button:disabled { 113 | background-color: hsl(0,0%,20%); 114 | color: hsl(0,0%,60%); 115 | } 116 | 117 | .root-dark-mode input:not([type=file]), .root-dark-mode textarea { 118 | background-color: hsl(0,0%,30%); 119 | color: var(--foreground-dark); 120 | } 121 | .root-dark-mode input:not([type=file])::placeholder { 122 | color: var(--foreground-dark); 123 | } -------------------------------------------------------------------------------- /src/Common.css: -------------------------------------------------------------------------------- 1 | .centered-line { 2 | overflow: hidden; 3 | text-align: center; 4 | } 5 | 6 | .centered-line::before, 7 | .centered-line::after { 8 | background-color: #000; 9 | content: ""; 10 | display: inline-block; 11 | height: 1px; 12 | position: relative; 13 | vertical-align: middle; 14 | width: 50%; 15 | } 16 | 17 | .root-dark-mode .centered-line { 18 | color: var(--foreground-dark); 19 | } 20 | .root-dark-mode .centered-line::before, .root-dark-mode .centered-line::after { 21 | background-color: var(--foreground-dark); 22 | } 23 | 24 | .centered-line::before { 25 | right: 1em; 26 | margin-left: -50%; 27 | } 28 | 29 | .centered-line::after { 30 | left: 1em; 31 | margin-right: -50%; 32 | } 33 | 34 | .title-line { 35 | color: #fff; 36 | margin-top: 1em; 37 | } 38 | .title-line::before, 39 | .title-line::after { 40 | background-color: #fff; 41 | box-shadow: 0 1px 1px #000; 42 | } 43 | 44 | .root-dark-mode .title-line { 45 | color: var(--foreground-dark); 46 | } 47 | .root-dark-mode .title-line::before, .root-dark-mode .title-line::after { 48 | background-color: var(--foreground-dark); 49 | } 50 | 51 | .aux-margin { 52 | width: calc(100% - 2 * 50px); 53 | margin: 0 50px; 54 | } 55 | @media screen and (max-width: 1300px) { 56 | .aux-margin { 57 | width: calc(100% - 2 * 10px); 58 | margin: 0 10px; 59 | } 60 | } 61 | 62 | .title { 63 | font-size: 1.5em; 64 | height: 5.25rem; 65 | padding-top: 1rem; 66 | text-align: center; 67 | } 68 | 69 | .title-slogan { 70 | font-size: 1rem; 71 | } 72 | 73 | .clickable { 74 | cursor: pointer; 75 | } 76 | 77 | .bg-img { 78 | position: fixed; 79 | z-index: -1; 80 | top: 0; 81 | left: 0; 82 | width: 100%; 83 | height: 100%; 84 | } 85 | 86 | .root-dark-mode .bg-img { 87 | filter: brightness(.6); 88 | } 89 | 90 | .black-outline { 91 | text-shadow: /* also change .flow-item-row-with-prompt:hover::before */ 92 | -1px -1px 0 rgba(0,0,0,.6), 93 | 0 -1px 0 rgba(0,0,0,.6), 94 | 1px -1px 0 rgba(0,0,0,.6), 95 | -1px 1px 0 rgba(0,0,0,.6), 96 | 0 1px 0 rgba(0,0,0,.6), 97 | 1px 1px 0 rgba(0,0,0,.6); 98 | } 99 | 100 | .search-query-highlight { 101 | border-bottom: 1px solid black; 102 | font-weight: bold; 103 | } 104 | 105 | .root-dark-mode .search-query-highlight { 106 | border-bottom: 1px solid white; 107 | } 108 | 109 | .url-pid-link { 110 | opacity: .6; 111 | } 112 | 113 | :root { 114 | --coloredspan-bgcolor-light: white; 115 | --coloredspan-bgcolor-dark: black; 116 | } 117 | 118 | .colored-span { 119 | background-color: var(--coloredspan-bgcolor-light); 120 | } 121 | 122 | .root-dark-mode .colored-span { 123 | background-color: var(--coloredspan-bgcolor-dark); 124 | } 125 | 126 | .icon+label { 127 | font-size: .9em; 128 | vertical-align: .05em; 129 | cursor: inherit; 130 | padding: 0 .1rem; 131 | margin-left: .1rem; 132 | } 133 | 134 | code.pre { 135 | white-space: pre; 136 | } 137 | 138 | .reply-nameplate { 139 | margin-right: .25rem; 140 | font-size: .85em; 141 | vertical-align: .07em; 142 | white-space: nowrap; 143 | } -------------------------------------------------------------------------------- /src/text_splitter.js: -------------------------------------------------------------------------------- 1 | // regexp should match the WHOLE segmented part 2 | export const BARE_PID_RE=/(^|[^\d\u20e3\ufe0e\ufe0f])([1-9]\d{4,5})(?![\d\u20e3\ufe0e\ufe0f])/g; 3 | export const PFX_PID_RE=/(\$[1-9]\d{4,5})(?![\d\u20e3\ufe0e\ufe0f])/g; 4 | export const URL_PID_RE=/((?:https?:\/\/)?_BRAND_WWW_DOMAIN\/?#(?:#|%23)([1-9]\d{4,5}))(?!\d|\u20e3|\ufe0e|\ufe0f)/g; 5 | export const NORM_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|THU|Undefined|Valuable|Wifeless|Xenial|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; 6 | export const BRG_NICKNAME_RE=/(^|[^A-Za-z])((?:(?:Angry|Baby|Crazy|Diligent|Excited|Fat|Grievous|Hungry|Interesting|Jolly|Kind|Little|Magic|Naïve|Old|PKU|Quiet|Rich|Spencer|THU|Undefined|Valuable|Wifeless|Xenial|Young|Zombie)\s)?(?:Alice|Bob|Carol|Dave|Eve|Francis|Grace|Hermione|Isabella|Jason|Kate|Luke|Margaret|Nathan|Olivia|Paul|Queen|Richard|Susan|Thomas|Uma|Voldemort|Winnie|Xander|Yasmine|Zach)|You Win(?: \d+)?|洞主)(?![A-Za-z])/gi; 7 | 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; 8 | 9 | // https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex 10 | function escape_regex(string) { 11 | return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string 12 | } 13 | 14 | function build_highlight_re(txt,split,option='g') { 15 | return txt ? new RegExp(`(${txt.split(split).filter((x)=>!!x).map(escape_regex).join('|')})`,option) : /^$/g; 16 | } 17 | 18 | export function build_hl_rules(search_term=null, bridge=false) { 19 | let ret=[]; 20 | 21 | // ↑ higher priority 22 | if(!bridge) ret.push(['url_pid',URL_PID_RE]); 23 | 24 | ret.push(['url',URL_RE]); 25 | 26 | ret.push(['pid_prefixed',PFX_PID_RE]); 27 | 28 | if(!bridge) ret.push(['pid_bare',BARE_PID_RE]); 29 | 30 | if(bridge) ret.push(['nickname',BRG_NICKNAME_RE]); 31 | else ret.push(['nickname',NORM_NICKNAME_RE]); 32 | 33 | if(search_term) ret.push(['search',build_highlight_re(search_term,' ','gi')]); 34 | 35 | return ret; 36 | } 37 | 38 | export function clean_pid(s) { 39 | if(s.charAt(0)==='#' || s.charAt(0)==='$') return parseInt(s.substr(1)); 40 | else return parseInt(s); 41 | } 42 | 43 | export function split_text(txt,rules) { 44 | // rules: [['name',/regex/],...] 45 | // return: [['name','part'],[null,'part'],...] 46 | 47 | txt=[[null,txt]]; 48 | rules.forEach((rule)=>{ 49 | let [name,regex]=rule; 50 | txt=[].concat.apply([],txt.map((part)=>{ 51 | let [rule,content]=part; 52 | if(rule) // already tagged by previous rules 53 | return [part]; 54 | else { 55 | //console.log(txt,content,name); 56 | return content 57 | .split(regex) 58 | .map((seg)=>( 59 | regex.test(seg) ? [name,seg] : [null,seg] 60 | )) 61 | .filter(([name,seg])=>( 62 | name!==null || seg 63 | )); 64 | } 65 | })); 66 | }); 67 | return txt; 68 | } 69 | -------------------------------------------------------------------------------- /src/PressureHelper.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import Pressure from 'pressure'; 3 | 4 | import './PressureHelper.css'; 5 | 6 | const THRESHOLD=.4; 7 | const MULTIPLIER=25; 8 | const BORDER_WIDTH=500; // also change css! 9 | 10 | export class PressureHelper extends Component { 11 | constructor(props) { 12 | super(props); 13 | this.state={ 14 | level: 0, 15 | fired: false, 16 | }; 17 | this.callback=props.callback; 18 | this.esc_interval=null; 19 | } 20 | 21 | do_fire() { 22 | if(this.esc_interval) { 23 | clearInterval(this.esc_interval); 24 | this.esc_interval=null; 25 | } 26 | this.setState({ 27 | level: 1, 28 | fired: true, 29 | }); 30 | this.callback(); 31 | window.setTimeout(()=>{ 32 | this.setState({ 33 | level: 0, 34 | fired: false, 35 | }); 36 | },300); 37 | } 38 | 39 | componentDidMount() { 40 | if(window.config.back_shortcut) { 41 | Pressure.set(document.body, { 42 | change: (force)=>{ 43 | if(!this.state.fired) { 44 | if(force>=.999) { 45 | this.do_fire(); 46 | } 47 | else 48 | this.setState({ 49 | level: force, 50 | }); 51 | } 52 | }, 53 | end: ()=>{ 54 | this.setState({ 55 | level: 0, 56 | fired: false, 57 | }); 58 | }, 59 | }, { 60 | polyfill: false, 61 | only: 'touch', 62 | preventSelect: false, 63 | }); 64 | 65 | document.addEventListener('keydown',(e)=>{ 66 | if(!e.repeat && e.key==='Escape') { 67 | if(this.esc_interval) 68 | clearInterval(this.esc_interval); 69 | this.setState({ 70 | level: THRESHOLD/2, 71 | },()=>{ 72 | this.esc_interval=setInterval(()=>{ 73 | let new_level=this.state.level+.1; 74 | if(new_level>=.999) 75 | this.do_fire(); 76 | else 77 | this.setState({ 78 | level: new_level, 79 | }); 80 | },30); 81 | }); 82 | } 83 | }); 84 | document.addEventListener('keyup',(e)=>{ 85 | if(e.key==='Escape') { 86 | if(this.esc_interval) { 87 | clearInterval(this.esc_interval); 88 | this.esc_interval=null; 89 | } 90 | this.setState({ 91 | level: 0, 92 | }); 93 | } 94 | }); 95 | } 96 | } 97 | 98 | render() { 99 | const pad=MULTIPLIER*(this.state.level-THRESHOLD)-BORDER_WIDTH; 100 | return ( 101 |
111 | ) 112 | } 113 | } -------------------------------------------------------------------------------- /src/Sidebar.css: -------------------------------------------------------------------------------- 1 | .sidebar-shadow { 2 | will-change: opacity; 3 | opacity: 0; 4 | background-color: black; 5 | pointer-events: none; 6 | transition: opacity 150ms ease-out; 7 | position: fixed; 8 | left: 0; 9 | top: 0; 10 | height: 100%; 11 | width: 100%; 12 | z-index: 20; 13 | } 14 | .sidebar-on .sidebar-shadow { 15 | opacity: .3; 16 | pointer-events: initial; 17 | } 18 | .sidebar-on .sidebar-shadow:active { 19 | opacity: .5; 20 | transition: unset; 21 | } 22 | 23 | .root-dark-mode .sidebar-on .sidebar-shadow { 24 | opacity: .65; 25 | } 26 | .root-dark-mode .sidebar-on .sidebar-shadow:active { 27 | opacity: .8; 28 | } 29 | 30 | .sidebar { 31 | user-select: text; 32 | position: fixed; 33 | top: 0; 34 | height: 100%; 35 | background-color: rgba(218,218,218,.9); 36 | overflow-y: auto; 37 | padding-top: 3em; 38 | padding-bottom: 1em; 39 | backdrop-filter: blur(5px); 40 | } 41 | 42 | .root-no-blur .sidebar { 43 | backdrop-filter: unset; 44 | } 45 | 46 | .root-dark-mode .sidebar { 47 | background-color: hsla(0,0%,5%,.75); 48 | } 49 | 50 | .sidebar, .sidebar-title { 51 | left: 700px; 52 | will-change: opacity, transform; 53 | z-index: 21; 54 | width: calc(100% - 700px); 55 | color: black; 56 | } 57 | .root-dark-mode .sidebar, .root-dark-mode .sidebar-title { 58 | color: var(--foreground-dark); 59 | } 60 | 61 | .sidebar-on .sidebar, .sidebar-on .sidebar-title { 62 | opacity: 1; 63 | animation: sidebar-fadein .12s cubic-bezier(0.15, 0.4, 0.6, 1); 64 | } 65 | .sidebar-off .sidebar, .sidebar-off .sidebar-title { 66 | visibility: hidden; 67 | pointer-events: none; 68 | backdrop-filter: none; 69 | animation: sidebar-fadeout .2s cubic-bezier(0.15, 0.4, 0.6, 1); 70 | } 71 | .sidebar-container { 72 | animation: sidebar-initial .25s linear; /* skip initial animation */ 73 | } 74 | 75 | @keyframes sidebar-fadeout { 76 | from { 77 | visibility: visible; 78 | opacity: 1; 79 | transform: none; 80 | backdrop-filter: none; 81 | } 82 | to { 83 | visibility: visible; 84 | opacity: 0; 85 | transform: translateX(200px); 86 | backdrop-filter: none; 87 | } 88 | } 89 | @keyframes sidebar-fadein { 90 | from { 91 | opacity: 0; 92 | transform: translateX(120px); 93 | backdrop-filter: none; 94 | } 95 | to { 96 | opacity: 1; 97 | transform: none; 98 | backdrop-filter: none; 99 | } 100 | } 101 | @keyframes sidebar-initial { 102 | from {opacity: 0;} 103 | to {opacity: 0;} 104 | } 105 | 106 | .sidebar-title { 107 | text-shadow: 0 0 3px white; 108 | font-weight: bold; 109 | position: fixed; 110 | width: 100%; 111 | top: 0; 112 | line-height: 3em; 113 | padding-left: .5em; 114 | background-color: rgba(250,250,250,.8); 115 | pointer-events: none; 116 | backdrop-filter: blur(5px); 117 | box-shadow: 0 3px 5px rgba(0,0,0,.2); 118 | } 119 | 120 | .root-no-blur .sidebar-title { 121 | backdrop-filter: unset; 122 | } 123 | 124 | .root-dark-mode .sidebar-title { 125 | background-color: hsla(0,0%,18%,.85); 126 | text-shadow: 0 0 3px black; 127 | } 128 | 129 | .sidebar-title a { 130 | pointer-events: initial; 131 | } 132 | 133 | .sidebar, .sidebar-title { 134 | padding-left: 1em; 135 | padding-right: 1em; 136 | } 137 | 138 | @media screen and (max-width: 1300px) { 139 | .sidebar, .sidebar-title { 140 | left: calc(100% - 550px); 141 | width: 550px; 142 | padding-left: .5em; 143 | padding-right: .5em; 144 | } 145 | } 146 | @media screen and (max-width: 580px) { 147 | .sidebar, .sidebar-title { 148 | left: 27px; 149 | width: calc(100% - 27px); 150 | padding-left: .25em; 151 | padding-right: .25em; 152 | } 153 | } 154 | 155 | .sidebar-flow-item { 156 | display: block; 157 | } 158 | .sidebar-flow-item .box { 159 | width: 100%; 160 | } -------------------------------------------------------------------------------- /src/Welcome.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | 3 | import {GATEWAY_DOMAIN} from './Common'; 4 | import {get_json} from './flows_api'; 5 | import {bgimg_style} from './Config'; 6 | 7 | import './Welcome.css'; 8 | 9 | export function LandingPage(props) { 10 | let [username,set_username]=useState(''); 11 | let [password,set_password]=useState(''); 12 | let [loading,set_loading]=useState(false); 13 | 14 | function do_login() { 15 | set_loading(true); 16 | fetch(GATEWAY_DOMAIN+'/api/legacy/login',{ 17 | method: 'post', 18 | headers: { 19 | 'Content-Type': 'application/json', 20 | }, 21 | body: JSON.stringify({ 22 | email: username, 23 | password: password, 24 | }), 25 | }) 26 | .then(get_json) 27 | .then((json)=>{ 28 | if(json.error) 29 | throw new Error(json.error_msg||json.error); 30 | 31 | set_loading(false); 32 | props.do_login(json.user_token); 33 | }) 34 | .catch((e)=>{ 35 | alert('登录失败。'+e); 36 | set_loading(false); 37 | }); 38 | } 39 | 40 | function on_keypress(e) { 41 | if(e.key==='Enter') 42 | do_login(); 43 | } 44 | 45 | return ( 46 |
47 |
48 |
49 |
50 |
51 |
52 |

53 | _BRAND_NAME 54 |

55 |

56 | _BRAND_SLOGAN 57 |

58 |
59 |
60 |
61 |
62 |
63 |
64 |

65 | 66 | 新用户注册 67 | 68 |

69 |

70 | _BRAND_REGISTER_SLOGAN 71 |

72 |
73 |
74 |

75 | 已经有账号? 76 |

77 |

78 | 82 |

83 |

84 | 88 |

89 |

90 | 93 |     94 |

95 |

96 | 97 | 忘记密码 98 | 99 |

100 |
101 |
102 |
103 |
104 |

_BRAND_LANDING_HEADER

105 |

106 | _BRAND_LANDING_BODY 107 |

108 |

_BRAND_LANDING_HEADER

109 |

110 | _BRAND_LANDING_BODY 111 |

112 |

113 | 114 | 立即注册,_BRAND_SLOGAN 115 | 116 |

117 |
118 |
119 |
120 |
121 | ); 122 | } -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // This optional code is used to register a service worker. 4 | // register() is not called by default. 5 | 6 | // This lets the app load faster on subsequent visits in production, and gives 7 | // it offline capabilities. However, it also means that developers (and users) 8 | // will only see deployed updates on subsequent visits to a page, after all the 9 | // existing tabs open on the page have been closed, since previously cached 10 | // resources are updated in the background. 11 | 12 | // To learn more about the benefits of this model and instructions on how to 13 | // opt-in, read http://bit.ly/CRA-PWA 14 | 15 | const isLocalhost = Boolean( 16 | window.location.hostname === 'localhost' || 17 | // [::1] is the IPv6 localhost address. 18 | window.location.hostname === '[::1]' || 19 | // 127.0.0.1/8 is considered localhost for IPv4. 20 | window.location.hostname.match( 21 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 22 | ) 23 | ); 24 | 25 | export function register(config) { 26 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 27 | // The URL constructor is available in all browsers that support SW. 28 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 29 | if (publicUrl.origin !== window.location.origin) { 30 | // Our service worker won't work if PUBLIC_URL is on a different origin 31 | // from what our page is served on. This might happen if a CDN is used to 32 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 33 | return; 34 | } 35 | 36 | window.addEventListener('load', () => { 37 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 38 | 39 | if (isLocalhost) { 40 | // This is running on localhost. Let's check if a service worker still exists or not. 41 | checkValidServiceWorker(swUrl, config); 42 | 43 | // Add some additional logging to localhost, pointing developers to the 44 | // service worker/PWA documentation. 45 | navigator.serviceWorker.ready.then(() => { 46 | console.log( 47 | 'This web app is being served cache-first by a service ' + 48 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 49 | ); 50 | }); 51 | } else { 52 | // Is not localhost. Just register service worker 53 | registerValidSW(swUrl, config); 54 | } 55 | }); 56 | } 57 | } 58 | 59 | function registerValidSW(swUrl, config) { 60 | navigator.serviceWorker 61 | .register(swUrl) 62 | .then(registration => { 63 | registration.onupdatefound = () => { 64 | const installingWorker = registration.installing; 65 | if (installingWorker == null) { 66 | return; 67 | } 68 | installingWorker.onstatechange = () => { 69 | if (installingWorker.state === 'installed') { 70 | if (navigator.serviceWorker.controller) { 71 | if(window._webhole_show_alert) 72 | window._webhole_show_alert('pwa-update',{ 73 | type: 'info', 74 | is_html: false, 75 | message: '发现新版本,下次访问时将更新', 76 | }); 77 | setTimeout(()=>{ 78 | installingWorker.postMessage({type: 'SKIP_WAITING'}); 79 | },1000); 80 | 81 | // At this point, the updated precached content has been fetched, 82 | // but the previous service worker will still serve the older 83 | // content until all client tabs are closed. 84 | console.log( 85 | 'New content is available and will be used when all ' + 86 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 87 | ); 88 | 89 | // Execute callback 90 | if (config && config.onUpdate) { 91 | config.onUpdate(registration); 92 | } 93 | } else { 94 | // At this point, everything has been precached. 95 | // It's the perfect time to display a 96 | // "Content is cached for offline use." message. 97 | console.log('Content is cached for offline use.'); 98 | 99 | // Execute callback 100 | if (config && config.onSuccess) { 101 | config.onSuccess(registration); 102 | } 103 | } 104 | } 105 | }; 106 | }; 107 | }) 108 | .catch(error => { 109 | console.error('Error during service worker registration:', error); 110 | }); 111 | } 112 | 113 | function checkValidServiceWorker(swUrl, config) { 114 | // Check if the service worker can be found. If it can't reload the page. 115 | fetch(swUrl) 116 | .then(response => { 117 | // Ensure service worker exists, and that we really are getting a JS file. 118 | const contentType = response.headers.get('content-type'); 119 | if ( 120 | response.status === 404 || 121 | (contentType != null && contentType.indexOf('javascript') === -1) 122 | ) { 123 | // No service worker found. Probably a different app. Reload the page. 124 | navigator.serviceWorker.ready.then(registration => { 125 | registration.unregister().then(() => { 126 | window.location.reload(); 127 | }); 128 | }); 129 | } else { 130 | // Service worker found. Proceed as normal. 131 | registerValidSW(swUrl, config); 132 | } 133 | }) 134 | .catch(() => { 135 | console.log( 136 | 'No internet connection found. Root is running in offline mode.' 137 | ); 138 | }); 139 | } 140 | 141 | export function unregister() { 142 | if ('serviceWorker' in navigator) { 143 | navigator.serviceWorker.ready.then(registration => { 144 | registration.unregister(); 145 | }); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/flows_api.js: -------------------------------------------------------------------------------- 1 | import {HAPI_DOMAIN} from './Common'; 2 | import {cache} from './cache'; 3 | 4 | export function token_param(token) { 5 | return ( 6 | '&jsapiver='+encodeURIComponent((process.env.REACT_APP_BUILD_INFO||'null')+'-'+(Math.floor(+new Date()/7200000)*2))+ 7 | (token ? ('&user_token='+token) : '') 8 | ); 9 | } 10 | 11 | export function get_json(res) { 12 | if(!res.ok) throw Error(`网络错误 ${res.status} ${res.statusText}`); 13 | return ( 14 | res 15 | .text() 16 | .then((t)=>{ 17 | try { 18 | return JSON.parse(t); 19 | } catch(e) { 20 | console.error('json parse error'); 21 | console.trace(e); 22 | console.log(t); 23 | throw new SyntaxError('JSON Parse Error '+t.substr(0,50)); 24 | } 25 | }) 26 | ); 27 | } 28 | 29 | function add_variant(li) { 30 | li.forEach((item)=>{ 31 | item.variant={}; 32 | }); 33 | } 34 | export const API={ 35 | load_replies: (pid,token,color_picker)=>{ 36 | pid=parseInt(pid); 37 | return fetch( 38 | HAPI_DOMAIN+'/api/holes/view/'+pid+ 39 | '?role=reply'+ // add a pseudo parameter because token_param starts with '&' 40 | token_param(token) 41 | ) 42 | .then(get_json) 43 | .then((json)=>{ 44 | if(json.error) { 45 | throw new Error(json.error_msg||json.error); 46 | } 47 | 48 | cache().put(pid,json.post_data.hot,json); 49 | 50 | // also change load_replies_with_cache! 51 | json.post_data.variant={}; 52 | json.data=json.data 53 | .map((info)=>{ 54 | info._display_color=color_picker.get(info.name); 55 | info.variant={}; 56 | return info; 57 | }); 58 | 59 | return json; 60 | }); 61 | }, 62 | 63 | load_replies_with_cache: (pid,token,color_picker,cache_version)=> { 64 | pid=parseInt(pid); 65 | return cache().get(pid,cache_version) 66 | .then(([json,reason])=>{ 67 | if(json) { 68 | // also change load_replies! 69 | json.post_data.variant={}; 70 | json.data=json.data 71 | .map((info)=>{ 72 | info._display_color=color_picker.get(info.name); 73 | info.variant={}; 74 | return info; 75 | }); 76 | 77 | return json; 78 | } 79 | else { 80 | return API.load_replies(pid,token,color_picker).then((json)=>{ 81 | if(reason==='expired') 82 | json.post_data.variant.new_reply=true; 83 | return json; 84 | }); 85 | } 86 | }); 87 | }, 88 | 89 | set_attention: (pid,attention,token)=>{ 90 | return fetch( 91 | HAPI_DOMAIN+'/api/holes/attention/do/'+encodeURIComponent(pid)+ 92 | '?switch='+(attention?'1':'0')+ 93 | token_param(token), 94 | {method: 'PUT'} 95 | ) 96 | .then(get_json) 97 | .then((json)=>{ 98 | cache().delete(pid); 99 | if(json.error) { 100 | alert(json.error_msg||json.error); 101 | throw new Error(json.error); 102 | } 103 | json.data.variant={}; 104 | return json; 105 | }); 106 | }, 107 | 108 | report: (item_type,id,report_type,reason,token)=>{ 109 | if(item_type!=='hole' && item_type!=='comment') throw Error('bad type'); 110 | return fetch(HAPI_DOMAIN+'/api/'+item_type+'s/flag/'+id+'?role=report'+token_param(token), { 111 | method: 'POST', 112 | headers: { 113 | 'Content-Type': 'application/json', 114 | }, 115 | body: JSON.stringify({ 116 | content: reason, 117 | type: report_type, 118 | }), 119 | }) 120 | .then(get_json) 121 | .then((json)=>{ 122 | if(json.error) 123 | throw new Error(json.error_msg||json.error); 124 | 125 | return json; 126 | }); 127 | }, 128 | 129 | get_list: (after,token)=>{ 130 | return fetch( 131 | HAPI_DOMAIN+'/api/holes/list/'+(after||0)+ 132 | '?limit=30'+ 133 | token_param(token) 134 | ) 135 | .then(get_json) 136 | .then((json)=>{ 137 | if(json.error) 138 | throw new Error(json.error_msg||json.error); 139 | 140 | add_variant(json.data); 141 | return json; 142 | }); 143 | }, 144 | 145 | get_search: (after,keyword,token)=>{ 146 | return fetch( 147 | HAPI_DOMAIN+'/api/holes/search/'+after+ 148 | '?keywords='+encodeURIComponent(keyword)+ 149 | token_param(token) 150 | ) 151 | .then(get_json) 152 | .then((json)=>{ 153 | if(json.error) 154 | throw new Error(json.error_msg||json.error); 155 | 156 | add_variant(json.data); 157 | return json; 158 | }); 159 | }, 160 | 161 | get_attention: (after,token)=>{ 162 | return fetch( 163 | HAPI_DOMAIN+'/api/holes/attention/'+after+ 164 | '?limit=30'+ 165 | token_param(token) 166 | ) 167 | .then(get_json) 168 | .then((json)=>{ 169 | if(json.error) 170 | throw new Error(json.error_msg||json.error); 171 | 172 | add_variant(json.data); 173 | return json; 174 | }); 175 | }, 176 | }; -------------------------------------------------------------------------------- /src/Title.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PureComponent} from 'react'; 2 | import {InfoSidebar, PostForm} from './UserAction'; 3 | import {TokenCtx} from './UserAction'; 4 | 5 | import './Title.css'; 6 | 7 | const flag_re=/^\/\/setflag ([a-zA-Z0-9_]+)=(.*)$/; 8 | 9 | class ControlBar extends PureComponent { 10 | constructor(props) { 11 | super(props); 12 | this.state={ 13 | search_text: '', 14 | }; 15 | this.set_mode=props.set_mode; 16 | 17 | this.on_change_bound=this.on_change.bind(this); 18 | this.on_keypress_bound=this.on_keypress.bind(this); 19 | this.do_refresh_bound=this.do_refresh.bind(this); 20 | this.do_attention_bound=this.do_attention.bind(this); 21 | } 22 | 23 | componentDidMount() { 24 | if(window.location.hash) { 25 | let text=decodeURIComponent(window.location.hash).substr(1); 26 | if(text.lastIndexOf('?')!==-1) 27 | text=text.substr(0,text.lastIndexOf('?')); // fuck wechat '#param?nsukey=...' 28 | this.setState({ 29 | search_text: text, 30 | }, ()=>{ 31 | this.on_keypress({key: 'Enter'}); 32 | }); 33 | } 34 | } 35 | 36 | on_change(event) { 37 | this.setState({ 38 | search_text: event.target.value, 39 | }); 40 | } 41 | 42 | on_keypress(event) { 43 | if(event.key==='Enter') { 44 | let flag_res=flag_re.exec(this.state.search_text); 45 | if(flag_res) { 46 | if(flag_res[2]) { 47 | localStorage[flag_res[1]]=flag_res[2]; 48 | alert('Set Flag '+flag_res[1]+'='+flag_res[2]+'\nYou may need to refresh this webpage.'); 49 | } else { 50 | delete localStorage[flag_res[1]]; 51 | alert('Clear Flag '+flag_res[1]+'\nYou may need to refresh this webpage.'); 52 | } 53 | return; 54 | } 55 | 56 | event.target.blur(); 57 | 58 | if(!this.state.search_text) { 59 | this.set_mode('list',null); 60 | return; 61 | } 62 | 63 | const mode=/^[#$]\d+$/.test(this.state.search_text) ? 'single' : 'search'; 64 | this.set_mode(mode,this.state.search_text||''); 65 | } 66 | } 67 | 68 | do_refresh() { 69 | window.scrollTo(0,0); 70 | this.setState({ 71 | search_text: '', 72 | }); 73 | this.set_mode('list',null); 74 | } 75 | 76 | do_attention() { 77 | window.scrollTo(0,0); 78 | this.setState({ 79 | search_text: '', 80 | }); 81 | this.set_mode('attention',null); 82 | } 83 | 84 | render() { 85 | return ( 86 | {({value: token})=>( 87 | 125 | )} 126 | ) 127 | } 128 | } 129 | 130 | export function Title(props) { 131 | return ( 132 |
133 | {/**/} 134 |
135 |
136 |
137 |

138 | props.show_sidebar( 139 | '_BRAND_NAME', 140 | 141 | )}> 142 | _BRAND_NAME 143 | 144 |

145 |

146 | _BRAND_SLOGAN 147 |

148 |
149 | 150 |
151 |
152 | ) 153 | } -------------------------------------------------------------------------------- /src/cache.js: -------------------------------------------------------------------------------- 1 | const HOLE_CACHE_DB_NAME='webhole_cache_db'; 2 | const CACHE_DB_VER=1; 3 | const MAINTENANCE_STEP=150; 4 | const MAINTENANCE_COUNT=1000; 5 | 6 | //const ENC_KEY=42; 7 | 8 | class Cache { 9 | constructor() { 10 | this.db=null; 11 | this.added_items_since_maintenance=0; 12 | this.encrypt=this.encrypt.bind(this); 13 | this.decrypt=this.decrypt.bind(this); 14 | const open_req=indexedDB.open(HOLE_CACHE_DB_NAME,CACHE_DB_VER); 15 | open_req.onerror=console.error.bind(console); 16 | open_req.onupgradeneeded=(event)=>{ 17 | console.log('comment cache db upgrade'); 18 | const db=event.target.result; 19 | const store=db.createObjectStore('comment',{ 20 | keyPath: 'pid', 21 | }); 22 | store.createIndex('last_access','last_access',{unique: false}); 23 | }; 24 | open_req.onsuccess=(event)=>{ 25 | console.log('comment cache db loaded'); 26 | this.db=event.target.result; 27 | setTimeout(this.maintenance.bind(this),1); 28 | }; 29 | } 30 | 31 | // use window.hole_cache.encrypt() only after cache is loaded! 32 | encrypt(pid,data) { 33 | let s=JSON.stringify(data); 34 | return s; 35 | /* 36 | let o=''; 37 | for(let i=0,key=(ENC_KEY^pid)%128;i{ 74 | if(!this.db) 75 | return resolve([null,'fail']); 76 | let get_req,store; 77 | try { 78 | const tx=this.db.transaction(['comment'],'readwrite'); 79 | store=tx.objectStore('comment'); 80 | get_req=store.get(pid); 81 | } catch(e) { // ios sometimes fail at here, just ignore it 82 | console.error(e); 83 | resolve([null,'fail']); 84 | } 85 | get_req.onsuccess=()=>{ 86 | let res=get_req.result; 87 | if(!res || !res.data_str) { 88 | //console.log('comment cache miss',pid); 89 | resolve([null,'miss']); 90 | } else if(target_version===res.version) { // hit 91 | console.log('comment cache hit',pid); 92 | res.last_access=(+new Date()); 93 | store.put(res); 94 | let data=this.decrypt(pid,res.data_str); 95 | resolve([data,'hit']); // obj or null 96 | } else { // expired 97 | console.log('comment cache expired',pid,': ver',res.version,'target',target_version); 98 | store.delete(pid); 99 | resolve([null,'expired']); 100 | } 101 | }; 102 | get_req.onerror=(e)=>{ 103 | console.warn('comment cache indexeddb open failed'); 104 | console.error(e); 105 | resolve([null,'fail']); 106 | }; 107 | }); 108 | } 109 | 110 | put(pid,target_version,data) { 111 | pid=parseInt(pid); 112 | return new Promise((resolve,reject)=>{ 113 | if(!this.db) 114 | return resolve(); 115 | try { 116 | const tx=this.db.transaction(['comment'],'readwrite'); 117 | const store=tx.objectStore('comment'); 118 | store.put({ 119 | pid: pid, 120 | version: target_version, 121 | data_str: this.encrypt(pid,data), 122 | last_access: +new Date(), 123 | }); 124 | if(++this.added_items_since_maintenance===MAINTENANCE_STEP) 125 | setTimeout(this.maintenance.bind(this),1); 126 | } catch(e) { 127 | console.error(e); 128 | return resolve(); 129 | } 130 | }); 131 | } 132 | 133 | delete(pid) { 134 | pid=parseInt(pid); 135 | return new Promise((resolve,reject)=>{ 136 | if(!this.db) 137 | return resolve(); 138 | let req; 139 | try { 140 | const tx=this.db.transaction(['comment'],'readwrite'); 141 | const store=tx.objectStore('comment'); 142 | req=store.delete(pid); 143 | } catch(e) { 144 | console.error(e); 145 | return resolve(); 146 | } 147 | //console.log('comment cache delete',pid); 148 | req.onerror=()=>{ 149 | console.warn('comment cache delete failed ',pid); 150 | return resolve(); 151 | }; 152 | req.onsuccess=()=>resolve(); 153 | }); 154 | } 155 | 156 | maintenance() { 157 | if(!this.db) 158 | return; 159 | const tx=this.db.transaction(['comment'],'readwrite'); 160 | const store=tx.objectStore('comment'); 161 | let count_req=store.count(); 162 | count_req.onsuccess=()=>{ 163 | let count=count_req.result; 164 | if(count>MAINTENANCE_COUNT) { 165 | console.log('comment cache db maintenance',count); 166 | store.index('last_access').openKeyCursor().onsuccess=(e)=>{ 167 | let cur=e.target.result; 168 | if(cur) { 169 | //console.log('maintenance: delete',cur); 170 | store.delete(cur.primaryKey); 171 | if(--count>MAINTENANCE_COUNT) 172 | cur.continue(); 173 | } 174 | }; 175 | } else { 176 | console.log('comment cache db no need to maintenance',count); 177 | } 178 | this.added_items_since_maintenance=0; 179 | }; 180 | count_req.onerror=console.error.bind(console); 181 | } 182 | 183 | clear() { 184 | if(!this.db) 185 | return; 186 | try { 187 | indexedDB.deleteDatabase(HOLE_CACHE_DB_NAME); 188 | console.log('delete comment cache db'); 189 | } catch(e) { 190 | console.error(e); 191 | } 192 | } 193 | }; 194 | 195 | export function cache() { 196 | if(!window.hole_cache) 197 | window.hole_cache=new Cache(); 198 | return window.hole_cache; 199 | } -------------------------------------------------------------------------------- /src/Flows.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --box-bgcolor-light: hsl(0,0%,97%); 3 | --box-bgcolor-dark: hsl(0,0%,16%); 4 | } 5 | 6 | .box { 7 | background-color: var(--box-bgcolor-light); 8 | color: black; 9 | border-radius: 5px; 10 | margin: 1em 0; 11 | padding: .5em; 12 | box-shadow: 0 2px 5px rgba(0,0,0,.4); 13 | } 14 | 15 | .root-dark-mode .box { 16 | background-color: var(--box-bgcolor-dark); 17 | color: var(--foreground-dark); 18 | box-shadow: 0 0 2px rgba(255,255,255,.25), 0 0 7px rgba(0,0,0,.15); 19 | } 20 | 21 | .box-tip { 22 | min-width: 100px; 23 | z-index: 1; 24 | text-align: center; 25 | } 26 | 27 | .box-danger { 28 | background-color: #d44; 29 | color: white; 30 | text-shadow: 0 0 2px black; 31 | } 32 | .root-dark-mode .box-danger { 33 | background-color: #c44; 34 | color: var(--foreground-dark); 35 | } 36 | 37 | .box-warning { 38 | background-color: #db8e14; 39 | color: white; 40 | text-shadow: 0 0 2px black; 41 | } 42 | .root-dark-mode .box-warning { 43 | background-color: #b47209; 44 | text-shadow: 0 0 2px black; 45 | } 46 | 47 | .box-danger a, .box-warning a { 48 | color: #ddf; 49 | } 50 | .box-danger a:hover, .box-warning a:hover { 51 | border-bottom: 1px solid #ddf; 52 | } 53 | 54 | .left-container .flow-item { 55 | display: inline-block; 56 | width: 600px; 57 | float: left; 58 | } 59 | 60 | .flow-reply-row { 61 | display: inline-flex; 62 | align-items: flex-start; 63 | width: calc(100% - 625px); 64 | margin-left: -25px; 65 | padding-left: 18px; 66 | overflow-x: auto; 67 | } 68 | 69 | .sidebar-flow-item .flow-item pre, .sidebar-flow-item .flow-reply pre { 70 | cursor: text; 71 | } 72 | 73 | .flow-reply-row::-webkit-scrollbar { 74 | display: none; 75 | } 76 | .flow-reply-row { 77 | scrollbar-width: none; 78 | -ms-overflow-style: none; 79 | } 80 | 81 | .flow-reply-row:empty { 82 | margin: 0 !important; 83 | display: none; 84 | } 85 | 86 | .flow-item-row::after { 87 | content: ""; 88 | display: block; 89 | clear: both; 90 | } 91 | 92 | .left-container .flow-reply { 93 | flex: 0 0 300px; 94 | max-height: 12.5em; 95 | margin-right: -7px; 96 | overflow-y: hidden; 97 | } 98 | 99 | .left-container .flow-item { 100 | margin-left: 50px; 101 | } 102 | 103 | @media screen and (min-width: 1301px) { 104 | .left-container .flow-item-row-with-prompt:hover::before { 105 | content: '>>'; 106 | position: absolute; 107 | left: 10px; 108 | margin-top: 1.5em; 109 | color: white; 110 | text-shadow: /* copied from .black-outline */ 111 | -1px -1px 0 rgba(0,0,0,.6), 112 | 0 -1px 0 rgba(0,0,0,.6), 113 | 1px -1px 0 rgba(0,0,0,.6), 114 | -1px 1px 0 rgba(0,0,0,.6), 115 | 0 1px 0 rgba(0,0,0,.6), 116 | 1px 1px 0 rgba(0,0,0,.6); 117 | font-family: 'Consolas', 'Courier', monospace; 118 | } 119 | } 120 | 121 | @media screen and (max-width: 1300px) { 122 | .left-container .flow-item { 123 | margin-left: 10px; 124 | } 125 | 126 | .flow-reply-row { 127 | width: calc(100% - 485px); 128 | } 129 | 130 | .left-container .flow-item { 131 | width: 500px; 132 | } 133 | 134 | .flow-item-row:hover::before { 135 | display: none; 136 | } 137 | } 138 | 139 | @media screen and (max-width: 900px) { 140 | .left-container .flow-item { 141 | display: block; 142 | width: calc(100vw - 20px); 143 | max-width: 500px; 144 | float: none; 145 | } 146 | 147 | .flow-reply-row { 148 | display: flex; 149 | width: 100% !important; 150 | margin-left: 0; 151 | padding-left: 30px; 152 | margin-top: -2.5em; 153 | margin-bottom: -1em; 154 | } 155 | } 156 | 157 | .left-container .flow-item-row { 158 | cursor: default; 159 | } 160 | 161 | .box-header, .box-footer { 162 | font-size: .8em; 163 | } 164 | 165 | .flow-item-row p.img { 166 | text-align: center; 167 | margin: .5em 5px 0 5px; 168 | } 169 | .flow-item-row p.img img { 170 | max-width: 100%; 171 | box-shadow: 0 1px 5px rgba(0,0,0,.4); 172 | } 173 | 174 | .left-container .flow-item-row p.img img { 175 | max-height: calc(25vh + 15em); 176 | } 177 | .sidebar .flow-item p.img img { 178 | max-height: 160vh; 179 | } 180 | .sidebar .flow-reply p.img img { 181 | max-height: calc(20vh + 10em); 182 | } 183 | 184 | .root-dark-mode .flow-item-row p.img img { 185 | filter: brightness(85%); 186 | } 187 | 188 | .box-header-badge { 189 | float: right; 190 | margin: 0 .5em; 191 | } 192 | 193 | .flow-item-dot { 194 | position: relative; 195 | top: calc(-.5em - 3px); 196 | left: calc(-.5em - 3px); 197 | width: 9px; 198 | height: 9px; 199 | margin-bottom: -9px; 200 | border-radius: 50%; 201 | box-shadow: 1px 1px 5px rgba(0,0,0,.5); 202 | display: none; 203 | } 204 | 205 | .flow-item-dot-post { 206 | background-color: #ffcc77; 207 | } 208 | .root-dark-mode .flow-item-dot-post { 209 | background-color: #eebb66; 210 | } 211 | 212 | .flow-item-dot-comment { 213 | background-color: #aaddff; 214 | } 215 | .root-dark-mode .flow-item-dot-comment { 216 | background-color: #99ccee; 217 | } 218 | 219 | .left-container .flow-item-dot { 220 | display: block; 221 | } 222 | 223 | .box-content { 224 | padding: .5em 0; 225 | } 226 | 227 | .left-container .box-content { 228 | max-height: calc(100vh + 15em); 229 | overflow-y: hidden; 230 | } 231 | 232 | .box-id { 233 | color: #666666; 234 | } 235 | 236 | .root-dark-mode .box-id { 237 | color: #bbbbbb; 238 | } 239 | 240 | .box-id a:hover::before { 241 | content: "复制全文"; 242 | position: relative; 243 | width: 5em; 244 | height: 1.3em; 245 | line-height: 1.3em; 246 | margin-bottom: -1.3em; 247 | border-radius: 3px; 248 | text-align: center; 249 | top: -1.5em; 250 | display: block; 251 | color: white; 252 | background-color: rgba(0,0,0,.6); 253 | pointer-events: none; 254 | } 255 | 256 | .flow-item-row-quote { 257 | opacity: .9; 258 | filter: brightness(90%); 259 | } 260 | 261 | .root-dark-mode .flow-item-row-quote { 262 | opacity: .7; 263 | filter: unset; 264 | } 265 | 266 | .flow-item-quote>.box { 267 | margin-left: 2.5em; 268 | max-height: 15em; 269 | overflow-y: hidden; 270 | } 271 | 272 | .flow-item-quote .flow-item-dot, 273 | .flow-item-quote .box-id a:hover::before { 274 | display: none; 275 | } 276 | 277 | .quote-tip { 278 | margin-top: .5em; 279 | margin-bottom: -10em; /* so that it will not block reply bar */ 280 | float: left; 281 | display: flex; 282 | flex-direction: column; 283 | width: 2.5em; 284 | text-align: center; 285 | color: white; 286 | } 287 | 288 | .box-header-tag { 289 | color: white; 290 | background-color: #00c; 291 | font-weight: bold; 292 | border-radius: 3px; 293 | margin-right: .25em; 294 | padding: 0 .25em; 295 | } 296 | 297 | .root-dark-mode .box-header-tag { 298 | background-color: #00a; 299 | } 300 | 301 | .filter-name-bar { 302 | animation: slide-in-from-top .15s ease-out; 303 | position: sticky; 304 | top: 1em; 305 | } 306 | 307 | @keyframes slide-in-from-top { 308 | 0% {opacity: 0; transform: translateY(-50%);} 309 | 100% {opacity: 1;} 310 | } 311 | 312 | .reply-header-badge { 313 | float: right; 314 | padding: .5em .2em .5em .2em; 315 | margin: -.5em -.2em -.5em .2em; 316 | opacity: .45; 317 | } 318 | .reply-header-badge:hover { 319 | opacity: 1; 320 | } 321 | 322 | p.img { 323 | position: relative; 324 | } 325 | .img-placeholder { 326 | background-color: #888; 327 | } 328 | .img-real { 329 | position: absolute; 330 | box-shadow: unset; /* will show in .img-placeholder */ 331 | } 332 | 333 | .flow-variant-warning { 334 | color: red; 335 | font-weight: bold; 336 | } 337 | 338 | .report-toolbar button { 339 | line-height: 2em; 340 | margin: .2em .5em .2em 0; 341 | min-width: 4em; 342 | } 343 | .report-reason { 344 | font-size: .9em; 345 | } 346 | 347 | .flow-hint { 348 | opacity: .6; 349 | font-size: .8em; 350 | } 351 | 352 | .box-header-text { 353 | opacity: .6; 354 | } -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {SwitchTransition, CSSTransition} from 'react-transition-group'; 3 | import {Flow, load_single_meta} from './Flows'; 4 | import {Title} from './Title'; 5 | import {Alerts} from './Alerts'; 6 | import {Sidebar} from './Sidebar'; 7 | import {PressureHelper} from './PressureHelper'; 8 | import {TokenCtx} from './UserAction'; 9 | import {load_config,bgimg_style} from './Config'; 10 | import {HAPI_DOMAIN} from './Common'; 11 | import {LandingPage} from './Welcome'; 12 | import {cache} from './cache'; 13 | import {get_json} from './flows_api'; 14 | 15 | import './App.css'; 16 | 17 | const MAX_SIDEBAR_STACK_SIZE=10; 18 | const USER_INFO_REFRESH_INTV_MS=60000; 19 | 20 | function listen_darkmode(override) { // override: true/false/undefined 21 | function update_color_scheme() { 22 | if(override===undefined ? window.matchMedia('(prefers-color-scheme: dark)').matches : override) 23 | document.body.classList.add('root-dark-mode'); 24 | else 25 | document.body.classList.remove('root-dark-mode'); 26 | } 27 | 28 | update_color_scheme(); 29 | window.matchMedia('(prefers-color-scheme: dark)').addListener(()=>{ 30 | update_color_scheme(); 31 | }); 32 | } 33 | 34 | const DEFAULT_USER_INFO={entitlements: [], alerts: [], version: '...', remember_token: null}; 35 | 36 | class App extends Component { 37 | constructor(props) { 38 | super(props); 39 | load_config(); 40 | listen_darkmode({default: undefined, light: false, dark: true}[window.config.color_scheme]); 41 | this.state={ 42 | sidebar_stack: [[null,null]], // list of [status, content] 43 | mode: 'list', // list, single, search, attention 44 | search_text: null, 45 | flow_render_key: +new Date(), 46 | user_token: localStorage['WEBHOLE_TOKEN']||null, 47 | user_info: DEFAULT_USER_INFO, 48 | user_info_status: 'loading', 49 | user_info_lasttime: -USER_INFO_REFRESH_INTV_MS, 50 | }; 51 | this.show_sidebar_bound=this.show_sidebar.bind(this); 52 | this.set_mode_bound=this.set_mode.bind(this); 53 | this.on_pressure_bound=this.on_pressure.bind(this); 54 | } 55 | 56 | componentDidMount() { 57 | cache(); // init db first 58 | 59 | window._webhole_show_hole=(...args)=>{ 60 | // delay execution so user_token always gets the latest value 61 | load_single_meta(this.show_sidebar.bind(this),this.state.user_token)(...args); 62 | }; 63 | 64 | if(this.state.user_token) 65 | this.get_user_info(this.state.user_token); 66 | 67 | if(!window.config.blur_effect) 68 | document.body.classList.add('root-no-blur'); 69 | } 70 | 71 | get_user_info(token) { 72 | this.setState({ 73 | //user_info: DEFAULT_USER_INFO, 74 | user_info_status: 'loading', 75 | }); 76 | console.log('get user info'); 77 | fetch(HAPI_DOMAIN+'/api/users/info?user_token='+encodeURIComponent(token)) 78 | .then(get_json) 79 | .then((res)=>{ 80 | if(res.error) 81 | throw new Error(res.error_msg||res.error) 82 | 83 | this.setState({ 84 | user_info: res, 85 | user_info_status: 'done', 86 | user_info_lasttime: (+new Date()), 87 | }); 88 | }) 89 | .catch((e)=>{ 90 | console.error('failed to load user info',e); 91 | this.setState({ 92 | user_info_status: 'failed', 93 | }); 94 | }); 95 | } 96 | 97 | do_login(token) { 98 | this.setState({user_token: token}); 99 | localStorage['WEBHOLE_TOKEN']=token; 100 | this.get_user_info(token); 101 | } 102 | 103 | static is_darkmode() { 104 | if(window.config.color_scheme==='dark') return true; 105 | if(window.config.color_scheme==='light') return false; 106 | else { // 'default' 107 | return window.matchMedia('(prefers-color-scheme: dark)').matches; 108 | } 109 | } 110 | 111 | on_pressure() { 112 | if(this.state.sidebar_stack.length>1) 113 | this.show_sidebar(null,null,'clear'); 114 | else 115 | this.set_mode('list',null); 116 | } 117 | 118 | show_sidebar(title,content,mode='push') { 119 | this.setState((prevState)=>{ 120 | let ns=prevState.sidebar_stack.slice(); 121 | if(mode==='push') { 122 | if(ns.length>MAX_SIDEBAR_STACK_SIZE) 123 | ns.splice(1,1); 124 | ns=ns.concat([[title,content]]); 125 | } else if(mode==='pop') { 126 | if(ns.length===1) return; 127 | ns.pop(); 128 | } else if(mode==='replace') { 129 | ns.pop(); 130 | ns=ns.concat([[title,content]]); 131 | } else if(mode==='clear') { 132 | ns=[[null,null]]; 133 | } else 134 | throw new Error('bad show_sidebar mode'); 135 | return { 136 | sidebar_stack: ns, 137 | }; 138 | }); 139 | } 140 | 141 | set_mode(mode,search_text) { 142 | this.setState({ 143 | mode: mode, 144 | search_text: search_text, 145 | flow_render_key: +new Date(), 146 | }); 147 | if((+new Date())-this.state.user_info_lasttime>USER_INFO_REFRESH_INTV_MS && this.state.user_token && this.state.user_info_status!=='loading') 148 | this.get_user_info(this.state.user_token); 149 | } 150 | 151 | render() { 152 | if(!this.state.user_token) 153 | return ( 154 | 155 | ); 156 | 157 | return ( 158 | { 164 | delete localStorage['WEBHOLE_TOKEN']; 165 | this.setState({ 166 | user_token: null, 167 | user_info: DEFAULT_USER_INFO, 168 | }); 169 | }, 170 | }}> 171 | 172 |
173 | 174 | <TokenCtx.Consumer>{(token)=>( 175 | <div className="left-container"> 176 | {this.state.user_info_status==='failed' && 177 | <div className="flow-item-row"> 178 | <div className="flow-item box box-warning"> 179 | 用户信息加载失败, 180 | <a onClick={()=>this.get_user_info(token.value)}>点击重试</a> 181 | </div> 182 | </div> 183 | } 184 | <Alerts token={token.value} info={this.state.user_info} /> 185 | <SwitchTransition mode="out-in"> 186 | <CSSTransition key={this.state.flow_render_key} timeout={100} classNames="flows-anim"> 187 | <Flow key={this.state.flow_render_key} show_sidebar={this.show_sidebar_bound} 188 | mode={this.state.mode} search_text={this.state.search_text} token={token.value} 189 | /> 190 | </CSSTransition> 191 | </SwitchTransition> 192 | <br /> 193 | </div> 194 | )}</TokenCtx.Consumer> 195 | <Sidebar show_sidebar={this.show_sidebar_bound} stack={this.state.sidebar_stack} /> 196 | </TokenCtx.Provider> 197 | ); 198 | } 199 | } 200 | 201 | export default App; 202 | -------------------------------------------------------------------------------- /src/Config.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PureComponent} from 'react'; 2 | 3 | import './Config.css'; 4 | 5 | const BUILTIN_IMGS={ 6 | 'static/bg/default.png': '_BRAND_DEFAULT_BGIMG_NAME', 7 | }; 8 | 9 | const DEFAULT_CONFIG={ 10 | background_img: 'static/bg/default.jpg', 11 | background_color: '#445', 12 | back_shortcut: false, 13 | easter_egg: true, 14 | color_scheme: 'default', 15 | blur_effect: false, 16 | }; 17 | 18 | export function load_config() { 19 | let config=Object.assign({},DEFAULT_CONFIG); 20 | let loaded_config; 21 | try { 22 | loaded_config=JSON.parse(localStorage['WEBHOLE_CONFIG']||'{}'); 23 | // config migration can be added here 24 | } catch(e) { 25 | alert('设置加载失败,将重置为默认设置!\n'+e); 26 | delete localStorage['hole_config']; 27 | loaded_config={}; 28 | } 29 | 30 | // unrecognized configs are removed 31 | Object.keys(loaded_config).forEach((key)=>{ 32 | if(config[key]!==undefined) 33 | config[key]=loaded_config[key]; 34 | }); 35 | 36 | console.log('config loaded',config); 37 | window.config=config; 38 | } 39 | export function save_config() { 40 | localStorage['WEBHOLE_CONFIG']=JSON.stringify(window.config); 41 | load_config(); 42 | } 43 | 44 | export function bgimg_style(img,color) { 45 | if(img===undefined) img=window.config.background_img; 46 | if(color===undefined) color=window.config.background_color; 47 | return { 48 | background: 'transparent center center', 49 | backgroundImage: img===null ? 'unset' : 'url("'+encodeURI(img)+'")', 50 | backgroundColor: color, 51 | backgroundSize: 'cover', 52 | }; 53 | } 54 | 55 | class ConfigBackground extends PureComponent { 56 | constructor(props) { 57 | super(props); 58 | this.state={ 59 | img: window.config.background_img, 60 | color: window.config.background_color, 61 | }; 62 | } 63 | 64 | save_changes() { 65 | this.props.callback({ 66 | background_img: this.state.img, 67 | background_color: this.state.color, 68 | }); 69 | } 70 | 71 | on_select(e) { 72 | let value=e.target.value; 73 | this.setState({ 74 | img: value==='##other' ? '' : 75 | value==='##color' ? null : value, 76 | },this.save_changes.bind(this)); 77 | } 78 | on_change_img(e) { 79 | this.setState({ 80 | img: e.target.value, 81 | },this.save_changes.bind(this)); 82 | } 83 | on_change_color(e) { 84 | this.setState({ 85 | color: e.target.value, 86 | },this.save_changes.bind(this)); 87 | } 88 | 89 | render() { 90 | let img_select= this.state.img===null ? '##color' : 91 | Object.keys(BUILTIN_IMGS).indexOf(this.state.img)===-1 ? '##other' : this.state.img; 92 | return ( 93 | <div> 94 | <p> 95 | <b>背景图片:</b> 96 | <select value={img_select} onChange={this.on_select.bind(this)}> 97 | {Object.keys(BUILTIN_IMGS).map((key)=>( 98 | <option key={key} value={key}>{BUILTIN_IMGS[key]}</option> 99 | ))} 100 | <option value="##other">输入图片网址……</option> 101 | <option value="##color">纯色背景……</option> 102 | </select> 103 |   104 | {img_select==='##other' && 105 | <input type="url" placeholder="图片网址" value={this.state.img} onChange={this.on_change_img.bind(this)} /> 106 | } 107 | {img_select==='##color' && 108 | <input type="color" value={this.state.color} onChange={this.on_change_color.bind(this)} /> 109 | } 110 | </p> 111 | <div className="bg-preview" style={bgimg_style(this.state.img,this.state.color)} /> 112 | </div> 113 | ); 114 | } 115 | } 116 | 117 | class ConfigColorScheme extends PureComponent { 118 | constructor(props) { 119 | super(props); 120 | this.state={ 121 | color_scheme: window.config.color_scheme, 122 | }; 123 | } 124 | 125 | save_changes() { 126 | this.props.callback({ 127 | color_scheme: this.state.color_scheme, 128 | }); 129 | } 130 | 131 | on_select(e) { 132 | let value=e.target.value; 133 | this.setState({ 134 | color_scheme: value, 135 | },this.save_changes.bind(this)); 136 | } 137 | 138 | render() { 139 | return ( 140 | <div> 141 | <p> 142 | <b>夜间模式:</b> 143 | <select value={this.state.color_scheme} onChange={this.on_select.bind(this)}> 144 | <option value="default">跟随系统</option> 145 | <option value="light">始终浅色模式</option> 146 | <option value="dark">始终深色模式</option> 147 | </select> 148 |   <small>#color_scheme</small> 149 | </p> 150 | <p> 151 | 选择浅色或深色模式,深色模式下将会调暗图片亮度 152 | </p> 153 | </div> 154 | ) 155 | } 156 | } 157 | 158 | class ConfigSwitch extends PureComponent { 159 | constructor(props) { 160 | super(props); 161 | this.state={ 162 | switch: window.config[this.props.id], 163 | }; 164 | } 165 | 166 | on_change(e) { 167 | let val=e.target.checked; 168 | this.setState({ 169 | switch: val, 170 | },()=>{ 171 | this.props.callback({ 172 | [this.props.id]: val, 173 | }); 174 | }); 175 | } 176 | 177 | render() { 178 | return ( 179 | <div> 180 | <p> 181 | <label> 182 | <input name={'config-'+this.props.id} type="checkbox" checked={this.state.switch} onChange={this.on_change.bind(this)} /> 183 | <b>{this.props.name}</b> 184 |   <small>#{this.props.id}</small> 185 | </label> 186 | </p> 187 | <p> 188 | {this.props.description} 189 | </p> 190 | </div> 191 | ); 192 | } 193 | } 194 | 195 | export class ConfigUI extends PureComponent { 196 | constructor(props) { 197 | super(props); 198 | this.save_changes_bound=this.save_changes.bind(this); 199 | } 200 | 201 | save_changes(chg) { 202 | console.log(chg); 203 | Object.keys(chg).forEach((key)=>{ 204 | window.config[key]=chg[key]; 205 | }); 206 | save_config(); 207 | } 208 | 209 | reset_settings() { 210 | if(window.confirm('重置所有设置?')) { 211 | window.config={}; 212 | save_config(); 213 | window.location.reload(); 214 | } 215 | } 216 | 217 | render() { 218 | return ( 219 | <div> 220 | <div className="box config-ui-header"> 221 | <p>这些功能仍在测试,可能不稳定(<a onClick={this.reset_settings.bind(this)}>全部重置</a>)</p> 222 | <p><b>修改设置后 <a onClick={()=>{window.location.reload()}}>刷新页面</a> 方可生效</b></p> 223 | </div> 224 | <div className="box"> 225 | <ConfigBackground callback={this.save_changes_bound} /> 226 | <hr /> 227 | <ConfigColorScheme callback={this.save_changes_bound} /> 228 | <hr /> 229 | <ConfigSwitch callback={this.save_changes_bound} id="back_shortcut" name="快速返回" 230 | description="短暂按住 Esc 键或重压屏幕(3D Touch)可以快速返回或者刷新树洞" 231 | /> 232 | <hr /> 233 | <ConfigSwitch callback={this.save_changes_bound} id="blur_effect" name="使用模糊效果" 234 | description="对界面元素采用模糊背景效果,更好看,但在某些设备上可能导致卡顿问题" 235 | /> 236 | <hr /> 237 | <ConfigSwitch callback={this.save_changes_bound} id="easter_egg" name="允许彩蛋" 238 | description="在某些情况下显示彩蛋" 239 | /> 240 | </div> 241 | </div> 242 | ) 243 | } 244 | } -------------------------------------------------------------------------------- /src/Bifrost.js: -------------------------------------------------------------------------------- 1 | import React, {PureComponent, useState, useContext, useEffect} from 'react'; 2 | import {TokenCtx} from './UserAction'; 3 | 4 | import {HAPI_DOMAIN} from './Common'; 5 | import {token_param, get_json} from './flows_api'; 6 | 7 | import './Bifrost.css'; 8 | 9 | function PortletSetTag(props) { 10 | let [content,set_content]=useState(props.info.tag||''); 11 | let [loading,set_loading]=useState(false); 12 | 13 | function submit() { 14 | set_loading(true); 15 | fetch(HAPI_DOMAIN+'/api/'+(props.is_reply?'comment':'hole')+'s/tag/'+props.id+'?role=tag'+token_param(props.token), { 16 | method: 'post', 17 | headers: { 18 | 'Content-Type': 'application/json', 19 | }, 20 | body: JSON.stringify({ 21 | content: content, 22 | }), 23 | }) 24 | .then(get_json) 25 | .then((json)=>{ 26 | set_loading(false); 27 | if(json.error) 28 | alert(json.error_msg || json.error); 29 | else 30 | alert('提交成功'); 31 | }) 32 | .catch((e)=>{ 33 | alert('Error: '+e); 34 | set_loading(false); 35 | }); 36 | } 37 | 38 | return ( 39 | <div> 40 | <button onClick={submit} disabled={loading}>提交</button> 41 | <input value={content} onChange={(e)=>set_content(e.target.value)} /> 42 | </div> 43 | ); 44 | } 45 | 46 | function PortletSetText(props) { 47 | let [content,set_content]=useState(props.info.text); 48 | let [loading,set_loading]=useState(false); 49 | 50 | function submit() { 51 | set_loading(true); 52 | fetch(HAPI_DOMAIN+'/api/'+(props.is_reply?'comment':'hole')+'s/edit/'+props.id+'?role=text'+token_param(props.token), { 53 | method: 'post', 54 | headers: { 55 | 'Content-Type': 'application/json', 56 | }, 57 | body: JSON.stringify({ 58 | text: content, 59 | }), 60 | }) 61 | .then(get_json) 62 | .then((json)=>{ 63 | set_loading(false); 64 | if(json.error) 65 | alert(json.error_msg || json.error); 66 | else 67 | alert('提交成功'); 68 | }) 69 | .catch((e)=>{ 70 | alert('Error: '+e); 71 | set_loading(false); 72 | }); 73 | } 74 | 75 | return ( 76 | <div> 77 | <button onClick={submit} disabled={loading}>提交</button> 78 | <br /> 79 | <textarea value={content} onChange={(e)=>set_content(e.target.value)} /> 80 | </div> 81 | ); 82 | } 83 | 84 | function PortletSetExtra(props) { 85 | let [type,set_type]=useState(props.info.type); 86 | let [extra,set_extra]=useState(props.info.extra||''); 87 | let [loading,set_loading]=useState(false); 88 | 89 | function submit() { 90 | set_loading(true); 91 | fetch(HAPI_DOMAIN+'/api/'+(props.is_reply?'comment':'hole')+'s/edit/'+props.id+'?role=extra'+token_param(props.token), { 92 | method: 'post', 93 | headers: { 94 | 'Content-Type': 'application/json', 95 | }, 96 | body: JSON.stringify({ 97 | type: type, 98 | extra: extra, 99 | }), 100 | }) 101 | .then(get_json) 102 | .then((json)=>{ 103 | set_loading(false); 104 | if(json.error) 105 | alert(json.error_msg || json.error); 106 | else 107 | alert('提交成功'); 108 | }) 109 | .catch((e)=>{ 110 | alert('Error: '+e); 111 | set_loading(false); 112 | }); 113 | } 114 | 115 | return ( 116 | <div> 117 | <button onClick={submit} disabled={loading}>提交</button> 118 | <input value={type} list="hole-type-completer" onChange={(e)=>set_type(e.target.value)} /> 119 | <datalist id="hole-type-completer"> 120 | <option value="text" /> 121 | <option value="image" /> 122 | <option value="html" /> 123 | </datalist> 124 | <br /> 125 | <textarea value={extra} onChange={(e)=>set_extra(e.target.value)} /> 126 | </div> 127 | ); 128 | } 129 | 130 | function PortletViewFlag(props) { 131 | let [res,set_res]=useState('loading...'); 132 | 133 | useEffect(()=>{ 134 | fetch(HAPI_DOMAIN+'/api/'+(props.is_reply?'comment':'hole')+'s/flag/'+props.id+'?role=viewflag'+token_param(props.token)) 135 | .then(get_json) 136 | .then((json)=>{ 137 | set_res(JSON.stringify(json,null,2)); 138 | }) 139 | .catch((e)=>{ 140 | set_res('Error: '+e); 141 | }); 142 | },[]); 143 | 144 | return ( 145 | <div style={{overflowX: 'auto'}}> 146 | <code className="pre">{res}</code> 147 | </div> 148 | ) 149 | } 150 | 151 | function PortletUnban(props) { 152 | let [flag_unfold,set_flag_unfold]=useState(false); 153 | let [flag_undel,set_flag_undel]=useState(false); 154 | let [flag_unban,set_flag_unban]=useState(true); 155 | let [reason,set_reason]=useState(''); 156 | let [loading,set_loading]=useState(false); 157 | 158 | function submit() { 159 | set_loading(true); 160 | fetch(HAPI_DOMAIN+'/api/'+(props.is_reply?'comment':'hole')+'s/unflag/'+props.id+'?role=unflag'+token_param(props.token), { 161 | method: 'post', 162 | headers: { 163 | 'Content-Type': 'application/json', 164 | }, 165 | body: JSON.stringify({ 166 | unfold: flag_unfold, 167 | undelete: flag_undel, 168 | unban: flag_unban, 169 | reason: reason, 170 | }), 171 | }) 172 | .then(get_json) 173 | .then((json)=>{ 174 | set_loading(false); 175 | if(json.error) 176 | alert(json.error_msg || json.error); 177 | else 178 | alert('提交成功'); 179 | }) 180 | .catch((e)=>{ 181 | alert('Error: '+e); 182 | set_loading(false); 183 | }); 184 | } 185 | 186 | return ( 187 | <div> 188 | <button onClick={submit} disabled={loading}>提交</button> 189 | <label> 190 | <input type="checkbox" checked={flag_unfold} onChange={(e)=>set_flag_unfold(e.target.checked)} /> 191 | 取消折叠 192 | </label>  193 | <label> 194 | <input type="checkbox" checked={flag_undel} onChange={(e)=>set_flag_undel(e.target.checked)} /> 195 | 取消删除 196 | </label>  197 | <label> 198 | <input type="checkbox" checked={flag_unban} onChange={(e)=>set_flag_unban(e.target.checked)} /> 199 | 解禁用户 200 | </label>  201 | <input value={reason} onChange={(e)=>set_reason(e.target.value)} placeholder="原因" /> 202 | </div> 203 | ) 204 | } 205 | 206 | export function should_show_bifrost_bar(perm) { 207 | return perm.Tagging || perm.EditingText || perm.EditingTypeExtra || perm.ViewingFlags || perm.UndoBan; 208 | } 209 | 210 | export function BifrostBar(props) { 211 | let {perm,value: token}=useContext(TokenCtx); 212 | let [mode,set_mode]=useState(null); 213 | 214 | let id=(props.is_reply ? props.info.cid : props.info.pid); 215 | 216 | let modes=[]; 217 | if(perm.Tagging) modes.push('set_tag'); 218 | if(perm.EditingText) modes.push('set_text'); 219 | if(perm.EditingTypeExtra) modes.push('set_extra'); 220 | if(perm.ViewingFlags) modes.push('view_flag'); 221 | if(perm.UndoBan) modes.push('unban'); 222 | 223 | const widgets={ 224 | set_tag: PortletSetTag, 225 | set_text: PortletSetText, 226 | set_extra: PortletSetExtra, 227 | view_flag: PortletViewFlag, 228 | unban: PortletUnban, 229 | }; 230 | const names={ 231 | set_tag: '修改Tag', 232 | set_text: '修改内容', 233 | set_extra: '修改附加内容', 234 | view_flag: '查看举报', 235 | unban: '解除删帖', 236 | }; 237 | let Widget; 238 | 239 | if(modes.length===0) 240 | return ( 241 | <div className="interactive flow-item-toolbar bifrost-toolbar"> 242 | <button onClick={()=>props.set_variant({show_bifrost: false})}>关闭</button> 243 |   没有可用的操作 244 | </div> 245 | ); 246 | else { 247 | Widget=widgets[mode]||(()=>null); 248 | return ( 249 | <div className="interactive flow-item-toolbar bifrost-toolbar"> 250 | <p className="bifrost-portlet-selector"> 251 | <button onClick={()=>props.set_variant({show_bifrost: false})} style={{float: 'right'}}>关闭</button> 252 | {modes.map((m)=>( 253 | <button key={m} onClick={()=>set_mode(m)} disabled={mode===m}> 254 | {names[m]} 255 | </button> 256 | ))} 257 | </p> 258 | <Widget {...props} perm={perm} id={id} token={token} /> 259 | </div> 260 | ) 261 | } 262 | } -------------------------------------------------------------------------------- /src/Common.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PureComponent} from 'react'; 2 | import TimeAgo from 'react-timeago'; 3 | import buildFormatter from 'react-timeago/lib/formatters/buildFormatter'; 4 | 5 | import './Common.css'; 6 | import {clean_pid} from './text_splitter'; 7 | 8 | export function TitleLine(props) { 9 | return ( 10 | <p className="centered-line title-line aux-margin"> 11 | <span className="black-outline">{props.text}</span> 12 | </p> 13 | ) 14 | } 15 | 16 | export const HAPI_DOMAIN=window.__WEBHOLE_HAPI_DOMAIN||'https://_BRAND_HAPI_DOMAIN'; 17 | export const GATEWAY_DOMAIN=window.__WEBHOLE_GATEWAY_DOMAIN||'https://_BRAND_GATEWAY_DOMAIN'; 18 | 19 | function pad2(x) { 20 | return x<10 ? '0'+x : ''+x; 21 | } 22 | export function format_time(time) { 23 | return `${time.getMonth()+1}-${pad2(time.getDate())} ${time.getHours()}:${pad2(time.getMinutes())}:${pad2(time.getSeconds())}`; 24 | } 25 | const chinese_format=buildFormatter({ 26 | prefixAgo: null, 27 | prefixFromNow: '未来', 28 | suffixAgo: '前', 29 | suffixFromNow: null, 30 | seconds: '不到1分钟', 31 | minute: '1分钟', 32 | minutes: '%d分钟', 33 | hour: '1小时', 34 | hours: '%d小时', 35 | day: '1天', 36 | days: '%d天', 37 | month: '1个月', 38 | months: '%d月', 39 | year: '1年', 40 | years: '%d年', 41 | 42 | wordSeparator: '', 43 | }); 44 | export function Time(props) { 45 | let {stamp,...others}=props; 46 | const time=new Date(stamp*1000); 47 | return ( 48 | <span {...others}> 49 | <TimeAgo date={time} formatter={chinese_format} title={time.toLocaleString('zh-CN', { 50 | timeZone: 'Asia/Shanghai', 51 | hour12: false, 52 | })} /> 53 |   54 | {format_time(time)} 55 | </span> 56 | ); 57 | } 58 | 59 | 60 | export function ColoredSpan(props) { 61 | return ( 62 | <span className="colored-span" style={{ 63 | '--coloredspan-bgcolor-light': props.colors[0], 64 | '--coloredspan-bgcolor-dark': props.colors[1], 65 | }}>{props.children}</span> 66 | ) 67 | } 68 | 69 | export class HighlightedText extends PureComponent { 70 | render() { 71 | function normalize_url(url) { 72 | return /^https?:\/\//.test(url) ? url : 'http://'+url; 73 | } 74 | return ( 75 | <pre> 76 | {this.props.parts.map((part,idx)=>{ 77 | let [rule,p]=part; 78 | return ( 79 | <span key={idx}>{ 80 | rule==='url_pid' ? <span className="url-pid-link" title={p}>/##</span> : 81 | rule==='url' ? <a href={normalize_url(p)} target="_blank" rel="noopener">{p}</a> : 82 | rule==='pid_bare' ? <a href={'##'+p} onClick={(e)=>{e.preventDefault(); this.props.show_pid(p);}}>{p}</a> : 83 | rule==='pid_prefixed' ? (()=>{ 84 | let pp=clean_pid(p); 85 | return (<a href={'##'+pp} onClick={(e)=>{e.preventDefault(); this.props.show_pid(pp);}}>{p}</a>); 86 | })() : 87 | rule==='nickname' ? <ColoredSpan colors={this.props.color_picker.get(p)}>{p}</ColoredSpan> : 88 | rule==='search' ? <span className="search-query-highlight">{p}</span> : 89 | rule==='reply_nameplate' ? <span className="reply-nameplate">[{p}]</span> : 90 | p 91 | }</span> 92 | ); 93 | })} 94 | </pre> 95 | ) 96 | } 97 | } 98 | 99 | window.TEXTAREA_BACKUP={}; 100 | 101 | export class SafeTextarea extends Component { 102 | constructor(props) { 103 | super(props); 104 | this.state={ 105 | text: '', 106 | }; 107 | this.on_change_bound=this.on_change.bind(this); 108 | this.on_keydown_bound=this.on_keydown.bind(this); 109 | this.clear=this.clear.bind(this); 110 | this.area_ref=React.createRef(); 111 | this.change_callback=props.on_change||(()=>{}); 112 | this.submit_callback=props.on_submit||(()=>{}); 113 | } 114 | 115 | componentDidMount() { 116 | this.setState({ 117 | text: window.TEXTAREA_BACKUP[this.props.id]||'' 118 | },()=>{ 119 | this.change_callback(this.state.text); 120 | }); 121 | } 122 | 123 | componentWillUnmount() { 124 | window.TEXTAREA_BACKUP[this.props.id]=this.state.text; 125 | this.change_callback(this.state.text); 126 | } 127 | 128 | on_change(event) { 129 | this.setState({ 130 | text: event.target.value, 131 | }); 132 | this.change_callback(event.target.value); 133 | } 134 | on_keydown(event) { 135 | if(event.key==='Enter' && event.ctrlKey && !event.altKey) { 136 | event.preventDefault(); 137 | this.submit_callback(); 138 | } 139 | } 140 | 141 | clear() { 142 | this.setState({ 143 | text: '', 144 | }); 145 | } 146 | set(text) { 147 | this.change_callback(text); 148 | this.setState({ 149 | text: text, 150 | }); 151 | } 152 | get() { 153 | return this.state.text; 154 | } 155 | focus() { 156 | this.area_ref.current.focus(); 157 | } 158 | 159 | render() { 160 | return ( 161 | <textarea ref={this.area_ref} onChange={this.on_change_bound} value={this.state.text} onKeyDown={this.on_keydown_bound} /> 162 | ) 163 | } 164 | } 165 | 166 | let pwa_prompt_event=null; 167 | window.addEventListener('beforeinstallprompt', (e) => { 168 | console.log('pwa: received before install prompt'); 169 | pwa_prompt_event=e; 170 | }); 171 | 172 | export function PromotionBar(props) { 173 | let is_ios=/iPhone|iPad|iPod/i.test(window.navigator.userAgent); 174 | let is_installed=(window.matchMedia('(display-mode: standalone)').matches) || (window.navigator.standalone); 175 | 176 | if(is_installed) 177 | return null; 178 | 179 | if(is_ios) 180 | // noinspection JSConstructorReturnsPrimitive 181 | return !navigator.standalone ? ( 182 | <div className="box promotion-bar"> 183 | <span className="icon icon-about" />  184 | 用 Safari 把_BRAND_NAME <b>添加到主屏幕</b> 更好用 185 | </div> 186 | ) : null; 187 | else 188 | // noinspection JSConstructorReturnsPrimitive 189 | return pwa_prompt_event ? ( 190 | <div className="box promotion-bar"> 191 | <span className="icon icon-about" />  192 | 把_BRAND_NAME <b><a onClick={()=>{ 193 | if(pwa_prompt_event) 194 | pwa_prompt_event.prompt(); 195 | }}>安装到桌面</a></b> 更好用 196 | </div> 197 | ) : null; 198 | } 199 | 200 | export function BrowserWarningBar(props) { 201 | let cr_version=/Chrome\/(\d+)/.exec(navigator.userAgent); 202 | cr_version=cr_version?cr_version[1]:0; 203 | if(/MicroMessenger\/|QQ\//.test(navigator.userAgent)) 204 | return ( 205 | <div className="box box-tip box-warning"> 206 | <b>您正在使用 QQ/微信 内嵌浏览器</b> 207 | <br /> 208 | 建议使用系统浏览器打开,否则可能出现兼容问题 209 | </div> 210 | ); 211 | if(/Edge\/1/.test(navigator.userAgent)) 212 | return ( 213 | <div className="box box-tip box-warning"> 214 | <b>您正在使用旧版 Microsoft Edge</b> 215 | <br /> 216 | 建议使用新版 Edge,否则可能出现兼容问题 217 | </div> 218 | ); 219 | else if(cr_version>1 && cr_version<57) 220 | return ( 221 | <div className="box box-tip box-warning"> 222 | <b>您正在使用古老的 Chrome {cr_version}</b> 223 | <br /> 224 | 建议使用新版浏览器,否则可能出现兼容问题 225 | </div> 226 | ); 227 | return null; 228 | } 229 | 230 | export class ClickHandler extends PureComponent { 231 | constructor(props) { 232 | super(props); 233 | this.state={ 234 | moved: true, 235 | init_y: 0, 236 | init_x: 0, 237 | }; 238 | this.on_begin_bound=this.on_begin.bind(this); 239 | this.on_move_bound=this.on_move.bind(this); 240 | this.on_end_bound=this.on_end.bind(this); 241 | 242 | this.MOVE_THRESHOLD=3; 243 | this.last_fire=0; 244 | } 245 | 246 | on_begin(e) { 247 | //console.log('click',(e.touches?e.touches[0]:e).screenY,(e.touches?e.touches[0]:e).screenX); 248 | this.setState({ 249 | moved: false, 250 | init_y: (e.touches?e.touches[0]:e).screenY, 251 | init_x: (e.touches?e.touches[0]:e).screenX, 252 | }); 253 | } 254 | on_move(e) { 255 | if(!this.state.moved) { 256 | 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); 257 | //console.log('move',mvmt); 258 | if(mvmt>this.MOVE_THRESHOLD) 259 | this.setState({ 260 | moved: true, 261 | }); 262 | } 263 | } 264 | on_end(event) { 265 | //console.log('end'); 266 | if(!this.state.moved) 267 | this.do_callback(event); 268 | this.setState({ 269 | moved: true, 270 | }); 271 | } 272 | 273 | do_callback(event) { 274 | if(this.last_fire+100>+new Date()) return; 275 | this.last_fire=+new Date(); 276 | this.props.callback(event); 277 | } 278 | 279 | render() { 280 | return ( 281 | <div onTouchStart={this.on_begin_bound} onMouseDown={this.on_begin_bound} 282 | onTouchMove={this.on_move_bound} onMouseMove={this.on_move_bound} 283 | onClick={this.on_end_bound} > 284 | {this.props.children} 285 | </div> 286 | ) 287 | } 288 | } -------------------------------------------------------------------------------- /src/fonts_9/icomoon.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" standalone="no"?> 2 | <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" > 3 | <svg xmlns="http://www.w3.org/2000/svg"> 4 | <metadata>Generated by IcoMoon</metadata> 5 | <defs> 6 | <font id="icomoon" horiz-adv-x="1024"> 7 | <font-face units-per-em="1024" ascent="960" descent="-64" /> 8 | <missing-glyph horiz-adv-x="1024" /> 9 | <glyph unicode=" " horiz-adv-x="512" d="" /> 10 | <glyph unicode="" glyph-name="send" horiz-adv-x="1025" d="M1008 944.571c12-8.571 17.714-22.286 15.429-36.571l-146.286-877.714c-1.714-10.857-8.571-20-18.286-25.714-5.143-2.857-11.429-4.571-17.714-4.571-4.571 0-9.143 1.143-13.714 2.857l-258.857 105.714-138.286-168.571c-6.857-8.571-17.143-13.143-28-13.143-4 0-8.571 0.571-12.571 2.286-14.286 5.143-24 18.857-24 34.286v199.429l493.714 605.143-610.857-528.571-225.714 92.571c-13.143 5.143-21.714 17.143-22.857 31.429-0.571 13.714 6.286 26.857 18.286 33.714l950.857 548.571c5.714 3.429 12 5.143 18.286 5.143 7.429 0 14.857-2.286 20.571-6.286z" /> 11 | <glyph unicode="" glyph-name="pen" d="M1018.17 668.11l-286.058 286.058c-9.334 9.334-21.644 7.234-27.356-4.666l-38.354-79.904 267.198-267.198 79.904 38.354c11.9 5.712 14 18.022 4.666 27.356zM615.384 824.616l-263.384-21.95c-17.5-2.166-32.080-5.898-37.090-28.752-0.006-0.024-0.012-0.042-0.018-0.066-71.422-343.070-314.892-677.848-314.892-677.848l57.374-57.374 271.986 271.99c-5.996 12.53-9.36 26.564-9.36 41.384 0 53.020 42.98 96 96 96s96-42.98 96-96-42.98-96-96-96c-14.82 0-28.852 3.364-41.384 9.36l-271.988-271.986 57.372-57.374c0 0 334.778 243.47 677.848 314.892 0.024 0.006 0.042 0.012 0.066 0.018 22.854 5.010 26.586 19.59 28.752 37.090l21.95 263.384-273.232 273.232z" /> 12 | <glyph unicode="" glyph-name="image" d="M959.884 832c0.040-0.034 0.082-0.076 0.116-0.116v-767.77c-0.034-0.040-0.076-0.082-0.116-0.116h-895.77c-0.040 0.034-0.082 0.076-0.114 0.116v767.772c0.034 0.040 0.076 0.082 0.114 0.114h895.77zM960 896h-896c-35.2 0-64-28.8-64-64v-768c0-35.2 28.8-64 64-64h896c35.2 0 64 28.8 64 64v768c0 35.2-28.8 64-64 64v0zM832 672c0-53.020-42.98-96-96-96s-96 42.98-96 96 42.98 96 96 96 96-42.98 96-96zM896 128h-768v128l224 384 256-320h64l224 192z" /> 13 | <glyph unicode="" glyph-name="mic" d="M480 256c88.366 0 160 71.634 160 160v384c0 88.366-71.634 160-160 160s-160-71.634-160-160v-384c0-88.366 71.636-160 160-160zM704 512v-96c0-123.71-100.29-224-224-224-123.712 0-224 100.29-224 224v96h-64v-96c0-148.238 112.004-270.3 256-286.22v-129.78h-128v-64h320v64h-128v129.78c143.994 15.92 256 137.982 256 286.22v96h-64z" /> 14 | <glyph unicode="" glyph-name="textfile" d="M917.806 730.924c-22.212 30.292-53.174 65.7-87.178 99.704s-69.412 64.964-99.704 87.178c-51.574 37.82-76.592 42.194-90.924 42.194h-496c-44.112 0-80-35.888-80-80v-864c0-44.112 35.888-80 80-80h736c44.112 0 80 35.888 80 80v624c0 14.332-4.372 39.35-42.194 90.924zM785.374 785.374c30.7-30.7 54.8-58.398 72.58-81.374h-153.954v153.946c22.984-17.78 50.678-41.878 81.374-72.572zM896 16c0-8.672-7.328-16-16-16h-736c-8.672 0-16 7.328-16 16v864c0 8.672 7.328 16 16 16 0 0 495.956 0.002 496 0v-224c0-17.672 14.326-32 32-32h224v-624zM736 128h-448c-17.672 0-32 14.326-32 32s14.328 32 32 32h448c17.674 0 32-14.326 32-32s-14.326-32-32-32zM736 256h-448c-17.672 0-32 14.326-32 32s14.328 32 32 32h448c17.674 0 32-14.326 32-32s-14.326-32-32-32zM736 384h-448c-17.672 0-32 14.326-32 32s14.328 32 32 32h448c17.674 0 32-14.326 32-32s-14.326-32-32-32z" /> 15 | <glyph unicode="" glyph-name="history" horiz-adv-x="1088" d="M640 896c247.424 0 448-200.576 448-448s-200.576-448-448-448v96c94.024 0 182.418 36.614 248.902 103.098s103.098 154.878 103.098 248.902c0 94.022-36.614 182.418-103.098 248.902s-154.878 103.098-248.902 103.098c-94.022 0-182.418-36.614-248.902-103.098-51.14-51.138-84.582-115.246-97.306-184.902h186.208l-224-256-224 256h164.57c31.060 217.102 217.738 384 443.43 384zM832 512v-128h-256v320h128v-192z" /> 16 | <glyph unicode="" glyph-name="reply" d="M512 896c282.77 0 512-186.25 512-416 0-229.752-229.23-416-512-416-27.156 0-53.81 1.734-79.824 5.044-109.978-109.978-241.25-129.7-368.176-132.596v26.916c68.536 33.578 128 94.74 128 164.636 0 9.754-0.758 19.33-2.164 28.696-115.796 76.264-189.836 192.754-189.836 323.304 0 229.75 229.23 416 512 416z" /> 17 | <glyph unicode="" glyph-name="quote" d="M225 512c123.712 0 224-100.29 224-224 0-123.712-100.288-224-224-224s-224 100.288-224 224l-1 32c0 247.424 200.576 448 448 448v-128c-85.474 0-165.834-33.286-226.274-93.726-11.634-11.636-22.252-24.016-31.83-37.020 11.438 1.8 23.16 2.746 35.104 2.746zM801 512c123.71 0 224-100.29 224-224 0-123.712-100.29-224-224-224s-224 100.288-224 224l-1 32c0 247.424 200.576 448 448 448v-128c-85.474 0-165.834-33.286-226.274-93.726-11.636-11.636-22.254-24.016-31.832-37.020 11.44 1.8 23.16 2.746 35.106 2.746z" /> 18 | <glyph unicode="" glyph-name="loading" d="M728.992 448c137.754 87.334 231.008 255.208 231.008 448 0 21.676-1.192 43.034-3.478 64h-889.042c-2.29-20.968-3.48-42.326-3.48-64 0-192.792 93.254-360.666 231.006-448-137.752-87.334-231.006-255.208-231.006-448 0-21.676 1.19-43.034 3.478-64h889.042c2.288 20.966 3.478 42.324 3.478 64 0.002 192.792-93.252 360.666-231.006 448zM160 0c0 186.912 80.162 345.414 224 397.708v100.586c-143.838 52.29-224 210.792-224 397.706v0h704c0-186.914-80.162-345.416-224-397.706v-100.586c143.838-52.294 224-210.796 224-397.708h-704zM619.626 290.406c-71.654 40.644-75.608 93.368-75.626 125.366v64.228c0 31.994 3.804 84.914 75.744 125.664 38.504 22.364 71.808 56.348 97.048 98.336h-409.582c25.266-42.032 58.612-76.042 97.166-98.406 71.654-40.644 75.606-93.366 75.626-125.366v-64.228c0-31.992-3.804-84.914-75.744-125.664-72.622-42.18-126.738-125.684-143.090-226.336h501.67c-16.364 100.708-70.53 184.248-143.212 226.406z" /> 19 | <glyph unicode="" glyph-name="login" d="M704 960c-176.73 0-320-143.268-320-320 0-20.026 1.858-39.616 5.376-58.624l-389.376-389.376v-192c0-35.346 28.654-64 64-64h64v64h128v128h128v128h128l83.042 83.042c34.010-12.316 70.696-19.042 108.958-19.042 176.73 0 320 143.268 320 320s-143.27 320-320 320zM799.874 639.874c-53.020 0-96 42.98-96 96s42.98 96 96 96 96-42.98 96-96-42.98-96-96-96z" /> 20 | <glyph unicode="" glyph-name="settings" d="M933.79 349.75c-53.726 93.054-21.416 212.304 72.152 266.488l-100.626 174.292c-28.75-16.854-62.176-26.518-97.846-26.518-107.536 0-194.708 87.746-194.708 195.99h-201.258c0.266-33.41-8.074-67.282-25.958-98.252-53.724-93.056-173.156-124.702-266.862-70.758l-100.624-174.292c28.97-16.472 54.050-40.588 71.886-71.478 53.638-92.908 21.512-211.92-71.708-266.224l100.626-174.292c28.65 16.696 61.916 26.254 97.4 26.254 107.196 0 194.144-87.192 194.7-194.958h201.254c-0.086 33.074 8.272 66.57 25.966 97.218 53.636 92.906 172.776 124.594 266.414 71.012l100.626 174.29c-28.78 16.466-53.692 40.498-71.434 71.228zM512 240.668c-114.508 0-207.336 92.824-207.336 207.334 0 114.508 92.826 207.334 207.336 207.334 114.508 0 207.332-92.826 207.332-207.334-0.002-114.51-92.824-207.334-207.332-207.334z" /> 21 | <glyph unicode="" glyph-name="stats" d="M128 64h896v-128h-1024v1024h128zM288 128c-53.020 0-96 42.98-96 96s42.98 96 96 96c2.828 0 5.622-0.148 8.388-0.386l103.192 171.986c-9.84 15.070-15.58 33.062-15.58 52.402 0 53.020 42.98 96 96 96s96-42.98 96-96c0-19.342-5.74-37.332-15.58-52.402l103.192-171.986c2.766 0.238 5.56 0.386 8.388 0.386 2.136 0 4.248-0.094 6.35-0.23l170.356 298.122c-10.536 15.408-16.706 34.036-16.706 54.11 0 53.020 42.98 96 96 96s96-42.98 96-96c0-53.020-42.98-96-96-96-2.14 0-4.248 0.094-6.35 0.232l-170.356-298.124c10.536-15.406 16.706-34.036 16.706-54.11 0-53.020-42.98-96-96-96s-96 42.98-96 96c0 19.34 5.74 37.332 15.578 52.402l-103.19 171.984c-2.766-0.238-5.56-0.386-8.388-0.386s-5.622 0.146-8.388 0.386l-103.192-171.986c9.84-15.068 15.58-33.060 15.58-52.4 0-53.020-42.98-96-96-96z" /> 22 | <glyph unicode="" glyph-name="fire" d="M321.008-64c-68.246 142.008-31.902 223.378 20.55 300.044 57.44 83.956 72.244 167.066 72.244 167.066s45.154-58.7 27.092-150.508c79.772 88.8 94.824 230.28 82.782 284.464 180.314-126.012 257.376-398.856 153.522-601.066 552.372 312.532 137.398 780.172 65.154 832.85 24.082-52.676 28.648-141.85-20-185.126-82.352 312.276-285.972 376.276-285.972 376.276 24.082-161.044-87.296-337.144-194.696-468.73-3.774 64.216-7.782 108.528-41.55 169.98-7.58-116.656-96.732-211.748-120.874-328.628-32.702-158.286 24.496-274.18 241.748-396.622z" /> 23 | <glyph unicode="" glyph-name="locate" d="M1024 512h-100.924c-27.64 178.24-168.836 319.436-347.076 347.076v100.924h-128v-100.924c-178.24-27.64-319.436-168.836-347.076-347.076h-100.924v-128h100.924c27.64-178.24 168.836-319.436 347.076-347.076v-100.924h128v100.924c178.24 27.64 319.436 168.836 347.076 347.076h100.924v128zM792.822 512h-99.762c-19.284 54.55-62.51 97.778-117.060 117.060v99.762c107.514-24.49 192.332-109.31 216.822-216.822zM512 384c-35.346 0-64 28.654-64 64s28.654 64 64 64c35.346 0 64-28.654 64-64s-28.654-64-64-64zM448 728.822v-99.762c-54.55-19.282-97.778-62.51-117.060-117.060h-99.762c24.49 107.512 109.31 192.332 216.822 216.822zM231.178 384h99.762c19.282-54.55 62.51-97.778 117.060-117.060v-99.762c-107.512 24.49-192.332 109.308-216.822 216.822zM576 167.178v99.762c54.55 19.284 97.778 62.51 117.060 117.060h99.762c-24.49-107.514-109.308-192.332-216.822-216.822z" /> 24 | <glyph unicode="" glyph-name="upload" d="M892.268 573.51c2.444 11.11 3.732 22.648 3.732 34.49 0 88.366-71.634 160-160 160-14.222 0-28.014-1.868-41.132-5.352-24.798 77.352-97.29 133.352-182.868 133.352-87.348 0-161.054-58.336-184.326-138.17-22.742 6.622-46.792 10.17-71.674 10.17-141.384 0-256-114.616-256-256 0-141.388 114.616-256 256-256h128v-192h256v192h224c88.366 0 160 71.632 160 160 0 78.72-56.854 144.162-131.732 157.51zM576 320v-192h-128v192h-160l224 224 224-224h-160z" /> 25 | <glyph unicode="" glyph-name="flag" d="M0 960h128v-1024h-128v1024zM832 316.998c82.624 0 154.57 19.984 192 49.5v512c-37.43-29.518-109.376-49.502-192-49.502s-154.57 19.984-192 49.502v-512c37.43-29.516 109.376-49.5 192-49.5zM608 927.472c-46.906 19.94-115.52 32.528-192 32.528-96.396 0-180.334-19.984-224-49.502v-512c43.666 29.518 127.604 49.502 224 49.502 76.48 0 145.094-12.588 192-32.528v512z" /> 26 | <glyph unicode="" glyph-name="attention" d="M256 832v-896l320 320 320-320v896zM768 960h-640v-896l64 64v768h576z" /> 27 | <glyph unicode="" glyph-name="star" d="M1024 562.95l-353.78 51.408-158.22 320.582-158.216-320.582-353.784-51.408 256-249.538-60.432-352.352 316.432 166.358 316.432-166.358-60.434 352.352 256.002 249.538zM512 206.502l-223.462-117.48 42.676 248.83-180.786 176.222 249.84 36.304 111.732 226.396 111.736-226.396 249.836-36.304-180.788-176.222 42.678-248.83-223.462 117.48z" /> 28 | <glyph unicode="" glyph-name="star-ok" d="M1024 562.95l-353.78 51.408-158.22 320.582-158.216-320.582-353.784-51.408 256-249.538-60.432-352.352 316.432 166.358 316.432-166.358-60.434 352.352 256.002 249.538z" /> 29 | <glyph unicode="" glyph-name="plus" d="M992 576h-352v352c0 17.672-14.328 32-32 32h-192c-17.672 0-32-14.328-32-32v-352h-352c-17.672 0-32-14.328-32-32v-192c0-17.672 14.328-32 32-32h352v-352c0-17.672 14.328-32 32-32h192c17.672 0 32 14.328 32 32v352h352c17.672 0 32 14.328 32 32v192c0 17.672-14.328 32-32 32z" /> 30 | <glyph unicode="" glyph-name="about" d="M448 656c0 26.4 21.6 48 48 48h32c26.4 0 48-21.6 48-48v-32c0-26.4-21.6-48-48-48h-32c-26.4 0-48 21.6-48 48v32zM640 192h-256v64h64v192h-64v64h192v-256h64zM512 960c-282.77 0-512-229.23-512-512s229.23-512 512-512 512 229.23 512 512-229.23 512-512 512zM512 32c-229.75 0-416 186.25-416 416s186.25 416 416 416 416-186.25 416-416-186.25-416-416-416z" /> 31 | <glyph unicode="" glyph-name="close" d="M512 960c-282.77 0-512-229.23-512-512s229.23-512 512-512 512 229.23 512 512-229.23 512-512 512zM512 32c-229.75 0-416 186.25-416 416s186.25 416 416 416 416-186.25 416-416-186.25-416-416-416zM672 704l-160-160-160 160-96-96 160-160-160-160 96-96 160 160 160-160 96 96-160 160 160 160z" /> 32 | <glyph unicode="" glyph-name="logout" d="M768 320v128h-320v128h320v128l192-192zM704 384v-256h-320v-192l-384 192v832h704v-320h-64v256h-512l256-128v-576h256v192z" /> 33 | <glyph unicode="" glyph-name="play" d="M512 960c-282.77 0-512-229.23-512-512s229.23-512 512-512 512 229.23 512 512-229.23 512-512 512zM512 32c-229.75 0-416 186.25-416 416s186.25 416 416 416 416-186.25 416-416-186.25-416-416-416zM384 672l384-224-384-224z" /> 34 | <glyph unicode="" glyph-name="pause" d="M512 960c-282.77 0-512-229.23-512-512s229.23-512 512-512 512 229.23 512 512-229.23 512-512 512zM512 32c-229.75 0-416 186.25-416 416s186.25 416 416 416 416-186.25 416-416-186.25-416-416-416zM320 640h128v-384h-128zM576 640h128v-384h-128z" /> 35 | <glyph unicode="" glyph-name="refresh" d="M889.68 793.68c-93.608 102.216-228.154 166.32-377.68 166.32-282.77 0-512-229.23-512-512h96c0 229.75 186.25 416 416 416 123.020 0 233.542-53.418 309.696-138.306l-149.696-149.694h352v352l-134.32-134.32zM928 448c0-229.75-186.25-416-416-416-123.020 0-233.542 53.418-309.694 138.306l149.694 149.694h-352v-352l134.32 134.32c93.608-102.216 228.154-166.32 377.68-166.32 282.77 0 512 229.23 512 512h-96z" /> 36 | <glyph unicode="" glyph-name="forward" d="M512 960c-282.77 0-512-229.23-512-512s229.23-512 512-512 512 229.23 512 512-229.23 512-512 512zM512 32c-229.75 0-416 186.25-416 416s186.25 416 416 416 416-186.25 416-416-186.25-416-416-416zM354.744 253.256l90.512-90.512 285.254 285.256-285.256 285.254-90.508-90.508 194.744-194.746z" /> 37 | <glyph unicode="" glyph-name="back" d="M512-64c282.77 0 512 229.23 512 512s-229.23 512-512 512-512-229.23-512-512 229.23-512 512-512zM512 864c229.75 0 416-186.25 416-416s-186.25-416-416-416-416 186.25-416 416 186.25 416 416 416zM669.256 642.744l-90.512 90.512-285.254-285.256 285.256-285.254 90.508 90.508-194.744 194.746z" /> 38 | <glyph unicode="" glyph-name="order-rev" d="M704 448v-384h64v384h160l-192 192-192-192zM64 768h96v-64h-96v64zM192 768h96v-64h-96v64zM320 768h64v-96h-64v96zM64 544h64v-96h-64v96zM160 512h96v-64h-96v64zM288 512h96v-64h-96v64zM64 672h64v-96h-64v96zM320 640h64v-96h-64v96zM320 256v-192h-192v192h192zM384 320h-320v-320h320v320z" /> 39 | <glyph unicode="" glyph-name="order-rev-down" d="M768 256v384h-64v-384h-160l192-192 192 192zM320 704v-192h-192v192h192zM384 768h-320v-320h320v320zM64 320h96v-64h-96v64zM192 320h96v-64h-96v64zM320 320h64v-96h-64v96zM64 96h64v-96h-64v96zM160 64h96v-64h-96v64zM288 64h96v-64h-96v64zM64 224h64v-96h-64v96zM320 192h64v-96h-64v96z" /> 40 | <glyph unicode="" glyph-name="github" d="M512.008 947.358c-282.738 0-512.008-229.218-512.008-511.998 0-226.214 146.704-418.132 350.136-485.836 25.586-4.738 34.992 11.11 34.992 24.632 0 12.204-0.48 52.542-0.696 95.324-142.448-30.976-172.504 60.41-172.504 60.41-23.282 59.176-56.848 74.916-56.848 74.916-46.452 31.778 3.51 31.124 3.51 31.124 51.4-3.61 78.476-52.766 78.476-52.766 45.672-78.27 119.776-55.64 149.004-42.558 4.588 33.086 17.852 55.68 32.506 68.464-113.73 12.942-233.276 56.85-233.276 253.032 0 55.898 20.004 101.574 52.76 137.428-5.316 12.9-22.854 64.972 4.952 135.5 0 0 43.006 13.752 140.84-52.49 40.836 11.348 84.636 17.036 128.154 17.234 43.502-0.198 87.336-5.886 128.256-17.234 97.734 66.244 140.656 52.49 140.656 52.49 27.872-70.528 10.35-122.6 5.036-135.5 32.82-35.856 52.694-81.532 52.694-137.428 0-196.654-119.778-239.95-233.79-252.624 18.364-15.89 34.724-47.046 34.724-94.812 0-68.508-0.596-123.644-0.596-140.508 0-13.628 9.222-29.594 35.172-24.566 203.322 67.776 349.842 259.626 349.842 485.768 0 282.78-229.234 511.998-511.992 511.998z" /> 41 | </font></defs></svg> -------------------------------------------------------------------------------- /src/UserAction.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PureComponent, useState, useEffect, useContext} from 'react'; 2 | import {SafeTextarea, PromotionBar, HAPI_DOMAIN, GATEWAY_DOMAIN, BrowserWarningBar} from './Common'; 3 | import {MessageViewer} from './Message'; 4 | import {ConfigUI} from './Config'; 5 | import copy from 'copy-to-clipboard'; 6 | import {cache} from './cache'; 7 | import {get_json, token_param} from './flows_api'; 8 | 9 | import './UserAction.css'; 10 | 11 | const MAX_IMG_DIAM=8000; 12 | const MAX_IMG_PX=5000000; 13 | const MAX_IMG_FILESIZE=500000; 14 | const MAX_IMG_FILESIZE_LIM=550000; // used for: png detect limit, too big failure limit 15 | const IMG_QUAL_MIN=.1; 16 | const IMG_QUAL_MAX=.9; 17 | 18 | export const TokenCtx=React.createContext({ 19 | value: null, 20 | ring: null, 21 | logout: ()=>{}, 22 | }); 23 | 24 | const supports_webp=(()=>{ 25 | let cvs=document.createElement('canvas'); 26 | cvs.width=1; 27 | cvs.height=1; 28 | let url=cvs.toDataURL('image/webp',1); 29 | return url.indexOf('webp')!==-1; 30 | })(); 31 | 32 | function bisect_img_quality(canvas,ql,qr,callback,progress_callback,type=null) { 33 | // check for png first 34 | if(type===null) { 35 | progress_callback('正在编码 png'); 36 | canvas.toBlob((blob)=>{ 37 | console.log('bisect img quality:','png','size',blob.size); 38 | if(blob.size<MAX_IMG_FILESIZE_LIM) 39 | callback('png', blob); 40 | else { 41 | type=((supports_webp && !window.__WEBHOLE_DISABLE_WEBP)?'webp':'jpeg'); 42 | bisect_img_quality(canvas,ql,qr,callback,progress_callback,type); 43 | } 44 | },'image/png'); 45 | return; 46 | } 47 | 48 | let q=(qr+ql)/2; 49 | if(qr-ql<.06) { // done 50 | progress_callback('正在编码 '+type+'@'+(q*100).toFixed(0)); 51 | canvas.toBlob((blob)=>{ 52 | console.log('bisect img quality:',type,'quality',q,'size',blob.size); 53 | if(ql<=.101 && blob.size>MAX_IMG_FILESIZE_LIM) 54 | callback('图片过大', null); 55 | else 56 | callback(`${type}@${(q*100).toFixed(0)}`, blob); 57 | },'image/'+type,q); 58 | } else { // bisect quality 59 | progress_callback('正在编码 jpeg@'+(q*100).toFixed(0)); 60 | canvas.toBlob((blob)=>{ 61 | console.log('bisect img quality:',type,'range',ql,qr,'quality',q,'size',blob.size); 62 | if(blob.size>MAX_IMG_FILESIZE) 63 | bisect_img_quality(canvas,ql,q,callback,progress_callback,type); 64 | else 65 | bisect_img_quality(canvas,q,qr,callback,progress_callback,type); 66 | },'image/jpeg',q); 67 | } 68 | } 69 | 70 | function InviteViewer(props) { 71 | let [res,set_res]=useState(null); 72 | 73 | useEffect(()=>{ 74 | fetch(HAPI_DOMAIN+'/api/users/invites?role=invite'+token_param(props.token)) 75 | .then(get_json) 76 | .then((json)=>{ 77 | if(json.error) 78 | throw new Error(json.error_msg||json.error); 79 | 80 | set_res(json); 81 | }) 82 | .catch((e)=>{ 83 | alert('加载失败。'+e); 84 | }); 85 | },[]); 86 | 87 | if(res===null) 88 | return ( 89 | <div className="box box-tip"> 90 | <span className="icon icon-loading" /> 正在获取…… 91 | </div> 92 | ); 93 | 94 | function copy_code() { 95 | if(copy(res.code)) 96 | alert('已复制'); 97 | } 98 | 99 | return ( 100 | <div className="box box-tip"> 101 | <p>使用下面的邀请码来邀请朋友注册:</p> 102 | <p className="invite-code"> 103 | <code>{res.code||'(无)'}</code> <a onClick={copy_code}>(复制)</a></p> 104 | <p>还可以邀请 {res.remaining||0} 人</p> 105 | </div> 106 | ) 107 | } 108 | 109 | export function InfoSidebar(props) { 110 | let tctx=useContext(TokenCtx); 111 | 112 | return ( 113 | <div> 114 | <BrowserWarningBar /> 115 | <PromotionBar /> 116 | <LoginForm show_sidebar={props.show_sidebar} /> 117 | <div className="box list-menu"> 118 | <a onClick={()=>{props.show_sidebar( 119 | '设置', 120 | <ConfigUI /> 121 | )}}> 122 | <span className="icon icon-settings" /><label>树洞设置</label> 123 | </a> 124 |    125 | <a href="https://_BRAND_HAPI_DOMAIN/rules" target="_blank"> 126 | <span className="icon icon-textfile" /><label>社区规范</label> 127 | </a> 128 |    129 | <a href="https://_BRAND_FEEDBACK_URL" target="_blank"> 130 | <span className="icon icon-fire" /><label>意见反馈</label> 131 | </a> 132 | </div> 133 | <div className="box help-desc-box"> 134 | <p> 135 | 本项目基于  136 | <a href="https://github.com/xmcp/webhole" target="_blank" rel="noopener">网页版树洞 by @xmcp</a> 137 | 。感谢  138 | <a href="https://reactjs.org/" target="_blank" rel="noopener">React</a> 139 | 、 140 | <a href="https://icomoon.io/#icons" target="_blank" rel="noopener">IcoMoon</a> 141 |  等开源项目 142 | </p> 143 | <p> 144 | <a onClick={()=>{ 145 | if('serviceWorker' in navigator) { 146 | navigator.serviceWorker.getRegistrations() 147 | .then((registrations)=>{ 148 | for(let registration of registrations) { 149 | console.log('unregister',registration); 150 | registration.unregister(); 151 | } 152 | }); 153 | } 154 | cache().clear(); 155 | setTimeout(()=>{ 156 | window.location.reload(true); 157 | },200); 158 | }}>强制检查更新</a> 159 | ({process.env.REACT_APP_BUILD_INFO||'---'} {tctx.backend_version} {process.env.NODE_ENV}) 160 | </p> 161 | <p> 162 | This program is free software: you can redistribute it and/or modify 163 | it under the terms of the GNU General Public License as published by 164 | the Free Software Foundation, either version 3 of the License, or 165 | (at your option) any later version. 166 | </p> 167 | <p> 168 | This program is distributed in the hope that it will be useful, 169 | but WITHOUT ANY WARRANTY; without even the implied warranty of 170 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the  171 | <a href="https://www.gnu.org/licenses/gpl-3.0.zh-cn.html" target="_blank">GNU General Public License</a> 172 |  for more details. 173 | </p> 174 | </div> 175 | </div> 176 | ); 177 | } 178 | 179 | export class LoginForm extends Component { 180 | copy_token(token) { 181 | if(copy(token)) 182 | alert('复制成功!\n请一定不要泄露哦'); 183 | } 184 | 185 | render() { 186 | return ( 187 | <TokenCtx.Consumer>{(token)=> 188 | <div className="login-form box"> 189 | <p> 190 | <b>您已登录。</b> 191 | <button type="button" onClick={token.logout}> 192 | <span className="icon icon-logout" /> 注销 193 | </button> 194 | <br /> 195 | </p> 196 | <p> 197 | <a onClick={()=>{this.props.show_sidebar( 198 | '系统消息', 199 | <MessageViewer token={token.value} /> 200 | )}}>查看系统消息</a><br /> 201 | 当您发送的内容违规时,我们将用系统消息提示您 202 | </p> 203 | <p> 204 | <a href="https://_BRAND_GATEWAY_DOMAIN/users/cp" target="_blank"> 205 | 账号管理 206 | </a><br /> 207 | 可以修改您的登录密码 208 | </p> 209 | {/*<p> 210 | <a onClick={this.copy_token.bind(this,token.value)}>复制 User Token</a><br /> 211 | User Token 用于迁移登录状态,切勿告知他人。 212 | </p>*/} 213 | {!!token.remember_token && 214 | <p> 215 | <a href={HAPI_DOMAIN+'/pillory?rr_token='+token.remember_token} target="_blank">违规处理公示</a><br /> 216 | _BRAND_COMMUNITY_SLOGAN,对于内容违反 <a href="https://_BRAND_HAPI_DOMAIN/rules" target="_blank">社区规范</a> 217 |  的删帖和封禁情况将在此进行公示 218 | </p> 219 | } 220 | </div> 221 | }</TokenCtx.Consumer> 222 | ) 223 | } 224 | } 225 | 226 | export class PostForm extends Component { 227 | constructor(props) { 228 | super(props); 229 | this.state={ 230 | bridge: false, 231 | text: '', 232 | type: 'text', 233 | loading_status: 'done', 234 | img_extra: null, // {tip, img} 235 | }; 236 | this.img_ref=React.createRef(); 237 | this.area_ref=this.props.area_ref||React.createRef(); 238 | this.on_change_bound=this.on_change.bind(this); 239 | this.on_img_change_bound=this.on_img_change.bind(this); 240 | this.global_keypress_handler_bound=this.global_keypress_handler.bind(this); 241 | } 242 | 243 | global_keypress_handler(e) { 244 | if(e.code==='Enter' && !e.ctrlKey && !e.altKey && ['input','textarea'].indexOf(e.target.tagName.toLowerCase())===-1) { 245 | if(this.area_ref.current) { 246 | e.preventDefault(); 247 | this.area_ref.current.focus(); 248 | } 249 | } 250 | } 251 | componentDidMount() { 252 | if(this.area_ref.current && !this.props.pid) // new post 253 | this.area_ref.current.focus(); 254 | document.addEventListener('keypress',this.global_keypress_handler_bound); 255 | } 256 | componentWillUnmount() { 257 | document.removeEventListener('keypress',this.global_keypress_handler_bound); 258 | } 259 | 260 | on_change(value) { 261 | this.setState({ 262 | text: value, 263 | }); 264 | } 265 | 266 | do_post(text,type,extra_data) { 267 | let url=this.props.pid ? 268 | ('/api/holes/reply/'+this.props.pid+'?role=reply'): // reply 269 | '/api/holes/post?role=post'; // post new 270 | let body=new FormData(); 271 | body.append('text',this.state.text); 272 | body.append('bridge',this.state.bridge); 273 | body.append('type',type); 274 | if(extra_data) { 275 | let type=extra_data.blob.type; 276 | if(type.indexOf('image/')!==0) { 277 | alert('上传图片类型错误'); 278 | this.setState({ 279 | loading_status: 'done', 280 | }); 281 | return; 282 | } 283 | body.append('data_type',extra_data.quality_str); 284 | body.append('data',extra_data.blob); 285 | } 286 | fetch(HAPI_DOMAIN+url+token_param(this.props.token), { 287 | method: 'POST', 288 | body: body, 289 | }) 290 | .then(get_json) 291 | .then((json)=>{ 292 | if(json.error) { 293 | throw new Error(json.error_msg||json.error); 294 | } 295 | 296 | this.setState({ 297 | loading_status: 'done', 298 | type: 'text', 299 | text: '', 300 | }); 301 | if(this.area_ref.current) // sidebar may be closed when posting 302 | this.area_ref.current.clear(); 303 | this.props.on_complete(); 304 | }) 305 | .catch((e)=>{ 306 | alert('发送失败。'+e); 307 | this.setState({ 308 | loading_status: 'done', 309 | }); 310 | }); 311 | } 312 | 313 | proc_img(file) { 314 | let that=this; 315 | return new Promise((resolve,reject)=>{ 316 | let reader=new FileReader(); 317 | function on_got_img(url) { 318 | const image = new Image(); 319 | image.onload=(()=>{ 320 | let width=image.width; 321 | let height=image.height; 322 | let compressed=false; 323 | 324 | if(width>MAX_IMG_DIAM) { 325 | height=height*MAX_IMG_DIAM/width; 326 | width=MAX_IMG_DIAM; 327 | compressed=true; 328 | } 329 | if(height>MAX_IMG_DIAM) { 330 | width=width*MAX_IMG_DIAM/height; 331 | height=MAX_IMG_DIAM; 332 | compressed=true; 333 | } 334 | if(height*width>MAX_IMG_PX) { 335 | let rate=Math.sqrt(height*width/MAX_IMG_PX); 336 | height/=rate; 337 | width/=rate; 338 | compressed=true; 339 | } 340 | console.log('chosen img size',width,height); 341 | 342 | let canvas=document.createElement('canvas'); 343 | let ctx=canvas.getContext('2d', { 344 | alpha: false, 345 | }); 346 | canvas.width=width; 347 | canvas.height=height; 348 | ctx.drawImage(image,0,0,width,height); 349 | 350 | bisect_img_quality(canvas,IMG_QUAL_MIN,IMG_QUAL_MAX, (quality_str, blob)=>{ 351 | if(blob===null) { 352 | reject('无法上传:'+quality_str); 353 | return; 354 | } 355 | 356 | resolve({ 357 | blob: blob, 358 | quality_str: quality_str, 359 | width: Math.round(width), 360 | height: Math.round(height), 361 | compressed: compressed, 362 | }); 363 | }, (progress)=>{ 364 | that.setState({ 365 | img_extra: { 366 | tip: '('+progress+'……)', 367 | img: null, 368 | } 369 | }); 370 | }); 371 | }); 372 | image.src=url; 373 | } 374 | reader.onload=(event)=>{ 375 | on_got_img(event.target.result); 376 | //fixOrientation(event.target.result,{},(fixed_dataurl)=>{ 377 | // on_got_img(fixed_dataurl); 378 | //}); 379 | }; 380 | reader.readAsDataURL(file); 381 | }); 382 | } 383 | 384 | on_img_change() { 385 | if(this.img_ref.current && this.img_ref.current.files.length) 386 | this.setState({ 387 | img_extra: { 388 | tip: '(正在处理图片……)', 389 | img: null, 390 | } 391 | },()=>{ 392 | this.proc_img(this.img_ref.current.files[0]) 393 | .then((d)=>{ 394 | this.setState({ 395 | img_extra: { 396 | tip: `(${d.compressed?'压缩到':'尺寸'} ${d.width}*${d.height} / `+ 397 | `${Math.floor(d.blob.size/1000)}KB ${d.quality_str})`, 398 | img: { 399 | blob: d.blob, 400 | quality_str: d.quality_str, 401 | width: d.width, 402 | height: d.height, 403 | }, 404 | }, 405 | }); 406 | }) 407 | .catch((e)=>{ 408 | this.setState({ 409 | img_extra: { 410 | tip: `图片无效:${e}`, 411 | img: null, 412 | }, 413 | }); 414 | }); 415 | }); 416 | else 417 | this.setState({ 418 | img_extra: { 419 | tip: null, 420 | img: null, 421 | }, 422 | }); 423 | } 424 | 425 | on_submit(event) { 426 | if(event) event.preventDefault(); 427 | if(this.state.loading_status==='loading') 428 | return; 429 | 430 | if(this.state.type==='text') { 431 | this.setState({ 432 | loading_status: 'loading', 433 | }); 434 | this.do_post(this.state.text,'text',null); 435 | } 436 | else if(this.state.type==='image') { 437 | if(!this.state.img_extra.img) { 438 | alert('请选择图片'); 439 | return; 440 | } 441 | 442 | this.setState({ 443 | loading_status: 'loading', 444 | }); 445 | this.do_post(this.state.text,this.state.type,this.state.img_extra.img); 446 | } 447 | } 448 | 449 | render() { 450 | let is_reply=(this.props.pid!==null); 451 | let area_id=is_reply ? this.props.pid : 'new_post'; 452 | return ( 453 | <> 454 | {!is_reply && 455 | <TokenCtx.Consumer>{(tctx)=>( 456 | <div className="box"> 457 | <p> 458 | 发帖前请阅读并同意 459 | <a href="https://_BRAND_HAPI_DOMAIN/rules" target="_blank">_BRAND_NAME社区规范</a> 460 | 。 461 | </p> 462 | </div> 463 | )}</TokenCtx.Consumer> 464 | } 465 | <form onSubmit={this.on_submit.bind(this)} className={is_reply ? ('post-form post-form-reply box'+(this.state.text?' reply-sticky':'')) : 'post-form box'}> 466 | <div className="post-form-bar"> 467 | <span className="post-form-switcher"> 468 | {is_reply ? '回复' : '发表'}   469 | <span className={'post-form-switch'+(this.state.type==='text'?' post-form-switch-cur':'')} onClick={()=>this.setState({type: 'text'})}> 470 | <span className="icon icon-pen" /> 文字 471 | </span> 472 | <span className={'post-form-switch'+(this.state.type==='image'?' post-form-switch-cur':'')} onClick={()=>this.setState({type: 'image', img_extra: {tip: null, blob: null}})}> 473 | <span className="icon icon-image" /> 图片 474 | </span> 475 | </span> 476 | {this.state.loading_status!=='done' ? 477 | <button className="post-btn" disabled="disabled"> 478 | <span className="icon icon-loading" /> 479 |  正在{this.state.loading_status==='processing' ? '处理' : '上传'} 480 | </button> : 481 | <button className="post-btn" type="submit"> 482 | <span className="icon icon-send" /> 483 |  发送 484 | </button> 485 | } 486 | </div> 487 | <SafeTextarea key={area_id} ref={this.area_ref} id={area_id} on_change={this.on_change_bound} on_submit={()=>this.on_submit()} /> 488 | {this.state.type==='image' && !!this.state.img_extra && 489 | <p className="post-form-img-tip"> 490 | <input ref={this.img_ref} type="file" accept="image/*" disabled={this.state.loading_status!=='done'} 491 | onChange={this.on_img_change_bound} style={{position: 'fixed', top: '-200%', visibility: 'hidden'}} 492 | /> 493 | {this.state.img_extra.tip ? 494 | <> 495 | {!!this.state.img_extra.img && 496 | <a onClick={()=>{this.img_ref.current.value=""; this.on_img_change();}}>删除图片</a> 497 | } 498 | {this.state.img_extra.tip} 499 | </> : 500 | <a onClick={()=>this.img_ref.current.click()}>选择要上传的图片</a> 501 | } 502 | </p> 503 | } 504 | </form> 505 | </> 506 | ) 507 | } 508 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | <one line to give the program's name and a brief idea of what it does.> 635 | Copyright (C) <year> <name of author> 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see <https://www.gnu.org/licenses/>. 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | <program> Copyright (C) <year> <name of author> 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | <https://www.gnu.org/licenses/>. 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | <https://www.gnu.org/licenses/why-not-lgpl.html>. 675 | --------------------------------------------------------------------------------