1 Commits
Author SHA1 Message Date
tmshv 1ce449320c ya trash 2018-11-28 18:37:16 +03:00
75 changed files with 103935 additions and 1246 deletions

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
MONGO_HUDOZKA_PASSWORD=h123
HUDOZKA_DB_URI=mongodb://hudozka:h123@mongo:27017/hudozka?authSource=admin
+7
View File
@@ -0,0 +1,7 @@
{
vk: {
appId: '4202466',
appSecret: '5NMWIbgb2ImAQeMeODEd'
},
outputDir: '/Users/tmshv/Yandex.Disk.localized/Hudozka/Site/Events'
}
+202
View File
@@ -0,0 +1,202 @@
#!/usr/local/bin/python3
import json
import os
import re
import string
from datetime import datetime
from functools import reduce
from os import makedirs
import json5
import sys
import requests
from utils import until, copy
from vk import vk_init, vk_wall_get_by_id, vk_photos_get_albums, vk_photos_get
config = json5.load(open('config.json5', 'r'))
vk_init(config['vk']['appId'], config['vk']['appSecret'])
pattern = re.compile(r'https?://vk\.com/wall([-\d]+)_(\d+)')
ms = []
size_priority = ['w', 'z', 'y']
# get_album_item = until(size_priority)
def get_item_by_size(size, item):
for s in item:
if s['type'] == size:
return s
return None
def get_album_item(item):
for s in size_priority:
img = get_item_by_size(s, item)
if img:
return img
return None
def is_valid_url(i: str):
return pattern.match(i) is not None
def add_middleware(var: str, fn):
ms.append((var, fn))
def apply_middleware(data: str, mw: tuple):
key, fn = mw
value = fn(data)
form = {}
form[mw[0]] = ''
data.format(**form)
return data
def apply_middlewares(output_dir, template, data):
result = template
for mw in ms:
key, fn = mw
form = {key: fn(output_dir, data)}
result = string.Template(result)
result = result.safe_substitute(**form)
return result
def attach_album(output_dir, data):
"""
{
'size': 13,
'title': 'Каллиграфия 27.11.2016',
'owner_id': -132442859,
'updated': 1480340404,
'description': '',
'id': '238711857',
'thumb': {'user_id': 100, 'access_key': '428f3dad13bdd5d730', 'owner_id': -132442859,
'date': 1480340404,
'photo_807': 'https://pp.vk.me/c837134/v837134463/11056/ZaoO1h5Idaw.jpg',
'id': 456239050,
'width': 1024, 'photo_75': 'https://pp.vk.me/c837134/v837134463/11053/aHOALfgr0VU.jpg',
'photo_130': 'https://pp.vk.me/c837134/v837134463/11054/czcuc7o-bN4.jpg', 'text': '',
'photo_604': 'https://pp.vk.me/c837134/v837134463/11055/P7yUE6WitSA.jpg', 'height': 683,
'photo_1280': 'https://pp.vk.me/c837134/v837134463/11057/eLG5Akgx3fk.jpg',
'album_id': 238711857
},
'created': 1480340311
}
:param data:
:return:
"""
oid = data['owner_id']
aid = data['id']
album = vk_photos_get(oid, aid)['response']['items']
files = []
for file in album:
img = get_album_item(file['sizes'])
if img:
files.append(img['src'])
# files.append(file['sizes'])
download_files(files, output_dir)
return '\n'.join(list(
map(
lambda i: '![](%s)' % i,
map(
lambda f: os.path.basename(f),
files
)
)
))
def get_date(ts: int):
date = datetime.fromtimestamp(ts)
return date.strftime("%Y.%m.%d")
def get_folder_name(post):
date_formatted = get_date(post['date'])
return '{date} vk-{owner_id}-{id}'.format(
date=date_formatted,
owner_id=post['owner_id'],
id=post['id'],
)
def download_files(files, output):
for url in files:
file_out = os.path.join(output, os.path.basename(url))
req = requests.get(url)
data = req.content
f = open(file_out, 'wb')
f.write(data)
f.close()
attachment_types = {
'album': attach_album
}
def get_post_attachments(output_dir, data):
attachments = []
for a in data['attachments']:
a_type = a['type']
if a_type in attachment_types:
fn = attachment_types[a_type]
attachment = fn(output_dir, a[a_type])
attachments.append(attachment)
return '\n'.join(attachments)
if __name__ == '__main__':
add_middleware('id', lambda _, data: 'vk-{owner_id}-{id}'.format(
owner_id=data['owner_id'],
id=data['id'],
))
add_middleware('date', lambda _, data: get_date(data['date']))
add_middleware('text', lambda _, data: data['text'])
add_middleware('attachments', get_post_attachments)
article_file = open('template.md', 'r', encoding='utf-8')
article = article_file.read()
article_file.close()
urls = sys.argv[1:]
for url in urls:
if not is_valid_url(url):
raise Exception('Cannot perform URL %s' % url)
vid = '%s_%s' % pattern.findall(url)[0]
res = vk_wall_get_by_id(vid)
if 'response' in res:
post = res['response'][0]
if 'copy_history' in post:
copy(post['copy_history'][0], post)
output_dir = os.path.join(config['outputDir'], get_folder_name(post))
makedirs(output_dir, exist_ok=True)
article = apply_middlewares(output_dir, article, post)
article_file = open(os.path.join(output_dir, 'vk.md'), 'w', encoding='utf-8')
article_file.write(article)
article_file.close()
# print(json.dumps(post, indent=4, ensure_ascii=False))
+8
View File
@@ -0,0 +1,8 @@
---
id: ${id}
date: ${date}
---
${text}
${attachments}
+13
View File
@@ -0,0 +1,13 @@
def until(keys: list):
def select(value):
for key in keys:
if key in value:
return value[key]
return None
return select
def copy(source: dict, destination: dict):
for k, v in source.items():
destination[k] = v
+166
View File
@@ -0,0 +1,166 @@
import requests
from urllib.parse import urlencode
client_id = '4362480'
client_secret = 'CI8hARpbqa5amp4OfTS6'
access_token = '39c1712d4005b24bd9faddf34b7f4647a26afb4c7b1a935ba7680b24378a9acb3e0faed5b3af8a1e7879c'
api_version = '5.52'
VK_API = 'https://api.vk.com/method/'
def vk_init(app_id, app_secret):
global client_id
global client_secret
client_id = app_id
client_secret = app_secret
def http_get(url):
req = requests.get(url)
return req.json()
def upload_file(url, file):
req = requests.post(url, files={'photo': file})
return req.json()
def vk_auth(client_id, redirect_uri, scope, response_type):
q = urlencode({
'client_id': client_id,
'scope': ','.join(scope),
'v': api_version,
'redirect_uri': redirect_uri,
'display': 'page',
'response_type': response_type
})
return 'https://oauth.vk.com/authorize?' + q
def vk_get_access_token(client_id, client_secret, redirect_uri, code):
q = urlencode({
'client_id': client_id,
'client_secret': client_secret,
'code': code,
'redirect_uri': redirect_uri,
})
uri = 'https://oauth.vk.com/access_token?' + q
return http_get(uri)
def vk_photos_get_wall_upload_server():
q = urlencode({
'access_token': access_token,
})
uri = VK_API + 'photos.getWallUploadServer?' + q
return http_get(uri)['response']
def vk_photos_save_wall_photo(server, hash, photo):
q = urlencode({
'access_token': access_token,
'v': api_version,
'server': server,
'hash': hash,
'photo': photo,
})
uri = VK_API + 'photos.saveWallPhoto?' + q
return http_get(uri)
def vk_wall_get_by_id(ids):
if not isinstance(ids, list):
ids = [ids]
q = urlencode({
'posts': ','.join(ids),
'v': api_version,
})
uri = VK_API + 'wall.getById?{q}'.format(q=q)
return http_get(uri)
def vk_wall_post(owner_id, message, attachments):
q = urlencode({
'access_token': access_token,
'v': api_version,
'owner_id': owner_id,
'message': message,
'attachments': ','.join(attachments)
})
uri = VK_API + 'wall.post?{q}'.format(q=q)
return http_get(uri)
def vk_photos_get_albums(owner_id, album_ids):
q = urlencode({
'v': api_version,
'owner_id': owner_id,
'album_ids': ','.join(album_ids),
'photo_sizes': '1',
'need_covers': '1',
})
uri = VK_API + 'photos.getAlbums?{q}'.format(q=q)
return http_get(uri)
def vk_photos_get(owner_id, album_id):
q = urlencode({
'v': api_version,
'owner_id': owner_id,
'album_id': album_id,
'photo_sizes': '1',
})
uri = VK_API + 'photos.get?{q}'.format(q=q)
return http_get(uri)
if __name__ == '__main__':
import os
print(os.getcwd())
# redirect_uri = 'http://tmshv.ru/callback'
redirect_uri = 'http://vk.com/blank.html'
# print(vk_auth(client_id, redirect_uri, ['photos', 'wall', 'offline', 'messages'], 'token'))
# exit()
upload_url = vk_photos_get_wall_upload_server()['upload_url']
print(upload_url)
# UPLOADING
uploaded = upload_file(upload_url, open('1.jpg', 'rb'))
print(uploaded)
exit()
# uploaded_photo = uploaded['photo']
saved_photo = vk_photos_save_wall_photo(**uploaded)
print(saved_photo)
# {'response': [
# {
# 'text': '',
# 'id': 420377937,
# 'album_id': -14,
# 'photo_75': 'http://cs631817.vk.me/v631817931/37c93/eiG89yDwZl4.jpg',
# 'photo_130': 'http://cs631817.vk.me/v631817931/37c94/FDDyNTsJYYQ.jpg',
# 'date': 1467142947,
# 'width': 200,
# 'owner_id': 1338931,
# 'photo_604': 'http://cs631817.vk.me/v631817931/37c95/_TMbT8QHwAc.jpg',
# 'height': 200}
# ]
# }
photo_id = saved_photo['response'][0]['id']
photo_owner_id = saved_photo['response'][0]['owner_id']
attachment = '{type}{owner_id}_{media_id}'.format(
type='photo',
owner_id=photo_owner_id,
media_id=photo_id
)
post = vk_wall_post(photo_owner_id, '#api #test', [attachment])
print(post)
+704 -1228
View File
File diff suppressed because it is too large. Load diff
+13 -3
View File
@@ -6,6 +6,7 @@
"main": "src/bin/www.js",
"scripts": {
"start": "node src/index.js",
"build-edit": "webpack -w --env.NODE_ENV=production --env.TARGET=edit",
"build-app": "webpack --env.NODE_ENV=production --env.TARGET=app",
"out": "babel src/ -d out/",
"gulp": "gulp",
@@ -19,13 +20,22 @@
"author": "Roman Timashev",
"license": "ISC",
"dependencies": {
"@mikeljames/draft-js-drag-n-drop-upload-plugin": "^2.0.1",
"async-file": "^2.0.2",
"classnames": "^2.2.5",
"co": "^4.4.0",
"draft-js": "^0.10.5",
"draft-js-alignment-plugin": "^2.0.3",
"draft-js-drag-n-drop-plugin": "^2.0.3",
"draft-js-focus-plugin": "^2.0.4",
"draft-js-image-plugin": "^2.0.5",
"draft-js-plugins-editor": "^2.0.8",
"draft-js-resizeable-plugin": "^2.0.6",
"ejson": "^2.1.2",
"handlebars": "^4.0.5",
"immutable": "^3.8.2",
"instagram-node": "^0.5.1",
"koa": "^2.0.0-alpha.3",
"koa": "^2.5.1",
"koa-bodyparser": "^3.0.0",
"koa-compose": "^3.1.0",
"koa-conditional-get": "^1.0.2",
@@ -39,7 +49,7 @@
"koa-session": "^4.0.0",
"koa-static": "^3.0.0",
"markdown": "^0.5.0",
"moment": "^2.18.1",
"moment": "^2.22.2",
"mongodb": "^2.1.14",
"mz": "^2.4.0",
"react": "^16.3.2",
@@ -66,7 +76,7 @@
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-react-jsx": "^6.23.0",
"babel-plugin-transform-regenerator": "^6.26.0",
"babel-preset-env": "^1.6.1",
"babel-preset-env": "^1.7.0",
"babel-register": "^6.26.0",
"css-loader": "^0.28.9",
"cssnano": "^3.4.0",
+1
View File
@@ -12,6 +12,7 @@ export const index = path.join(__dirname, 'templates/main.html')
export const sitemapCacheTime = 600000// 600 sec - cache purge period
export const viewMain = path.join(__dirname, 'views/main.hbs')
export const viewEdit = path.join(__dirname, 'views/edit.hbs')
export const view404 = path.join(__dirname, 'views/404.hbs')
export const sessionConfig = {
+90
View File
@@ -0,0 +1,90 @@
// import 'babel-core/register'
// import 'babel-polyfill'
import {render} from 'react-dom'
import getEditApp from './edit'
import TextToken from './edit/features/Marker/lib/marker/TextToken'
import {Marker} from './edit/features/Marker/lib/marker/Marker'
import SplitToken from './edit/features/Marker/lib/marker/SplitToken'
import YoutubeToken from './edit/features/Marker/lib/marker/YoutubeToken'
import {TokenFactory} from './edit/features/Marker/lib/marker/TokenFactory'
import React from 'react'
import UrlToken from './edit/features/Marker/lib/marker/UrlToken'
function getMarker() {
return (new Marker())
.addTokenFactory(new TokenFactory({token: SplitToken}))
.addTokenFactory(new TokenFactory({
token: YoutubeToken,
component: ({url}) => (
<div
className='MarkerWidgetYoutube'
>
<a href={url}>
{url}
</a>
</div>
)
// component: ({url}) => (
// <div
// className='MarkerWidgetYoutube'
// >
// <div className='kazimir__video'>
// <iframe
// src={url}
// frameBorder='0'
// allowFullScreen
// />
// </div>
// </div>
// )
}))
// .addTokenFactory(new TokenFactory({token: InstagramToken}))
// .addTokenFactory(BuildTokenFactory(DocumentUrlToken, build=read_document))
// .addTokenFactory(new TokenFactory({token: ImageToken}))
// .addTokenFactory(BuildTokenFactory(DocumentToken, build=build_document))
// .addTokenFactory(BuildTokenFactory(CSVToken, build=read_csv))
// .addTokenFactory(new TokenFactory({token: FileToken}))
.addTokenFactory(new TokenFactory({
token: TextToken,
component: ({html}) => (
<div
className='MarkerWidgetHtml'
dangerouslySetInnerHTML={{__html: html}}
/>
)
}), {
blank: true,
})
// .add_tree_middleware(fix_links_quotes)
// sample_tree = await marker.create_tree(sample_text)
}
function getData() {
return `
# Edit
https://www.youtube.com/watch?v=pxtCffGya9g
Text row
`
// return [
//
// 'bla-bla',
// '### pop-pop',
// 'uiu',
// 'Url: [ya](http://ya.ru)',
// ]
}
function main() {
const mount = document.querySelector('#app')
const marker = getMarker()
const data = getData()
const app = getEditApp(marker, data)
render(app, mount)
}
main()
+3
View File
@@ -0,0 +1,3 @@
export default function effects() {
return []
}
+141
View File
@@ -0,0 +1,141 @@
import React, {Component} from 'react'
import Marker from '../Marker'
import './App.less'
import {MarkerWidgetHtml, MarkerWidgetText, MarkerWidgetUrl} from '../Marker/MarkerWidget'
import ContentEditable from '../Marker/ContentEditable'
import Editor from '../Editor/'
// import Map from '../Map'
// import Body from '../Body'
// import AppWrapper from './AppWrapper'
//
// import {bindActionCreators} from 'redux'
// import {connect} from 'react-redux'
// import * as actions from './duck'
//
// import {routerActions} from 'react-router-redux'
// import {title as defaultTitle} from '../../../config'
// import {getVariable} from '../../../lib/style'
// import {debounce} from '../../../lib/fn'
// const hit = debounce(50, path => ym('hit', path))
//
// const projectPath = project => `/projects/${project.name}`
// const frameSize = isVertical => [window.innerWidth, window.innerHeight]
// const frameSizeMultiplier = () => getVariable('--content-width-state-normal')
// const landscapeProjectBounds = (width, height, ratio=frameSizeMultiplier()) => [
// [0, 0],
// [width * ratio, 0],
// ]
// const portraitProjectBounds = (width, height, ratio=frameSizeMultiplier()) => [
// [0, 0],
// [0, width * ratio],
// ]
//
// function mapStateToProps(state) {
// const {fullPage, isVertical, halfPageAvailable} = state.app.page
//
// return {
// projects: state.app.projects.items,
// activeProject: state.app.projects.activeProject,
// fullPage: halfPageAvailable
// ? fullPage
// : true,
// showFullPageToggle: halfPageAvailable,
// isVertical,
// }
// }
//
// function mapDispatchToProps(dispatch) {
// return bindActionCreators({
// ...actions,
// push: routerActions.push
// }, dispatch)
// }
// async function build_document(data) {
// // text = data['caption'] if data['caption'] else data['file']
// // # document = await self.build()
// document = {
// 'url': data['file'],
// 'image_url': data['file'],
// 'file_url': data['file'],
// 'title': text,
// 'file_size': '0 KB',
// 'file_format': 'PDF',
// }
// return document
// }
// async function read_csv(data) {
// return '\n'.join([
// 'N,Name',
// `0,${data}`,
// '1,Pop',
// '2,Top',
// ])
// }
// async function read_document(data) {
// const doc = await Document.find_one({'id': data})
// return {
// 'url': doc['url'],
// 'image_url': doc['url'],
// 'file_url': doc['url'],
// 'title': doc['title'],
// 'file_size': '0 KB',
// 'file_format': 'PDF',
// }
// }
export default class App extends Component {
constructor(props) {
super(props)
this.onMarkerUpdate = this.onMarkerUpdate.bind(this)
this.state = {
tokens: [],
}
}
onMarkerUpdate(tokens) {
// console.log('Update')
// console.log(tokens)
this.setState({tokens})
}
componentWillMount() {
const {data, marker} = this.props
marker.evaluate(data)
.then(tokens => {
this.setState({
tokens,
})
})
// this.setState({
// data: this
// .getData()
// .reduce((acc, x) => [...acc, ...x], []),
// })
}
render() {
return (
<div className="App">
<div className={'App-Body'}>
<Editor/>
{/* <Marker */}
{/* data={this.state.tokens} */}
{/* marker={this.props.marker} */}
{/* onUpdate={this.onMarkerUpdate} */}
{/* /> */}
</div>
</div>
)
}
}
// export default connect(mapStateToProps, mapDispatchToProps)(App)
+9
View File
@@ -0,0 +1,9 @@
.App {
display: flex;
justify-content: center;
&-Body {
padding-top: 10px;
width: 800px;
}
}
+8
View File
@@ -0,0 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
});
+26
View File
@@ -0,0 +1,26 @@
.AppWrapper {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 1000;
display: flex;
flex-direction: column;
justify-content: flex-start; /* align items in Main Axis */
align-items: stretch; /* align items in Cross Axis */
align-content: stretch; /* Extra space in Cross Axis */
pointer-events: none;
}
.AppWrapper footer {
background-color: rgba(34, 34, 34, 0.95);
color: #ebebeb;
padding: 1em 0.5em 0.5em;
font-weight: 100;
}
+88
View File
@@ -0,0 +1,88 @@
import React, {Component} from 'react'
import {Link} from 'react-router'
import './AppWrapper.css'
import './AppWrapperHeader.css'
import './AppWrapperBody.css'
import './AppWrapperContent.css'
import './Icon.css'
const Icon = ({children: iconName}) => (
<span className={`Icon ${iconName}`}/>
)
class Toggle extends Component {
constructor(props) {
super(props)
this.state = {
value: true
}
}
onClick = () => {
const {value} = this.state
const {onClick} = this.props
this.setState({
value: !value,
})
onClick(!value)
}
render() {
const {value} = this.state
const icon = value
? 'IconFullScreenOn'
: 'IconFullScreenOff'
return (
<button className="InvisibleButton" onClick={this.onClick}>
<Icon>{icon}</Icon>
</button>
)
}
}
const Head = ({title, onToggleFullPage, showFullPageToggle, showControls = false}) => (
<header className="AppWrapperHeader">
<div>
</div>
<div className="AppWrapperHeader-main">
<h1>{title}</h1>
</div>
<div>
{!showControls ? null : (
<div className="controls">
{!showFullPageToggle ? null: (
<Toggle onClick={onToggleFullPage}/>
)}
</div>
)}
</div>
</header>
)
const Body = ({children}) => (
<div className="AppWrapperBody">
<div className="AppWrapperContent">
{children}
</div>
</div>
)
const AppWrapper = ({title, children, showControls, showFullPageToggle, onToggleFullPage}) => (
<div className="AppWrapper">
<Head
title={title}
showControls={showControls}
onToggleFullPage={onToggleFullPage}
showFullPageToggle={showFullPageToggle}
/>
<Body>{children}</Body>
</div>
)
export default AppWrapper
+7
View File
@@ -0,0 +1,7 @@
.AppWrapperBody {
display: flex;
flex: 1;
width: 100%;
height: 100%;
}
@@ -0,0 +1,11 @@
.AppWrapperContent {
margin: 0;
padding: 0;
width: 100%;
position: relative;
/* Needed for when the area gets squished too far and there is content
that can't be displayed */
overflow: auto;
}
@@ -0,0 +1,39 @@
.AppWrapperHeader {
background-color: rgba(34, 34, 34, 0.95);
color: #ebebeb;
/*background-color: rgba(255, 255, 255, 0.95);*/
/*color: #222;*/
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.15);
pointer-events: auto;
display: flex;
align-items: center;
}
.AppWrapperHeader-main {
flex: 1;
}
.AppWrapperHeader h1 {
text-align: center;
margin: 0;
padding: .5em 0 .4em;
font-size: 1.25rem;
font-weight: 100;
}
.controls {
margin: 0 15px;
}
.controls a{
color: #bbb;
text-decoration: none;
}
.controls a:hover{
color: white;
}
+37
View File
@@ -0,0 +1,37 @@
.Icon {
display: inline-block;
width: 20px;
height: 20px;
/*background: grey;*/
background-size: 100%;
}
.IconFullScreenOn {
background-image: url('/icon-fullscreen-on.svg');
}
.IconFullScreenOff {
background-image: url('/icon-fullscreen-off.svg');
}
.InvisibleButton {
background: none;
border: none;
outline: none;
}
.InvisibleButton {
width: 24px;
height: 24px;
padding: 0;
border-radius: 0;
cursor: pointer;
}
/*.InvisibleButton:focus {*/
/*border: 2px solid #2f9fff;*/
/*}*/
+107
View File
@@ -0,0 +1,107 @@
import {combineReducers} from 'redux'
import api from '../../../api'
import {pack} from '../../../lib/fn'
import {isMobile, isLandscape} from '../../../lib/device'
const FETCH_PROJECTS = 'arch/app/FETCH_PROJECTS'
const FOCUS_PROJECT = 'arch/app/FOCUS_PROJECT'
const TOGGLE_FULL_PAGE = 'arch/app/TOGGLE_FULL_PAGE'
const CHANGE_PAGE_ORIENTATION = 'arch/app/CHANGE_PAGE_ORIENTATION'
export const PAGE_ORIENTATION_LANDSCAPE = 'landscape'
export const PAGE_ORIENTATION_PORTRAIT = 'portrait'
export function fetchProjects() {
return {
type: FETCH_PROJECTS,
async: api.projects.fetch()
.then(pack('data'))
}
}
export function focusProject(projectName) {
return {
type: FOCUS_PROJECT,
projectName
}
}
export function toggleFullPage() {
return {
type: TOGGLE_FULL_PAGE,
}
}
export function changePageOrientation(orientation) {
return {
type: CHANGE_PAGE_ORIENTATION,
orientation,
}
}
const init = {
items: [],
total: 0,
activeProject: null,
}
const initPage = {
fullPage: false,
halfPageAvailable: !isMobile(),
isVertical: !isLandscape(),
}
function projects(state = init, action) {
switch (action.type) {
case FETCH_PROJECTS: {
const {items, total} = action.data
return {...state, items, total}
}
case FOCUS_PROJECT: {
const projectName = action.projectName
const activeProject = state.items.find(p => p.name === projectName)
return {...state, activeProject}
}
default:
return state
}
}
function page(state = initPage, action) {
switch (action.type) {
case TOGGLE_FULL_PAGE: {
const fullPage = !state.fullPage
return {...state, fullPage}
}
case CHANGE_PAGE_ORIENTATION: {
const {orientation} = action
const isVertical = orientation === PAGE_ORIENTATION_PORTRAIT
return {...state, isVertical}
}
case FOCUS_PROJECT: {
let fullPage = state.fullPage
const projectName = action.projectName
fullPage = !projectName
? false
: fullPage
return {...state, fullPage}
}
default:
return state
}
}
export default combineReducers({
projects,
page,
})
+1
View File
@@ -0,0 +1 @@
export {default} from './App'
+11
View File
@@ -0,0 +1,11 @@
/**
* Draft v0.10.5
*
* Copyright (c) 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
.DraftEditor-editorContainer,.DraftEditor-root,.public-DraftEditor-content{height:inherit;text-align:initial}.public-DraftEditor-content[contenteditable=true]{-webkit-user-modify:read-write-plaintext-only}.DraftEditor-root{position:relative}.DraftEditor-editorContainer{background-color:rgba(255,255,255,0);border-left:.1px solid transparent;position:relative;z-index:1}.public-DraftEditor-block{position:relative}.DraftEditor-alignLeft .public-DraftStyleDefault-block{text-align:left}.DraftEditor-alignLeft .public-DraftEditorPlaceholder-root{left:0;text-align:left}.DraftEditor-alignCenter .public-DraftStyleDefault-block{text-align:center}.DraftEditor-alignCenter .public-DraftEditorPlaceholder-root{margin:0 auto;text-align:center;width:100%}.DraftEditor-alignRight .public-DraftStyleDefault-block{text-align:right}.DraftEditor-alignRight .public-DraftEditorPlaceholder-root{right:0;text-align:right}.public-DraftEditorPlaceholder-root{color:#9197a3;position:absolute;z-index:1}.public-DraftEditorPlaceholder-hasFocus{color:#bdc1c9}.DraftEditorPlaceholder-hidden{display:none}.public-DraftStyleDefault-block{position:relative;white-space:pre-wrap}.public-DraftStyleDefault-ltr{direction:ltr;text-align:left}.public-DraftStyleDefault-rtl{direction:rtl;text-align:right}.public-DraftStyleDefault-listLTR{direction:ltr}.public-DraftStyleDefault-listRTL{direction:rtl}.public-DraftStyleDefault-ol,.public-DraftStyleDefault-ul{margin:16px 0;padding:0}.public-DraftStyleDefault-depth0.public-DraftStyleDefault-listLTR{margin-left:1.5em}.public-DraftStyleDefault-depth0.public-DraftStyleDefault-listRTL{margin-right:1.5em}.public-DraftStyleDefault-depth1.public-DraftStyleDefault-listLTR{margin-left:3em}.public-DraftStyleDefault-depth1.public-DraftStyleDefault-listRTL{margin-right:3em}.public-DraftStyleDefault-depth2.public-DraftStyleDefault-listLTR{margin-left:4.5em}.public-DraftStyleDefault-depth2.public-DraftStyleDefault-listRTL{margin-right:4.5em}.public-DraftStyleDefault-depth3.public-DraftStyleDefault-listLTR{margin-left:6em}.public-DraftStyleDefault-depth3.public-DraftStyleDefault-listRTL{margin-right:6em}.public-DraftStyleDefault-depth4.public-DraftStyleDefault-listLTR{margin-left:7.5em}.public-DraftStyleDefault-depth4.public-DraftStyleDefault-listRTL{margin-right:7.5em}.public-DraftStyleDefault-unorderedListItem{list-style-type:square;position:relative}.public-DraftStyleDefault-unorderedListItem.public-DraftStyleDefault-depth0{list-style-type:disc}.public-DraftStyleDefault-unorderedListItem.public-DraftStyleDefault-depth1{list-style-type:circle}.public-DraftStyleDefault-orderedListItem{list-style-type:none;position:relative}.public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-listLTR:before{left:-36px;position:absolute;text-align:right;width:30px}.public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-listRTL:before{position:absolute;right:-36px;text-align:left;width:30px}.public-DraftStyleDefault-orderedListItem:before{content:counter(ol0) ". ";counter-increment:ol0}.public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth1:before{content:counter(ol1) ". ";counter-increment:ol1}.public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth2:before{content:counter(ol2) ". ";counter-increment:ol2}.public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth3:before{content:counter(ol3) ". ";counter-increment:ol3}.public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth4:before{content:counter(ol4) ". ";counter-increment:ol4}.public-DraftStyleDefault-depth0.public-DraftStyleDefault-reset{counter-reset:ol0}.public-DraftStyleDefault-depth1.public-DraftStyleDefault-reset{counter-reset:ol1}.public-DraftStyleDefault-depth2.public-DraftStyleDefault-reset{counter-reset:ol2}.public-DraftStyleDefault-depth3.public-DraftStyleDefault-reset{counter-reset:ol3}.public-DraftStyleDefault-depth4.public-DraftStyleDefault-reset{counter-reset:ol4}
+313
View File
@@ -0,0 +1,313 @@
import React from 'react'
import Immutable from 'immutable'
import {
DefaultDraftBlockRenderMap,
EditorState,
RichUtils,
convertFromRaw,
getDefaultKeyBinding,
AtomicBlockUtils,
Modifier
} from 'draft-js'
import DraftEditor, {composeDecorators} from 'draft-js-plugins-editor'
import createImagePlugin from 'draft-js-image-plugin'
import createAlignmentPlugin from 'draft-js-alignment-plugin'
import createFocusPlugin from 'draft-js-focus-plugin'
import createResizeablePlugin from 'draft-js-resizeable-plugin'
import createBlockDndPlugin from 'draft-js-drag-n-drop-plugin'
// import createDragNDropUploadPlugin from 'draft-js-drag-n-drop-upload-plugin'
import createDndFileUploadPlugin from '@mikeljames/draft-js-drag-n-drop-upload-plugin'
// import './Draft.less'
import 'draft-js/dist/Draft.css'
import 'draft-js-image-plugin/lib/plugin.css'
const focusPlugin = createFocusPlugin()
const resizeablePlugin = createResizeablePlugin()
const blockDndPlugin = createBlockDndPlugin()
const alignmentPlugin = createAlignmentPlugin()
const {AlignmentTool} = alignmentPlugin
const decorator = composeDecorators(
resizeablePlugin.decorator,
alignmentPlugin.decorator,
focusPlugin.decorator,
blockDndPlugin.decorator
)
const imagePlugin = createImagePlugin({decorator})
const dndFileUploadPlugin = createDndFileUploadPlugin({
handleUpload: console.log,
addImage: imagePlugin.addImage,
})
// const dragNDropFileUploadPlugin = createDragNDropUploadPlugin({
// handleUpload: mockUpload,
// addImage: imagePlugin.addImage,
// })
const plugins = [
dndFileUploadPlugin,
blockDndPlugin,
focusPlugin,
alignmentPlugin,
resizeablePlugin,
imagePlugin
]
const initialState = {
'entityMap': {
'0': {
'type': 'IMAGE',
'mutability': 'IMMUTABLE',
'data': {
'src': 'https://static.shlisselburg.org/art/images/7c206b4e211bb8486801fd84a3bf309b-big.jpg'
}
}
},
'blocks': [
{
'key': '9gm3s',
'text': 'You can have images in your text field. This is a very rudimentary example, but you can enhance the image plugin with resizing, focus or alignment plugins.',
'type': 'unstyled',
'depth': 0,
'inlineStyleRanges': [],
'entityRanges': [],
'data': {}
},
{
'key': 'ov7r',
'text': ' ',
'type': 'atomic',
'depth': 0,
'inlineStyleRanges': [],
'entityRanges': [{
'offset': 0,
'length': 1,
'key': 0
}],
'data': {}
},
{
'key': 'e23a8',
'text': 'See advanced examples further down …',
'type': 'unstyled',
'depth': 0,
'inlineStyleRanges': [],
'entityRanges': [],
'data': {}
}
]
}
class MyCustomBlock extends React.Component {
// constructor(props) {
// super(props)
// }
render() {
return (
<div className='MyCustomBlock'>
{/* here, this.props.children contains a <section> container, as that was the matching element */}
{this.props.children}
</div>
)
}
}
class MediaComponent extends React.Component {
render() {
const {block, contentState} = this.props
const {foo} = this.props.blockProps
const data = contentState.getEntity(block.getEntityAt(0)).getData()
console.log(data)
return (
<i>
{data.text}
<img src={data.text} alt=""/>
</i>
)
// Return a <figure> or some other content using this data.
}
}
function blockRendererFn(contentBlock) {
const type = contentBlock.getType()
if (type === 'atomic') {
console.log('blockRendererFn', contentBlock.getKey(), type)
return {
component: MediaComponent,
editable: false,
props: {
foo: 'bar',
},
}
} else {
return null
}
}
const blockRenderMap = Immutable.Map({
'section': {
element: 'section',
wrapper: MyCustomBlock,
}
})
// Include 'paragraph' as a valid block and updated the unstyled element but
// keep support for other draft default block types
const extendedBlockRenderMap = DefaultDraftBlockRenderMap.merge(blockRenderMap)
export default class Editor extends React.Component {
constructor(props) {
super(props)
this.state = {
editorState: EditorState.createWithContent(convertFromRaw(initialState)),
}
this.onChange = this.onChange.bind(this)
this.onBoldClick = () => {
this.onChange(RichUtils.toggleInlineStyle(this.state.editorState, 'BOLD'))
}
this.focus = () => {
this.editor.focus()
}
this.onZomboClick = () => {
const contentState = this.state.editorState.getCurrentContent();
const contentStateWithEntity = contentState.createEntity(
'LINK',
'MUTABLE',
{url: 'http://www.zombo.com'}
)
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
const contentStateWithLink = Modifier.applyEntity(
contentStateWithEntity,
selectionState,
entityKey
)
this.onChange(contentStateWithLink);
// this.onChange(RichUtils.toggleInlineStyle(this.state.editorState, 'BOLD'))
}
this.handleKeyCommand = this.handleKeyCommand.bind(this)
this.keyBindingFn = this.keyBindingFn.bind(this)
this.insertBlock = this.insertBlock.bind(this)
}
insertBlock(type, data) {
const {editorState} = this.state
const contentState = editorState.getCurrentContent()
const contentStateWithEntity = contentState.createEntity(type, 'IMMUTABLE', data)
const entityKey = contentStateWithEntity.getLastCreatedEntityKey()
const newEditorState = EditorState.set(editorState, {
currentContent: contentStateWithEntity
})
this.setState({
editorState: AtomicBlockUtils.insertAtomicBlock(newEditorState, entityKey, ' '),
})
}
onChange(editorState) {
const content = editorState.getCurrentContent()
const s = editorState.getSelection()
const key = s.getStartKey()
const prevKey = content.getKeyBefore(key)
const block = content.getBlockForKey(key)
const prevBlock = content.getBlockForKey(prevKey)
const text = block.getText()
if (/\.jpg/.test(text)) {
this.insertBlock('jpg', {text})
// const newContent = Modifier.setBlockType(content, s, 'atomic')
//
// const newEditorState = EditorState.set(editorState, {
// currentContent: newContent,
// })
//
// this.setState({
// editorState: newEditorState,
// })
} else {
this.setState({editorState})
}
// const blockData = block.getData()
// console.log(block.getText(), block.getType())
// console.log(block.getEntityAt(0))
}
keyBindingFn(e) {
// console.log("K", e.key)
// if (e.key === 'Enter') {
// return 'enter'
// }
return getDefaultKeyBinding(e)
}
handleKeyCommand(command) {
console.log('CMD', command)
const newState = RichUtils.handleKeyCommand(this.state.editorState, command)
if (newState) {
this.onChange(newState)
return 'handled'
}
return 'not-handled'
}
render() {
const content = this.state.editorState.getCurrentContent()
const e = this.state.editorState
const s = e.getSelection()
const key = s.getStartKey()
const so = s.getStartOffset()
const block = content.getBlockForKey(key)
return (
<div>
<DraftEditor
editorState={this.state.editorState}
onChange={this.onChange}
blockRendererFn={blockRendererFn}
blockRenderMap={extendedBlockRenderMap}
handleKeyCommand={this.handleKeyCommand}
keyBindingFn={this.keyBindingFn}
plugins={plugins}
ref={(element) => {
this.editor = element
}}
/>
{/* <AlignmentTool/> */}
<button onClick={this.onBoldClick}>Bold</button>
<div>
<p>
block key: {key} type: {block.getType()}
</p>
<p>
start offset {so}
</p>
<pre>
{content.getPlainText('\n')}
</pre>
</div>
</div>
)
}
}
Whitespace-only changes.
+1
View File
@@ -0,0 +1 @@
export {default} from './Editor'
@@ -0,0 +1,78 @@
import React, {Component} from 'react'
export default class ContentEditable extends Component {
constructor(props) {
super(props)
this.emitChange = this.emitChange.bind(this)
this.onRef = this.onRef.bind(this)
}
getHtml() {
return this.props.children
}
onRef(element) {
this.htmlEl = element
}
emitChange() {
if (!this.htmlEl) return
const html = this.htmlEl.innerHTML
if (html !== this.lastHtml) this.props.onChange(html)
this.lastHtml = html
}
componentDidMount() {
this.htmlEl.focus()
}
shouldComponentUpdate(nextProps) {
let {props, htmlEl} = this
// We need not rerender if the change of props simply reflects the user's edits.
// Rerendering in this case would make the cursor/caret jump
// Rerender if there is no element yet... (somehow?)
if (!this.htmlEl) return true
// // ...or if html really changed... (programmatically, not by user edit)
// if (nextProps.html !== htmlEl.innerHTML && nextProps.html !== props.html) {
// return true
// }
return false
// let optional = ['style', 'className', 'disabled', 'tagName']
//
// // Handle additional properties
// return optional.some(name => props[name] !== nextProps[name])
}
componentDidUpdate() {
const html = this.getHtml()
if (this.htmlEl && this.htmlEl.innerHTML !== html) {
// Perhaps React (whose VDOM gets outdated because we often prevent
// rerendering) did not update the DOM. So we update it manually now.
this.htmlEl.innerHTML = html
}
}
render() {
const {mix} = this.props;
return <div
className={mix}
ref={this.onRef}
onInput={this.emitChange}
onChange={this.emitChange}
// onBlur={this.props.onBlur || this.emitChange}
contentEditable={true}
suppressContentEditableWarning={true}
// dangerouslySetInnerHTML={{__html: this.getHtml()}}
>
{this.getHtml()}
</div>
}
}
+214
View File
@@ -0,0 +1,214 @@
import React, {Component} from 'react'
import {MarkerTabIndex} from './MarkerTabIndex'
import {MarkerCell} from './MarkerCell'
import './Marker.less'
import {exist} from '../../lib/fn'
import ContentEditable from './ContentEditable'
import {constrain} from "./lib/fn";
export class Marker extends Component {
constructor(props) {
super(props)
this.state = {
currentCellIndex: 0,
}
this.onCellUpdate = this.onCellUpdate.bind(this)
this.onCellSelect = this.onCellSelect.bind(this)
this.onKeyDown = this.onKeyDown.bind(this)
}
onKeyDown(event) {
switch (event.key) {
case 'ArrowUp': {
console.log('Marker ArrowUp')
this.setState({
currentCellIndex: this.getPrevCellIndex(),
})
break
}
case 'ArrowDown': {
console.log('Marker ArrowDown')
this.setState({
currentCellIndex: this.getNextCellIndex(),
})
break
}
case 'Enter': {
console.log('Marker Enter')
const s = getSelection()
console.log(s, s.getRangeAt(0))
event.preventDefault()
// if (!event.shiftKey) return
// event.stopPropagation()
return this.handleEnter()
}
}
};
getCellIndex(n) {
const {data} = this.props
const {currentCellIndex} = this.state
const index = currentCellIndex + n
return constrain(index, 0, data.length - 1)
}
getPrevCellIndex(...args) {
return this.getCellIndex(-1, ...args)
}
getNextCellIndex(...args) {
return this.getCellIndex(1, ...args)
}
onCellSelect(index) {
this.setState({
currentCellIndex: index,
})
}
getDataAt(index) {
return this.props.data[index]
}
getBlankTextAt(index) {
if (index === 0) return ''
const cellBefore = this.getDataAt(index)
const textBefore = cellBefore.data
let text
const ol = (text) => {
const m = /^(\d+)\.\s/.exec(text)
if (m) {
const d = parseFloat(m[1])
return `${d + 1}.`
} else {
return null
}
}
const ul = (text) => /^-\s/.test(text)
? '- '
: null
text = ol(textBefore)
if (text) return text
text = ul(textBefore)
if (text) return text
return ''
}
async handleEnter() {
const {currentCellIndex} = this.state
const currentCell = this.getDataAt(currentCellIndex)
if (currentCell.data) {
const nextCellIndex = currentCellIndex + 1
if (nextCellIndex > this.props.data.length - 1) {
const {data, marker, onUpdate} = this.props
const blankText = this.getBlankTextAt(currentCellIndex)
const newCell = await marker.blank({text: blankText})
const newData = [
...data,
newCell,
]
onUpdate(newData)
} else {
}
this.setState({
currentCellIndex: nextCellIndex,
})
}
}
async onCellUpdate(index, text) {
console.log('UPDATE', text)
const {data, marker, onUpdate} = this.props
const cells = await marker.evaluate(text)
const before = data.slice(0, index)
const after = data.slice(index + 1)
let newData = [
...before,
...cells,
...after,
]
// const next = index + cells.length
// if (next > newData.length - 1) {
// const newCell = await marker.blank()
// newData.push(newCell)
// }
console.log(newData.map(x => x.data).join('\n'))
onUpdate(newData)
}
componentWillMount() {
// this.setState({
// currentCellIndex: 0,
// })
}
render() {
const {data} = this.props
if (!data.length) return null
const {currentCellIndex} = this.state
const tabIndex = 1
return (
<div
className='Marker'
// onKeyDownCapture={this.onKeyDown}
onKeyDown={this.onKeyDown}
>
{data.map((x, i) => {
return (i === currentCellIndex ? (
<ContentEditable
mix={'MarkerCell'}
tabIndex={tabIndex + i}
key={i}
onChange={text => this.onCellUpdate(i, text)}
>
{x.data}
</ContentEditable>
) : (
<div
className={'MarkerCell'}
tabIndex={tabIndex + i}
key={i}
onFocus={() => this.onCellSelect(i)}
>
{x.render()}
</div>
)
)
})}
</div>
)
}
}
+52
View File
@@ -0,0 +1,52 @@
.Marker {
//--active-color: #73acf6;
--active-color: #f0f0f0;
--cell-padding: 8px;
--font-size: 16px;
font-size: var(--font-size);
font-family: sans-serif;
//line-height: var(--font-size);
textarea {
width: 100%;
font-size: var(--font-size);
font-family: sans-serif;
padding: 0;
box-sizing: border-box;
height: var(--font-size);
//height: calc(2px + var(--font-size));
border: none;
background-color: var(--active-color);
outline: none;
display: block;
}
}
.MarkerCellTabIndex {
outline: none;
}
.MarkerCell {
//padding: var(--cell-padding);
padding-bottom: 8px;
position: relative;
&--active {
background-color: var(--active-color);
//background-color: #f6903b;
}
&--edit {
div {
outline: none;
}
//background-color: #00b7ec;
background-color: var(--active-color);
}
}
+72
View File
@@ -0,0 +1,72 @@
import React, {Component} from 'react'
import className from 'classnames'
import {trimNewline} from './lib/fn'
import './Marker.less'
import {MarkerEdit} from './MarkerEdit'
const Cell = ({children, active = false, edit = false}) => (
<div className={className('MarkerCell', {
'MarkerCell--active': active,
'MarkerCell--edit': edit,
})}>
{children}
</div>
)
export class MarkerCell extends Component {
constructor(props) {
super(props)
this.onSubmit = this.onSubmit.bind(this)
this.onChange = this.onChange.bind(this)
this.state = {
content: null,
}
}
getCellData() {
return this.state.content
? this.state.content
: this.props.cell.data
}
getCellComponent() {
return this.props.cell.render()
}
onSubmit(e) {
e.preventDefault()
const {onUpdate} = this.props
onUpdate(this.getCellData())
}
onChange(e) {
const content = trimNewline(e.target.value)
this.setState({
content,
})
}
render() {
const {active, edit} = this.props
return edit
? (
<Cell edit={edit}>
<MarkerEdit
onSubmit={this.onSubmit}
onChange={this.onChange}
>
{this.getCellData()}
</MarkerEdit>
</Cell>
) : (
<Cell active={active}>
{this.getCellComponent()}
</Cell>
)
}
}
+85
View File
@@ -0,0 +1,85 @@
import React, {Component, createContext} from 'react'
import className from 'classnames'
import {trim} from './lib/fn'
import {MarkerContext} from './Marker'
import './Marker.less'
import ContentEditable from './ContentEditable'
export class MarkerEdit extends Component {
constructor(props) {
super(props)
// this.onInputRef = this.onInputRef.bind(this)
this.onChange = this.onChange.bind(this)
this.onFocus = this.onFocus.bind(this)
this.onKeyDown = this.onKeyDown.bind(this)
}
onChange(event) {
console.log(event, event.target)
this.props.onChange(event)
}
onFocus(e) {
e.stopPropagation()
}
onKeyDown(event) {
switch (event.key) {
case 'ArrowUp': {
if (event.shiftKey) return
event.stopPropagation()
break
}
case 'ArrowDown': {
if (event.shiftKey) return
event.stopPropagation()
break
}
case 'Enter': {
console.log('MarkerEdit Enter')
if (event.shiftKey) return
// event.stopPropagation()
const {onSubmit} = this.props
onSubmit(event)
}
}
}
// onInputRef(e) {
// this.input = e
// }
//
// componentDidMount() {
// this.input.focus()
// }
render() {
const {children} = this.props
return (
<ContentEditable
// ref={this.onInputRef}
onChange={this.onChange}
onFocus={this.onFocus}
onKeyDown={this.onKeyDown}
>
{children}
</ContentEditable>
)
// return (
// <textarea
// value={children}
// ref={this.onInputRef}
// onChange={onChange}
// onFocus={this.onFocus}
// onKeyDown={this.onKeyDown}
// />
// )
}
}
@@ -0,0 +1,43 @@
import React, {Component, createContext} from 'react'
import './Marker.less'
export class MarkerTabIndex extends Component {
constructor(props) {
super(props)
this.onFocus = this.onFocus.bind(this)
this.onRef = this.onRef.bind(this)
}
onFocus() {
const {onFocus, data} = this.props
onFocus(data)
}
onRef(e) {
this.element = e
}
componentDidUpdate() {
const {focus} = this.props
if (focus) {
this.element.focus()
}
}
render() {
const {tabIndex, children} = this.props
return (
<div
className={'MarkerCellTabIndex'}
children={children}
tabIndex={tabIndex}
onFocus={this.onFocus}
ref={this.onRef}
role={'button'}
/>
)
}
}
+28
View File
@@ -0,0 +1,28 @@
import React from 'react'
import './Marker.less'
export const MarkerWidgetText = ({text}) => (
<div
className='MarkerWidgetText'
>
{text}
</div>
)
export const MarkerWidgetHtml = ({html}) => (
<div
className='MarkerWidgetHtml'
dangerouslySetInnerHTML={{__html: html}}
/>
)
export const MarkerWidgetUrl = ({url, text}) => (
<div
className='MarkerWidgetUrl'
>
<a href={url}>
{text}
</a>
</div>
)
+1
View File
@@ -0,0 +1 @@
export {Marker as default} from './Marker'
+31
View File
@@ -0,0 +1,31 @@
/**
* Remove trailing spaces from string
*
* f(_x_) => x
*
* @param str
*/
export function trim(str) {
return str
.replace(/^\s+/, '')
.replace(/\s+$/, '')
}
/**
* Remove trailing spaces from string
*
* f(_x_) => x
*
* @param str
*/
export function trimNewline(str) {
return str
.replace(/^\n+/, '')
.replace(/\n+$/, '')
}
export function constrain(n, min, max) {
if (n < min) return min
if (n > max) return max
return n
}
@@ -0,0 +1,34 @@
import React from 'react'
import Token from './Token'
import {trim} from '../fn'
import {getHtml} from './index'
export default class FileToken extends Token {
static test(data) {
return is_file(data, documentExtensions + imageExtensions)
}
constructor({data}) {
super({
name: 'file',
data,
})
this.joinable = false
}
async compile() {
const data = parse_file(this.getData())
const url = data['file']
const text = data.caption
? data.caption
: url
return (
<a href={url}>
{text}
</a>
)
}
}
@@ -0,0 +1,78 @@
import React from 'react'
import Token from './Token'
import {isImage, parseFile} from './index'
export class ImageToken extends Token {
static test(data) {
return isImage(data)
}
constructor({data}) {
super({
name: 'image',
data,
})
this.joinable = true
this.build = null
}
merge(token) {
const mergedToken = new ImageCollectionToken([this.data, token.data])
mergedToken.build = this.build
return mergedToken
}
async compile() {
}
}
export class ImageCollectionToken extends ImageToken {
constructor({data}) {
super({data})
this.name = 'image_collection'
this.build = null
}
merge(token) {
const data = Array.isArray(token.getData())
? token.getData()
: [token.getData()]
const mergedToken = new ImageCollectionToken({
data: [...self.getData(), ...data]
})
mergedToken.build = self.build
return mergedToken
}
async compile() {
const data = this.parseData()
let images = []
for (let x of data) {
const img = await this.build(x)
images.push(img)
}
return (
<div className='kazimir__image-collection'>
<div
className='fotorama'
data-width='100%'
data-ratio='800/600'
>
{images.map(({src}) => (
<img src={src}/>
))}
</div>
</div>
)
}
parseData() {
return this
.getData()
.map(parseFile)
}
}
@@ -0,0 +1,138 @@
import SplitToken from './SplitToken'
export class Marker {
constructor() {
this.tokenFactories = []
this.treeMiddlewares = []
this.join = true
this.blankFactory = null
}
async blank({text}) {
if (!this.blankFactory) {
throw new Error(`Blank factory is not defined`)
}
const token = await this.blankFactory.create(text)
return token.compile()
}
addTokenFactory(tokenFactory, {blank = false} = {}) {
this.tokenFactories.push(tokenFactory)
if (blank) {
this.blankFactory = tokenFactory
}
return this
}
addTreeMiddleware(callback) {
this.treeMiddlewares.push(callback)
return this
}
async evaluate(text) {
let tokens = text.split('\n')
tokens = this.markupTokens(tokens)
if (this.join) {
tokens = this.joinTokens(tokens)
}
return compileTokens(tokens)
// tree = lxml.html.fromstring(compiled)
// return await self.process_tree(tree)
}
joinTokens(tokens) {
return mergeTokens(joinTokens(tokens))
}
async processTree(tree) {
// for fn in self.treeMiddlewares:
// tree = await fn(tree)
// return tree
}
markupTokens(tokens) {
const markedTokens = []
tokens.forEach(data => {
const token = this.createToken(data)
if (!token) {
throw new Error(`Token not recognized for data ${data}`)
}
markedTokens.push(token)
})
return markedTokens
}
createToken(data) {
for (let factory of this.tokenFactories) {
if (factory.test(data)) {
return factory.create(data)
}
}
return null
}
}
function joinTokens(tokens) {
const isSplit = x => x instanceof SplitToken
let bufferTag = null
let tokensBuffer = []
let tokensList = []
for (let token of tokens) {
const tag = token.name
const changed = bufferTag !== null && tag !== bufferTag
let flush = isSplit(token)
if (changed) {
flush = true
}
if (!token.isJoinable()) {
flush = true
}
if (flush) {
if (tokensBuffer.length) {
tokensList.push(tokensBuffer)
}
if (isSplit(token)) {
tokensBuffer = []
bufferTag = null
} else {
tokensBuffer = [token]
bufferTag = tag
}
} else {
bufferTag = tag
tokensBuffer.push(token)
}
}
if (tokensBuffer.length) {
tokensList.push(tokensBuffer)
}
return tokensList
}
function mergeTokens(tokensList) {
return tokensList
.map(ts => ts
.reduce(
(token, x) => token.merge(x)
)
)
}
async function compileTokens(tokens) {
return Promise.all(
tokens.map(x => x.compile())
)
}
@@ -0,0 +1,24 @@
import Token from './Token'
import {trim} from '../fn'
export default class SplitToken extends Token {
static test(data) {
data = trim(data)
return data === ''
}
constructor(options) {
super({
...options,
name: 'split',
})
}
render() {
return null
}
merge(token) {
return this
}
}
@@ -0,0 +1,44 @@
import Token from './Token'
import {trim} from '../fn'
import {getHtml} from './index'
export default class TextToken extends Token {
static test(data) {
return true
}
constructor(options) {
super({
...options,
name: 'text',
})
}
merge(token) {
const data = [this.data, token.data].join('\n')
return new TextToken(this.getMergeOptions({
data: trim(data),
}))
}
async getData() {
const html = getHtml(this.data)
return {html}
}
// async compile() {
// // return getHtml(this.getData())
//
//
//
//
//
// return (
// <div
// className='MarkerWidgetHtml'
// dangerouslySetInnerHTML={{__html: html}}
// />
// )
// }
}
@@ -0,0 +1,51 @@
export default class Token {
static test(data) {
return true
}
constructor({name, data, component}) {
this.name = name
this.data = data
this.component = component
this.joinable = true
this.compiledData = null
}
isJoinable() {
return this.joinable
}
render() {
if (!this.compiledData) {
throw new Error('Token not compiled. Call Token::compile before.')
}
return this.component(this.compiledData)
}
async getData() {
return this.data
}
async compile() {
this.compiledData = await this.getData()
return this
}
merge(token) {
return this
}
getMergeOptions(extend) {
return {
name: this.name,
data: this.data,
component: this.component,
...extend,
}
}
toString() {
return `[Token ${this.name} ${this.data}]`
}
}
@@ -0,0 +1,35 @@
export class TokenFactory {
constructor({token, component}) {
this.token = token
this.component = component
}
test(sample) {
return this.token.test(sample)
}
create(data) {
const Token = this.token
return new Token({
data,
component: this.component,
})
}
}
export class BuildTokenFactory extends TokenFactory {
constructor({token, build, component}) {
super({token})
this.build = build
this.component = component
}
create(data) {
const Token = this.token
return new Token({
data,
component: this.component,
build: this.build,
})
}
}
@@ -0,0 +1,16 @@
import Token from './Token'
import {isUrl} from './index'
export default class UrlToken extends Token {
static test(data) {
return isUrl(data)
}
constructor(options) {
super({
...options,
name: 'url',
})
this.joinable = false
}
}
@@ -0,0 +1,24 @@
import {isUrl} from './index'
import React from 'react'
import UrlToken from './UrlToken'
export default class YoutubeToken extends UrlToken {
static test(data) {
if (!isUrl(data)) {
return false
}
const url = new URL(data)
return ['www.youtube.com', 'youtube.com']
.includes(url.hostname)
}
async getData() {
const url = new URL(this.data)
const youtubeId = url.searchParams.get('v')
return {
url: `//www.youtube.com/embed/${youtubeId}`
}
}
}
@@ -0,0 +1,76 @@
import {markdown} from 'markdown'
const {toHTML: markdownToHtml} = markdown
const splitExt = s => {
const parts = s.split('.')
const name = parts.splice(0, parts.length - 1)
const ext = parts[parts.length - 1]
return {name, ext}
}
export const imageExtensions = ['.jpg', '.jpeg', '.png']
export const documentExtensions = ['.doc', '.docx', '.pdf', '.xls', '.xlsx']
export function getHtml(text) {
return markdownToHtml(text)
}
export function isUrl(sample) {
try {
return Boolean(new URL(sample))
} catch (e) {
return false
}
}
export function isImage(sample) {
const file = parseFile(sample)['file']
return isFile(file, imageExtensions)
}
export function isDocument(sample) {
const file = parseFile(sample)['file']
return isFile(file, documentExtensions)
}
export function isFile(sample, extensions) {
let {name, ext} = splitExt(sample)
ext = ext.toLowerCase()
return extensions.include(ext)
}
export function parseFile(sample) {
const re = /![(.*)]((.*))/
const p = re.exec(sample)
let caption
let file
if (p) {
caption = p.group(1)
file = p.group(2)
} else {
sample = sample.split(' ')
file = sample[0]
caption = ' '.join(sample.substr(1))
}
return {
file,
caption,
}
}
export function parseLink(sample) {
const re = /![(.*)]((.*))/
const p = re.exec(sample)
if (!p) {
return null
}
const url = p[2]
return {
url: new URL(url),
alt: p[1],
}
}
+35
View File
@@ -0,0 +1,35 @@
:root {
--content-width-state-normal: 60%;
--content-width-state-full: 100%;
--content-width: var(--content-width-state-normal);
--module-m: .5em;
}
html, body, #root {
width: 100%;
height: 100%;
position: absolute;
background-color: #222;
margin: 0;
padding: 0;
}
body {
font-family: "Input Sans Compressed", sans-serif;
}
.leaflet-container {
height: 100%;
/*height: 100px;*/
width: 100%;
background-color: white;
}
h1 {
font-size: 1.25em;
}
+55
View File
@@ -0,0 +1,55 @@
import React from 'react'
import App from './features/App'
// import {createDevTools} from 'redux-devtools'
// import LogMonitor from 'redux-devtools-log-monitor'
// import DockMonitor from 'redux-devtools-dock-monitor'
import {Provider} from 'react-redux'
import reducer from './reducers'
import effects from './effects'
import {create} from './store'
import {Router, Route, browserHistory} from 'react-router'
import {syncHistoryWithStore} from 'react-router-redux'
const initialState = {}
// const DevTools = createDevTools(
// <DockMonitor defaultIsVisible={false}
// toggleVisibilityKey="ctrl-h"
// changePositionKey="ctrl-q"
// >
// <LogMonitor theme="tomorrow" preserveScrollTop={false}/>
// </DockMonitor>
// )
const store = create(reducer, initialState, [
// DevTools.instrument()
], effects())
// const history = syncHistoryWithStore(browserHistory, store)
export default function (marker, data) {
// return (
// <App/>
// )
return (
<Provider store={store}>
<div>
<App
marker={marker}
data={data}
/>
{/*<Router history={history}>*/}
{/*<Route path="/edit" component={App}>*/}
{/*</Route>*/}
{/*</Router>*/}
{/*<DevTools/>*/}
</div>
</Provider>
)
}
+516
View File
@@ -0,0 +1,516 @@
import L from 'leaflet'
/*
Leaflet.pattern, Provides tools to set the backgrounds of vector shapes in Leaflet to be patterns.
https://github.com/teastman/Leaflet.pattern
(c) 2015, Tyler Eastman
*/
(function (window, document) {
/*
* L.Pattern is the base class for fill patterns for leaflet Paths.
*/
L.Pattern = L.Class.extend({
// includes: [L.Mixin.Events],
includes: [L.Evented],
options: {
x: 0,
y: 0,
width: 8,
height: 8,
patternUnits: 'userSpaceOnUse',
patternContentUnits: 'userSpaceOnUse'
// angle: <0 - 360>
// patternTransform: <transform-list>
},
_addShapes: L.Util.falseFn,
_update: L.Util.falseFn,
initialize: function (options) {
this._shapes = {};
L.setOptions(this, options);
},
onAdd: function (map) {
this._map = map.target ? map.target : map;
this._map._initDefRoot();
// Create the DOM Object for the pattern.
this._initDom();
// Any shapes that were added before this was added to the map need to have their onAdd called.
// for (let i of this._shapes) {
// i.onAdd(this);
// }
Object
.values(this._shapes)
.forEach(x => x.onAdd(this))
// Call any children that want to add their own shapes.
this._addShapes();
// Add the DOM Object to the DOM Tree
this._addDom();
this.redraw();
if (this.getEvents) {
this._map.on(this.getEvents(), this);
}
this.fire('add');
this._map.fire('patternadd', {pattern: this});
},
onRemove: function () {
this._removeDom();
},
redraw: function () {
if (this._map) {
this._update();
// for (let i of this._shapes) {
// i.redraw();
// }
Object
.values(this._shapes)
.forEach(x => x.redraw())
}
return this;
},
setStyle: function (style) {
L.setOptions(this, style);
if (this._map) {
this._updateStyle();
this.redraw();
}
return this;
},
addTo: function (map) {
map.addPattern(this);
return this;
},
remove: function () {
return this.removeFrom(this._map);
},
removeFrom: function (map) {
if (map) {
map.removePattern(this);
}
return this;
}
});
L.Map.addInitHook(function () {
this._patterns = {};
});
L.Map.include({
addPattern: function (pattern) {
var id = L.stamp(pattern);
if (this._patterns[id]) { return pattern; }
this._patterns[id] = pattern;
this.whenReady(pattern.onAdd, pattern);
return this;
},
removePattern: function (pattern) {
var id = L.stamp(pattern);
if (!this._patterns[id]) { return this; }
if (this._loaded) {
pattern.onRemove(this);
}
if (pattern.getEvents) {
this.off(pattern.getEvents(), pattern);
}
delete this._patterns[id];
if (this._loaded) {
this.fire('patternremove', {pattern: pattern});
pattern.fire('remove');
}
pattern._map = null;
return this;
},
hasPattern: function (pattern) {
return !!pattern && (L.stamp(pattern) in this._patterns);
}
});
L.Pattern.SVG_NS = 'http://www.w3.org/2000/svg';
L.Pattern = L.Pattern.extend({
_createElement: function (name) {
return document.createElementNS(L.Pattern.SVG_NS, name);
},
_initDom: function () {
this._dom = this._createElement('pattern');
if (this.options.className) {
L.DomUtil.addClass(this._dom, this.options.className);
}
this._updateStyle();
},
_addDom: function () {
this._map._defRoot.appendChild(this._dom);
},
_removeDom: function () {
L.DomUtil.remove(this._dom);
},
_updateStyle: function () {
var dom = this._dom,
options = this.options;
if (!dom) { return; }
dom.setAttribute('id', L.stamp(this));
dom.setAttribute('x', options.x);
dom.setAttribute('y', options.y);
dom.setAttribute('width', options.width);
dom.setAttribute('height', options.height);
dom.setAttribute('patternUnits', options.patternUnits);
dom.setAttribute('patternContentUnits', options.patternContentUnits);
if (options.patternTransform || options.angle) {
var transform = options.patternTransform ? options.patternTransform + " " : "";
transform += options.angle ? "rotate(" + options.angle + ") " : "";
dom.setAttribute('patternTransform', transform);
}
else {
dom.removeAttribute('patternTransform');
}
// for (let i of this._shapes) {
// i._updateStyle();
// }
Object
.values(this._shapes)
.forEach(x => x._updateStyle())
}
});
L.Map.include({
_initDefRoot: function () {
if (!this._defRoot) {
if (typeof this.getRenderer === 'function') {
var renderer = this.getRenderer(this);
this._defRoot = L.Pattern.prototype._createElement('defs');
renderer._container.appendChild(this._defRoot);
} else {
if (!this._pathRoot) {
this._initPathRoot();
}
this._defRoot = L.Pattern.prototype._createElement('defs');
this._pathRoot.appendChild(this._defRoot);
}
}
}
});
if (L.SVG) {
L.SVG.include({
_superUpdateStyle: L.SVG.prototype._updateStyle,
_updateStyle: function (layer) {
this._superUpdateStyle(layer);
if (layer.options.fill && layer.options.fillPattern) {
layer._path.setAttribute('fill', 'url(#' + L.stamp(layer.options.fillPattern) + ")");
}
}
});
}
else {
L.Path.include({
_superUpdateStyle: L.Path.prototype._updateStyle,
_updateStyle: function () {
this._superUpdateStyle();
if (this.options.fill && this.options.fillPattern) {
this._path.setAttribute('fill', 'url(#' + L.stamp(this.options.fillPattern) + ")");
}
}
});
}
/*
* L.StripePattern is an implementation of Pattern that creates stripes.
*/
L.StripePattern = L.Pattern.extend({
options: {
weight: 4,
spaceWeight: 4,
color: '#000000',
spaceColor: '#ffffff',
opacity: 1.0,
spaceOpacity: 0.0
},
_addShapes: function () {
this._stripe = new L.PatternPath({
stroke: true,
weight: this.options.weight,
color: this.options.color,
opacity: this.options.opacity
});
this._space = new L.PatternPath({
stroke: true,
weight: this.options.spaceWeight,
color: this.options.spaceColor,
opacity: this.options.spaceOpacity
});
this.addShape(this._stripe);
this.addShape(this._space);
this._update();
},
_update: function () {
this._stripe.options.d = 'M0 ' + this._stripe.options.weight / 2 + ' H ' + this.options.width;
this._space.options.d = 'M0 ' + (this._stripe.options.weight + this._space.options.weight / 2) + ' H ' + this.options.width;
},
setStyle: L.Pattern.prototype.setStyle
});
L.stripePattern = function (options) {
return new L.StripePattern(options);
};
/*
* L.PatternShape is the base class that is used to define the shapes in Patterns.
*/
L.PatternShape = L.Class.extend({
options: {
stroke: true,
color: '#3388ff',
weight: 3,
opacity: 1,
lineCap: 'round',
lineJoin: 'round',
// dashArray: null
// dashOffset: null
// fill: false
// fillColor: same as color by default
fillOpacity: 0.2,
fillRule: 'evenodd',
// fillPattern: L.Pattern
},
initialize: function (options) {
L.setOptions(this, options);
},
// Called when the parent Pattern get's added to the map,
// or when added to a Pattern that is already on the map.
onAdd: function (pattern) {
this._pattern = pattern;
if (this._pattern._dom) {
this._initDom(); // This function is implemented by it's children.
this._addDom();
}
},
addTo: function (pattern) {
pattern.addShape(this);
return this;
},
redraw: function () {
if (this._pattern) {
this._updateShape(); // This function is implemented by it's children.
}
return this;
},
setStyle: function (style) {
L.setOptions(this, style);
if (this._pattern) {
this._updateStyle();
}
return this;
},
setShape: function (shape) {
this.options = L.extend({}, this.options, shape);
this._updateShape();
},
});
L.Pattern.include({
addShape: function (shape) {
var id = L.stamp(shape);
if (this._shapes[id]) { return shape; }
this._shapes[id] = shape;
shape.onAdd(this);
}
});
L.PatternShape.SVG_NS = 'http://www.w3.org/2000/svg';
L.PatternShape = L.PatternShape.extend({
_createElement: function (name) {
return document.createElementNS(L.PatternShape.SVG_NS, name);
},
_initDom: L.Util.falseFn,
_updateShape: L.Util.falseFn,
_initDomElement: function (type) {
this._dom = this._createElement(type);
if (this.options.className) {
L.DomUtil.addClass(this._dom, this.options.className);
}
this._updateStyle();
},
_addDom: function () {
this._pattern._dom.appendChild(this._dom);
},
_updateStyle: function () {
var dom = this._dom,
options = this.options;
if (!dom) { return; }
if (options.stroke) {
dom.setAttribute('stroke', options.color);
dom.setAttribute('stroke-opacity', options.opacity);
dom.setAttribute('stroke-width', options.weight);
dom.setAttribute('stroke-linecap', options.lineCap);
dom.setAttribute('stroke-linejoin', options.lineJoin);
if (options.dashArray) {
dom.setAttribute('stroke-dasharray', options.dashArray);
} else {
dom.removeAttribute('stroke-dasharray');
}
if (options.dashOffset) {
dom.setAttribute('stroke-dashoffset', options.dashOffset);
} else {
dom.removeAttribute('stroke-dashoffset');
}
} else {
dom.setAttribute('stroke', 'none');
}
if (options.fill) {
if (options.fillPattern) {
dom.setAttribute('fill', 'url(#' + L.stamp(options.fillPattern) + ")");
}
else {
dom.setAttribute('fill', options.fillColor || options.color);
}
dom.setAttribute('fill-opacity', options.fillOpacity);
dom.setAttribute('fill-rule', options.fillRule || 'evenodd');
} else {
dom.setAttribute('fill', 'none');
}
dom.setAttribute('pointer-events', options.pointerEvents || (options.interactive ? 'visiblePainted' : 'none'));
}
});
/*
* L.PatternPath is the implementation of PatternShape for adding Paths
*/
L.PatternPath = L.PatternShape.extend({
// options: {
// d: <svg path code>
// },
_initDom: function () {
this._initDomElement('path');
},
_updateShape: function () {
if (!this._dom) { return; }
this._dom.setAttribute('d', this.options.d);
}
});
/*
* L.PatternCircle is the implementation of PatternShape for adding Circles
*/
L.PatternCircle = L.PatternShape.extend({
options: {
x: 0,
y: 0,
radius: 0
},
_initDom: function () {
this._initDomElement('circle');
},
_updateShape: function () {
if (!this._dom) { return; }
this._dom.setAttribute('cx', this.options.x);
this._dom.setAttribute('cy', this.options.y);
this._dom.setAttribute('r', this.options.radius);
}
});
/*
* L.PatternRect is the implementation of PatternShape for adding Rectangles
*/
L.PatternRect = L.PatternShape.extend({
options: {
x: 0,
y: 0,
width: 10,
height: 10,
// rx: x radius for rounded corners
// ry: y radius for rounded corners
},
_initDom: function () {
this._initDomElement('rect');
},
_updateShape: function () {
if (!this._dom) { return; }
this._dom.setAttribute('x', this.options.x);
this._dom.setAttribute('y', this.options.y);
this._dom.setAttribute('width', this.options.width);
this._dom.setAttribute('height', this.options.height);
if (this.options.rx) { this._dom.setAttribute('rx', this.options.rx); }
if (this.options.ry) { this._dom.setAttribute('ry', this.options.ry); }
}
});
}(window, document));
+7
View File
@@ -0,0 +1,7 @@
export function actionRequest(type){
return type + '_REQUEST'
}
export function actionFailure(type){
return type + '_FAILURE'
}
+21
View File
@@ -0,0 +1,21 @@
import {mobileDetectionScreenSize} from '../config'
export const isLandscapeMedia = matchMedia('(orientation: landscape)')
function matchMedia(query) {
return window.matchMedia(query)
}
export function isMobile() {
const landscape = isLandscape()
const mobileWidth = landscape
? mobileDetectionScreenSize[0]
: mobileDetectionScreenSize[1]
const isMobileMedia = matchMedia(`(max-width: ${mobileWidth}px)`)
return isMobileMedia.matches
}
export function isLandscape(){
return isLandscapeMedia.matches
}
+107
View File
@@ -0,0 +1,107 @@
/**
* f([x, y, z, ...], [m, n, k, ...]) -> [[x, m], [y, n], [z, k], ...]
* @param list1
* @param list2
*/
export function zip(list1, list2) {
const [small, other] = list1.length <= list2.length
? [list1, list2]
: [list2, list1]
return small.map((s, i) => [s, other[i]])
}
/**
* f(g, [x, y, z, ...], [m, n, k, ...]) -> [[x, m], [y, n], [z, k], ...] based on G
* @param list1
* @param list2
*/
export function zipBy (key, list1, list2) {
const [small, other] = list1.length <= list2.length
? [list1, list2]
: [list2, list1]
return small.map(s => {
const k = key(s)
const o = other.find(i => key(i) === k)
return [s, o]
})
}
export function arraysIsEqual(list1, list2){
if (list1.length !== list2.length) return false
const booleans = zip(list1, list2)
.map((a, b) => a === b)
return all(booleans)
}
export function all(list){
return list
.map(Boolean)
.reduce((acc, i) => acc && i, true)
}
export function any(list){
return list
.map(Boolean)
.reduce((acc, i) => acc || i, false)
}
export function exist(value){
return all([
value !== null,
value !== undefined,
])
}
/**
* f(g) -> m(x) -> g(x) -> x
*
* @param fn
* @returns {function(*)}
*/
export function pass(fn) {
return x => {
fn(x)
return x
}
}
/**
* f(n) -> g(x) -> {[n]: x}
*
* @param name
* @returns {function(*): {}}
*/
export function pack(name) {
//return value => {[name]: value}
return value => ({[name]: value})
}
/**
* f(g, m, ...) -> g . m . ...
*
* @param functions
* @returns {function(*=): *}
*/
export function compose(...functions) {
const fs = functions.reverse()
return (...value) => fs.reduce((acc, fn) => fn(...acc), value)
}
/**
* f(t, g) -> g(x) after t milliseconds
*
* @param ms
* @param callback
* @returns {function(...[*])}
*/
export function debounce(ms, callback) {
let id = 0
return (...args) => {
clearTimeout(id)
id = setTimeout(callback, ms, ...args)
}
}
+7
View File
@@ -0,0 +1,7 @@
import Leaflet from 'leaflet'
export function getBoundsOf(feature) {
return Leaflet
.geoJSON(feature)
.getBounds()
}
+19
View File
@@ -0,0 +1,19 @@
import {actionRequest, actionFailure} from './action'
export default function promiseMiddleware() {
return next => action => {
const {async, type, ...rest} = action
if (!async) return next(action)
next({...rest, type: actionRequest(type)})
return async
.then(res => {
next({...rest, ...res, error: null, type})
return true
})
.catch(error => {
next({...rest, error, type: actionFailure(type)})
return false
})
}
}
+6
View File
@@ -0,0 +1,6 @@
export default function createEffects(effects) {
return store => next => action => {
next(action)
effects.forEach(effect => effect(store.getState(), action, store.dispatch))
}
}
+3
View File
@@ -0,0 +1,3 @@
export function sleep(ms = 0) {
return new Promise(resolve => setTimeout(resolve, ms))
}
+16
View File
@@ -0,0 +1,16 @@
export function getVariable(name, element = document.documentElement) {
const value = getComputedStyle(element).getPropertyValue(name)
return parseValue(value)
}
function parseValue(value) {
const values = /\s*(\d+)(%|px)\s*/.exec(value)
if (values) {
const v = Number(values[1])
// const v = values[1]
const w = values[2]
if (w === '%') return v / 100
return v
}
return value
}
+8
View File
@@ -0,0 +1,8 @@
import {combineReducers} from 'redux'
import {routerReducer} from 'react-router-redux'
// import app from '../features/App/duck'
export default combineReducers({
// app,
routing: routerReducer
})
+15
View File
@@ -0,0 +1,15 @@
import {createStore, applyMiddleware, compose} from 'redux'
// import thunk from 'redux-thunk'
// import async from '../lib/promiseMiddleware'
// import createEffects from '../lib/sideEffectsMiddleware'
// import {routerMiddleware} from 'react-router-redux'
// import {browserHistory} from 'react-router'
export function create(reducer, init, enhancers = [], effects = []) {
// const browser = routerMiddleware(browserHistory)
// const es = createEffects(effects)
// const enhancer = compose(applyMiddleware(browser, async, thunk, es), ...enhancers)
const enhancer = compose(applyMiddleware(), ...enhancers)
return createStore(reducer, init, enhancer)
}
+14 -1
View File
@@ -11,7 +11,7 @@ const isEqualPate = require('../lib/url').isEqualPath
const {compose, any} = require('../lib/common')
const menuModel = require('../models/menu').default
const {viewMain} = require('../config')
const {viewMain, viewEdit} = require('../config')
const defaultOptions = {
commentsEnabled: false,
@@ -105,4 +105,17 @@ async function render(path, data, meta, options = {}) {
})
}
async function renderEdit() {
const renderOptions = {
...defaultOptions,
templateFile: viewEdit,
}
const source = await readFile(renderOptions.templateFile, 'utf-8')
const template = handlebars.compile(source)
return template({})
}
exports.render = render
exports.renderEdit = renderEdit
+14
View File
@@ -0,0 +1,14 @@
// auth - fn that returns boolean
export function ensureServiceAuth(auth) {
return async (ctx, next) => {
let service = ctx.request.headers['x-service-name']
let token = ctx.request.headers['x-service-token']
if (!auth(service, token)) {
ctx.status = 401
} else {
await next()
}
}
}
+10
View File
@@ -0,0 +1,10 @@
const {get} = require('koa-route')
const {renderEdit} = require('../lib/render')
function getEdit() {
return get('/edit', async ctx => {
ctx.body = await renderEdit()
})
}
exports.getEdit = getEdit
+2
View File
@@ -27,6 +27,7 @@ const document = require('./routes/document')
const schedule = require('./routes/schedule')
const pages = require('./routes/pages')
const sitemap = require('./routes/sitemap')
const edit = require('./routes/edit')
const dirPublic = path.join(__dirname, '../public')
@@ -65,6 +66,7 @@ export default function (config) {
app.use(error.notFound(view404))
app.use(sitemap())
app.use(edit.getEdit())
app.use(home.getHome(config.articlesPerPage))
app.use(gallery.getGallery())
app.use(article.getArticles(config.articlesPerPage))
+27
View File
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<base href="/">
<title>Edit Hudozka</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, maximum-scale=1.0"/>
<meta name="referrer" content="unsafe-url">
<link rel="icon" type="image/png" href="//static.shlisselburg.org/art/graphics/favicon-32.png"/>
<link rel="apple-touch-icon" sizes="57x57" href="//static.shlisselburg.org/art/graphics/favicon-57.png"/>
<link rel="apple-touch-icon" sizes="72x72" href="//static.shlisselburg.org/art/graphics/favicon-72.png"/>
<link rel="apple-touch-icon" sizes="114x114" href="//static.shlisselburg.org/art/graphics/favicon-114.png"/>
<link rel="apple-touch-icon" sizes="144x144" href="//static.shlisselburg.org/art/graphics/favicon-144.png"/>
<!--<link href="https://fonts.googleapis.com/css?family=PT+Sans|PT+Serif" rel="stylesheet" as="font">-->
<link rel="stylesheet" href="/style.css">
</head>
<body>
<div id="app"></div>
<script src="edit.js"></script>
</body>
</html>
+65
View File
@@ -0,0 +1,65 @@
import asyncio
# import os
# async def image_magick_resize(input_file, size):
# s = '%dx%d' % size
# name = '{s}-{name}'.format(s=s, name=os.path.basename(input_file))
# output_file = os.path.join('/Users/tmshv/Pyasync/', name)
#
# cmd = [
# '/usr/local/bin/convert', '"%s"' % input_file,
# '-strip',
# '-auto-orient',
# '-resize', s,
# '-quality', '85',
# '"%s"' % output_file,
# ]
#
# cmd = ' '.join(cmd)
# await asyncio.create_subprocess_shell(cmd)
from sync.models.Image import create_image, default_url_factory
async def resize(input_file):
# from utils.image import image_magick_resize
sizes = [
('a', 1880, 1900),
('b', 10, 10),
('c', 5, 5),
]
await create_image(input_file, sizes, default_url_factory, '/Users/tmshv')
# for size in sizes:
# await image_magick_resize(input_file, '/Users/tmshv/AAA-{}.jpg'.format(size), size)
async def run():
from sync.data import request
files = {'file': open('/Users/tmshv/Downloads/39174605.png', 'rb')}
data = {
'file': files
}
# await request.put('https://static.shlisselburg.org/upload/art/uploads/test.jpg', data)
await request.put('http://localhost:8080/upload/art/uploads/test.jpg', data)
# await asyncio.wait([
# resize('/Users/tmshv/Yandex.Disk.localized/Hudozka/Trash/Cats/Photo 26-01-15 18 07 39.jpg'),
#
# # resize('/Users/tmshv/Yandex.Disk.localized/Hudozka/Trash/Cats/Photo 26-01-15 18 07 54.jpg'),
# # resize('/Users/tmshv/Yandex.Disk.localized/Hudozka/Trash/Cats/Photo 26-01-15 18 08 14.jpg'),
# # resize('/Users/tmshv/Yandex.Disk.localized/Hudozka/Trash/Cats/Photo 26-01-15 18 08 18.jpg'),
# # resize('/Users/tmshv/Yandex.Disk.localized/Hudozka/Trash/Cats/Photo 26-01-15 18 08 30.jpg'),
# # resize('/Users/tmshv/Yandex.Disk.localized/Hudozka/Trash/Cats/Photo 26-01-15 18 09 04.jpg'),
# ])
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(run())
loop.close()
+24
View File
@@ -0,0 +1,24 @@
---
database_uri: 'mongodb://localhost:27017/hudozka'
env: 'development'
interval: 0
origin: 'Yandex.Disk'
skip_unchanged: False
image_processing: True
upload_enabled: True
upload_auth:
login: 'uploader'
password: '1207690962638465'
provider:
name: 'yd'
root: '/Hudozka/Site'
yandex_disk_access_token: 'AQAAAAABbOfKAAFEbvckxZKiR01JsTcDniJsi2A'
update_enabled: True
delete_enabled: False
+31
View File
@@ -0,0 +1,31 @@
---
database_uri: 'mongodb://hudozka:h123@localhost:1817/hudozka?authSource=admin'
database_uri2: 'mongodb://localhost:27017/hudozka'
env: 'development'
interval: 0
origin: 'Yandex.Disk'
skip_unchanged: False
image_processing: False
upload_enabled: False
upload_auth:
login: 'uploader'
password: '1207690962638465'
provider:
name: 'fs'
root: '/Users/tmshv/Yandex.Disk.localized/Hudozka/Site'
s3:
host: '185.185.68.103:2090'
bucket: 'art'
access_key: 'CUKJ1K1557N6YJEF3F6K'
secret_key: 'WCx52kadWYB3yGAvpPvi+JfzcZkEAQY2P0nelpHq'
yandex_disk_access_token: 'AQAAAAABbOfKAAFEbvckxZKiR01JsTcDniJsi2A'
update_enabled: True
delete_enabled: True
+21
View File
@@ -0,0 +1,21 @@
---
database_uri: 'mongodb://hudozka:R6juwUwx4wz7WYKjQufz@art.shlisselburg.org:1817/hudozka?authSource=admin'
env: 'development'
skip_unchanged: False
image_processing: True
upload_enabled: True
origin: 'Yandex.Disk'
upload_auth:
login: 'uploader'
password: '1207690962638465'
provider:
name: 'fs'
root: '/Users/tmshv/Yandex.Disk.localized/Hudozka/Site'
yandex_disk_access_token: 'AQAAAAABbOfKAAFEblSYy3Q2jkEauWzv6ttyfXM'
update_enabled: True
delete_enabled: True
+99651
View File
File diff suppressed because it is too large. Load diff
+1
View File
@@ -0,0 +1 @@
python3 sync.py --config config-production.yaml
+4 -6
View File
@@ -116,11 +116,11 @@ collection_albums = 'albums'
collection_pages = 'pages'
hash_salt_articles = '5'
hash_salt_pages = '2'
hash_salt_pages = '3'
hash_salt_albums = '0'
hash_salt_documents = '1'
hash_salt_images = '1'
hash_salt_person = '2'
hash_salt_images = '2'
hash_salt_person = '3'
provider_name = value('provider')['name'] # env('SYNC_PROVIDER', 'fs')
provider_root = value('provider')['root'] # env('SYNC_PROVIDER', 'fs')
@@ -141,9 +141,7 @@ document_url_upload_template = 'https://static.shlisselburg.org/upload/art/uploa
document_url_preview_template = 'https://static.shlisselburg.org/art/images/{id}-{size}{ext}'
image_url_upload = 'https://static.shlisselburg.org/upload/art/images/'
image_url_base = 'https://static.shlisselburg.org/art/images/'
person_image_url_template = 'https://static.shlisselburg.org/art/images/person-{id}-{size}{ext}'
image_url_base = 'https://art.shlisselburg.org/data/images/'
yandex_disk_access_token = value('yandex_disk_access_token') # env('YANDEX_DISK_ACCESS_TOKEN')
+50
View File
@@ -0,0 +1,50 @@
import settings
from sync.data.fs import FSProvider
from sync.data.yandexdisk import YDProvider
import asyncio
async def run_check(run_interval=0):
run_interval = 1
from yandexdisk import YDClient
client = YDClient(settings.yandex_disk_access_token)
async def need_to_update():
res = client.last_uploaded()
items = res['items']
paths = [i['path'] for i in items]
for i in paths:
if i.startswith('disk:/Hudozka/Site'):
return True
return False
while True:
status = await need_to_update()
if status:
print('DO SYNC')
# await run(0)
if run_interval == 0:
break
else:
await asyncio.sleep(run_interval)
def get_provider(provider_type, root):
if provider_type == 'fs':
return FSProvider(root)
elif provider_type == 'yd':
return YDProvider(root, settings.yandex_disk_access_token)
return None
if __name__ == '__main__':
interval = settings.interval
loop = asyncio.get_event_loop()
loop.run_until_complete(run_check(interval))
loop.close()
+5 -5
View File
@@ -96,14 +96,14 @@ async def run(run_interval=0):
while True:
await asyncio.wait([
run_sync(sync_documents),
# run_sync(sync_documents),
run_sync(sync_pages),
run_sync(sync_persons),
run_sync(sync_articles),
run_sync(sync_schedules),
run_sync(sync_albums),
run_sync(sync_settings),
run_sync(sync_images),
# run_sync(sync_schedules),
# run_sync(sync_albums),
# run_sync(sync_settings),
# run_sync(sync_images),
])
if run_interval == 0:
+12 -3
View File
@@ -39,6 +39,15 @@ async def post(url, data, params={}, headers={}):
async def put(url, data):
async with aiohttp.ClientSession as session:
res = session.put(url, data=data)
return await res.text()
# login, password = settings.upload_auth
login, password = ('uploader', '207690962638465')
auth = aiohttp.BasicAuth(login, password, encoding='utf-8')
async with aiohttp.request('put', url=url, data=data, auth=auth) as resp:
print(resp.status)
print(await resp.text())
# async with aiohttp.ClientSession(auth=auth) as session:
# res = session.put(url, data=data)
# return await res.text()
+44
View File
@@ -16,6 +16,30 @@ class YDClient:
Authorization='OAuth %s' % self.access_token,
)
async def get_flat(self, limit=20):
"""
https://cloud-api.yandex.net/v1/disk/resources/files?
[ limit=<количество файлов в списке>]
[& media_type=<тип запрашиваемых файлов>]
[& offset=<смещение относительно начала списка>]
[& fields=<нужные ключи ответа>]
[& preview_size=<размер превью>]
[& preview_crop=<признак обрезки превью>]
:return:
"""
url = '{0}/resources/files'.format(self.base_url)
payload = dict(
limit=limit,
offset=0,
)
r = requests.get(url, headers=self.base_headers, params=payload)
self.check_code(r)
json_dict = r.json()
# return Directory(**json_dict)
return json_dict
def get_disk_metadata(self):
"""
:return: disk metadata
@@ -31,6 +55,26 @@ class YDClient:
system_folders=disk['system_folders']
)
def last_uploaded(self):
"""
https://cloud-api.yandex.net/v1/disk/resources/last-uploaded?
[ limit=<количество файлов в списке>]
[& media_type=<тип запрашиваемых файлов>]
[& fields=<нужные ключи ответа>]
[& preview_size=<размер превью>]
[& preview_crop=<признак обрезки превью>]
"""
url = '{0}/resources/last-uploaded'.format(self.base_url)
payload = dict(
limit=100,
)
r = requests.get(url, headers=self.base_headers, params=payload)
self.check_code(r)
json_dict = r.json()
# return Directory(**json_dict)
return json_dict
def get_content(self, path):
"""
:param path: path to folder
+13
View File
@@ -6,6 +6,7 @@ const targetDir = path.resolve(__dirname, 'public')
module.exports = {
entry: {
app: './src/public/app.js',
edit: './src/edit.entry.js',
},
output: {
path: targetDir,
@@ -56,11 +57,23 @@ module.exports = {
},
},
]
},
{
test: /\.css/,
use: [
'style-loader',
'css-loader',
]
}
],
},
plugins: [
// new HtmlWebpackPlugin({
// filename: 'edit.html',
// template: './src/views/edit.html',
// })
]
}