Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ce449320c |
No files matched your search
@@ -0,0 +1,2 @@
|
||||
MONGO_HUDOZKA_PASSWORD=h123
|
||||
HUDOZKA_DB_URI=mongodb://hudozka:h123@mongo:27017/hudozka?authSource=admin
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
vk: {
|
||||
appId: '4202466',
|
||||
appSecret: '5NMWIbgb2ImAQeMeODEd'
|
||||
},
|
||||
outputDir: '/Users/tmshv/Yandex.Disk.localized/Hudozka/Site/Events'
|
||||
}
|
||||
@@ -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: '' % 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))
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
id: ${id}
|
||||
date: ${date}
|
||||
---
|
||||
|
||||
${text}
|
||||
|
||||
${attachments}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Generated
+704
-1228
File diff suppressed because it is too large.
Load diff
+13
-3
@@ -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",
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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()
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function effects() {
|
||||
return []
|
||||
}
|
||||
@@ -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)
|
||||
@@ -0,0 +1,9 @@
|
||||
.App {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
&-Body {
|
||||
padding-top: 10px;
|
||||
width: 800px;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;*/
|
||||
/*}*/
|
||||
@@ -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,
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
export {default} from './App'
|
||||
@@ -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}
|
||||
@@ -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.
@@ -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>
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
export {Marker as default} from './Marker'
|
||||
@@ -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],
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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));
|
||||
@@ -0,0 +1,7 @@
|
||||
export function actionRequest(type){
|
||||
return type + '_REQUEST'
|
||||
}
|
||||
|
||||
export function actionFailure(type){
|
||||
return type + '_FAILURE'
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import Leaflet from 'leaflet'
|
||||
|
||||
export function getBoundsOf(feature) {
|
||||
return Leaflet
|
||||
.geoJSON(feature)
|
||||
.getBounds()
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default function createEffects(effects) {
|
||||
return store => next => action => {
|
||||
next(action)
|
||||
effects.forEach(effect => effect(store.getState(), action, store.dispatch))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function sleep(ms = 0) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
@@ -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
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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))
|
||||
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
Executable
+1
@@ -0,0 +1 @@
|
||||
python3 sync.py --config config-production.yaml
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
// })
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user