Stephan Bösch-Plepelits
2 years ago
31 changed files with 1493 additions and 343 deletions
-
14README.md
-
3ajax.php
-
8conf.php-dist
-
37customCategory.php
-
5doc/Tutorial.md
-
3doc/TwigJS.md
-
12init.sql
-
10lang/en.json
-
3modulekit.php
-
9package-lock.json
-
1package.json
-
20repo.php
-
53src/Browser.js
-
412src/OpenStreetBrowserLoader.js
-
86src/Repository.js
-
8src/RepositoryDir.php
-
2src/RepositoryGit.php
-
97src/Window.js
-
232src/addCategories.js
-
464src/customCategory.js
-
99src/customCategory.php
-
9src/database.php
-
7src/domSort.js
-
2src/index.js
-
35src/moreCategories.js
-
4src/options.php
-
40src/repositories.php
-
44src/repositoriesGitea.php
-
4src/tagTranslations.js
-
34src/twigFunctions.js
-
79style.css
@ -0,0 +1,37 @@ |
|||||
|
<?php include "conf.php"; /* load a local configuration */ ?>
|
||||
|
<?php session_start(); ?>
|
||||
|
<?php require 'vendor/autoload.php'; /* composer includes */ ?>
|
||||
|
<?php include "modulekit/loader.php"; /* loads all php-includes */ ?>
|
||||
|
<?php call_hooks("ajax_start"); /* initialize submodules */ ?>
|
||||
|
<?php |
||||
|
if (!isset($db)) { |
||||
|
exit(0); |
||||
|
} |
||||
|
|
||||
|
if (isset($_REQUEST['action']) && $_REQUEST['action'] === 'list') { |
||||
|
$result = $customCategoryRepository->list($_REQUEST); |
||||
|
|
||||
|
Header("Content-Type: application/json; charset=utf-8"); |
||||
|
print json_readable_encode($result); |
||||
|
} |
||||
|
|
||||
|
if (isset($_REQUEST['id'])) { |
||||
|
$category = $customCategoryRepository->getCategory($_REQUEST['id']); |
||||
|
if ($category) { |
||||
|
$customCategoryRepository->recordAccess($_REQUEST['id']); |
||||
|
} |
||||
|
|
||||
|
Header("Content-Type: application/yaml; charset=utf-8"); |
||||
|
Header("Content-Disposition: inline; filename=\"{$_REQUEST['id']}.yaml\""); |
||||
|
print $category; |
||||
|
} |
||||
|
|
||||
|
if (isset($_REQUEST['action']) && $_REQUEST['action'] === 'save') { |
||||
|
$content = file_get_contents("php://input"); |
||||
|
|
||||
|
$id = $customCategoryRepository->saveCategory($content); |
||||
|
$customCategoryRepository->recordAccess($id); |
||||
|
|
||||
|
Header("Content-Type: text/plain; charset=utf-8"); |
||||
|
print $id; |
||||
|
} |
@ -0,0 +1,12 @@ |
|||||
|
create table customCategory ( |
||||
|
id char(32) not null, |
||||
|
content mediumtext not null, |
||||
|
created datetime not null default CURRENT_TIMESTAMP, |
||||
|
primary key(id) |
||||
|
); |
||||
|
|
||||
|
create table customCategoryAccess ( |
||||
|
id char(32) not null, |
||||
|
ts datetime not null default CURRENT_TIMESTAMP, |
||||
|
foreign key(id) references customCategory(id) on delete cascade |
||||
|
); |
@ -0,0 +1,53 @@ |
|||||
|
const EventEmitter = require('events') |
||||
|
const queryString = require('query-string') |
||||
|
|
||||
|
const domSort = require('./domSort') |
||||
|
|
||||
|
module.exports = class Browser extends EventEmitter { |
||||
|
constructor (id, dom) { |
||||
|
super() |
||||
|
|
||||
|
this.id = id |
||||
|
this.dom = dom |
||||
|
this.history = [] |
||||
|
} |
||||
|
|
||||
|
buildPage (parameters) { |
||||
|
this.clear() |
||||
|
|
||||
|
hooks.call('browser-' + this.id, this, parameters) |
||||
|
this.emit('buildPage', parameters) |
||||
|
this.parameters = parameters |
||||
|
|
||||
|
domSort(this.dom) |
||||
|
} |
||||
|
|
||||
|
clear () { |
||||
|
while (this.dom.lastChild) { |
||||
|
this.dom.removeChild(this.dom.lastChild) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
catchLinks () { |
||||
|
const links = this.dom.getElementsByTagName('a') |
||||
|
Array.from(links).forEach(link => { |
||||
|
const href = link.getAttribute('href') |
||||
|
|
||||
|
if (href && href.substr(0, this.id.length + 2) === '#' + this.id + '?') { |
||||
|
link.onclick = () => { |
||||
|
this.history.push(this.parameters) |
||||
|
|
||||
|
const parameters = queryString.parse(href.substr(this.id.length + 2)) |
||||
|
this.buildPage(parameters) |
||||
|
|
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
close () { |
||||
|
this.clear() |
||||
|
this.emit('close') |
||||
|
} |
||||
|
} |
@ -1,8 +1,90 @@ |
|||||
module.exports = class Repository { |
module.exports = class Repository { |
||||
constructor (id, data) { |
constructor (id, data) { |
||||
this.id = id |
this.id = id |
||||
this.data = data |
|
||||
|
this.isLoaded = false |
||||
|
|
||||
this.lang = this.data.lang || {} |
|
||||
|
if (data) { |
||||
|
this.data = data |
||||
|
this.lang = this.data.lang || {} |
||||
|
this.loadCallbacks = null |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
file_get_contents (fileName, options, callback) { |
||||
|
let param = [] |
||||
|
param.push('repo=' + encodeURIComponent(this.id)) |
||||
|
param.push('file=' + encodeURIComponent(fileName)) |
||||
|
param.push(config.categoriesRev) |
||||
|
param = param.length ? '?' + param.join('&') : '' |
||||
|
|
||||
|
fetch('repo.php' + param) |
||||
|
.then(res => res.text()) |
||||
|
.then(data => { |
||||
|
global.setTimeout(() => { |
||||
|
callback(null, data) |
||||
|
}, 0) |
||||
|
}) |
||||
|
.catch(err => { |
||||
|
global.setTimeout(() => { |
||||
|
callback(err) |
||||
|
}, 0) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
load (callback) { |
||||
|
if (this.loadCallbacks) { |
||||
|
return this.loadCallbacks.push(callback) |
||||
|
} |
||||
|
|
||||
|
this.loadCallbacks = [callback] |
||||
|
|
||||
|
var param = [] |
||||
|
|
||||
|
param.push('repo=' + encodeURIComponent(this.id)) |
||||
|
param.push('lang=' + encodeURIComponent(ui_lang)) |
||||
|
param.push(config.categoriesRev) |
||||
|
param = param.length ? '?' + param.join('&') : '' |
||||
|
|
||||
|
fetch('repo.php' + param) |
||||
|
.then(res => res.json()) |
||||
|
.then(data => { |
||||
|
this.data = data |
||||
|
this.lang = this.data.lang || {} |
||||
|
this.err = null |
||||
|
|
||||
|
global.setTimeout(() => { |
||||
|
const cbs = this.loadCallbacks |
||||
|
this.loadCallbacks = null |
||||
|
cbs.forEach(cb => cb(null)) |
||||
|
}, 0) |
||||
|
}) |
||||
|
.catch(err => { |
||||
|
this.err = err |
||||
|
global.setTimeout(() => { |
||||
|
const cbs = this.loadCallbacks |
||||
|
this.loadCallbacks = null |
||||
|
cbs.forEach(cb => cb(err)) |
||||
|
}, 0) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
clearCache () { |
||||
|
this.data = null |
||||
|
} |
||||
|
|
||||
|
getCategory (id, options, callback) { |
||||
|
if (!(id in this.data.categories)) { |
||||
|
return callback(new Error('Repository ' + this.id + ': Category "' + id + '" not defined'), null) |
||||
|
} |
||||
|
|
||||
|
callback(null, this.data.categories[id]) |
||||
|
} |
||||
|
|
||||
|
getTemplate (id, options, callback) { |
||||
|
if (!(id in this.data.templates)) { |
||||
|
return callback(new Error('Repository ' + this.id + ': Template "' + id + '" not defined'), null) |
||||
|
} |
||||
|
|
||||
|
callback(null, this.data.templates[id]) |
||||
} |
} |
||||
} |
} |
@ -0,0 +1,97 @@ |
|||||
|
const EventEmitter = require('events') |
||||
|
|
||||
|
module.exports = class Window extends EventEmitter { |
||||
|
constructor (options) { |
||||
|
super() |
||||
|
|
||||
|
this.visible = false |
||||
|
this.dom = document.createElement('div') |
||||
|
this.dom.className = 'Window' |
||||
|
|
||||
|
this.header = document.createElement('div') |
||||
|
this.header.className = 'header' |
||||
|
this.header.innerHTML = options.title |
||||
|
this.dom.appendChild(this.header) |
||||
|
|
||||
|
this.closeBtn = document.createElement('div') |
||||
|
this.closeBtn.className = 'closeBtn' |
||||
|
this.closeBtn.title = lang('close') |
||||
|
this.closeBtn.onclick = (e) => { |
||||
|
this.close() |
||||
|
e.stopImmediatePropagation() |
||||
|
} |
||||
|
this.header.appendChild(this.closeBtn) |
||||
|
|
||||
|
this.content = document.createElement('div') |
||||
|
this.content.className = 'content' |
||||
|
this.dom.appendChild(this.content) |
||||
|
|
||||
|
dragElement(this.dom) |
||||
|
|
||||
|
this.dom.onclick = () => { |
||||
|
if (!this.visible) { return } |
||||
|
|
||||
|
const activeEl = document.activeElement |
||||
|
|
||||
|
if (document.body.lastElementChild !== this.dom) { |
||||
|
document.body.appendChild(this.dom) |
||||
|
activeEl.focus() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
show () { |
||||
|
this.visible = true |
||||
|
document.body.appendChild(this.dom) |
||||
|
this.emit('show') |
||||
|
} |
||||
|
|
||||
|
close () { |
||||
|
this.visible = false |
||||
|
document.body.removeChild(this.dom) |
||||
|
this.emit('close') |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// copied from https://www.w3schools.com/HOWTO/howto_js_draggable.asp
|
||||
|
// Make the DIV element draggable:
|
||||
|
function dragElement(elmnt) { |
||||
|
var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; |
||||
|
if (elmnt.firstChild) { |
||||
|
// if present, the header is where you move the DIV from:
|
||||
|
elmnt.firstChild.onmousedown = dragMouseDown; |
||||
|
} else { |
||||
|
// otherwise, move the DIV from anywhere inside the DIV:
|
||||
|
elmnt.onmousedown = dragMouseDown; |
||||
|
} |
||||
|
|
||||
|
function dragMouseDown(e) { |
||||
|
e = e || window.event; |
||||
|
e.preventDefault(); |
||||
|
// get the mouse cursor position at startup:
|
||||
|
pos3 = e.clientX; |
||||
|
pos4 = e.clientY; |
||||
|
document.onmouseup = closeDragElement; |
||||
|
// call a function whenever the cursor moves:
|
||||
|
document.onmousemove = elementDrag; |
||||
|
} |
||||
|
|
||||
|
function elementDrag(e) { |
||||
|
e = e || window.event; |
||||
|
e.preventDefault(); |
||||
|
// calculate the new cursor position:
|
||||
|
pos1 = pos3 - e.clientX; |
||||
|
pos2 = pos4 - e.clientY; |
||||
|
pos3 = e.clientX; |
||||
|
pos4 = e.clientY; |
||||
|
// set the element's new position:
|
||||
|
elmnt.style.top = (elmnt.offsetTop - pos2) + "px"; |
||||
|
elmnt.style.left = (elmnt.offsetLeft - pos1) + "px"; |
||||
|
} |
||||
|
|
||||
|
function closeDragElement() { |
||||
|
// stop moving when mouse button is released:
|
||||
|
document.onmouseup = null; |
||||
|
document.onmousemove = null; |
||||
|
} |
||||
|
} |
@ -0,0 +1,464 @@ |
|||||
|
const tabs = require('modulekit-tabs') |
||||
|
const yaml = require('js-yaml') |
||||
|
const md5 = require('md5') |
||||
|
const OverpassLayer = require('overpass-layer') |
||||
|
const jsonMultilineStrings = require('json-multiline-strings') |
||||
|
|
||||
|
const Window = require('./Window') |
||||
|
const OpenStreetBrowserLoader = require('./OpenStreetBrowserLoader') |
||||
|
|
||||
|
const editors = [] |
||||
|
|
||||
|
class CustomCategoryRepository { |
||||
|
constructor () { |
||||
|
this.clearCache() |
||||
|
} |
||||
|
|
||||
|
load (callback) { |
||||
|
callback(null) |
||||
|
} |
||||
|
|
||||
|
clearCache () { |
||||
|
this.cache = {} |
||||
|
} |
||||
|
|
||||
|
listCategories (options, callback) { |
||||
|
fetch('customCategory.php?action=list') |
||||
|
.then(res => res.json()) |
||||
|
.then(result => callback(null, result)) |
||||
|
} |
||||
|
|
||||
|
getCategory (id, options, callback) { |
||||
|
if (id in this.cache) { |
||||
|
const data = this.parseCategory(id, this.cache[id]) |
||||
|
return callback(null, data, this.cache[id]) |
||||
|
} |
||||
|
|
||||
|
fetch('customCategory.php?id=' + id) |
||||
|
.then(res => res.text()) |
||||
|
.then(content => { |
||||
|
this.cache[id] = content |
||||
|
|
||||
|
const data = this.parseCategory(id, content) |
||||
|
|
||||
|
callback(null, data, content) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
parseCategory (id, content) { |
||||
|
let data |
||||
|
|
||||
|
try { |
||||
|
data = yaml.load(content) |
||||
|
} |
||||
|
catch (e) { |
||||
|
return global.alert(e) |
||||
|
} |
||||
|
|
||||
|
if (data && typeof data !== 'object') { |
||||
|
callback(new Error('Data can not be parsed into an object')) |
||||
|
} |
||||
|
|
||||
|
if (!data.name) { |
||||
|
data.name = 'Custom ' + id.substr(0, 6) |
||||
|
} |
||||
|
|
||||
|
return data |
||||
|
} |
||||
|
|
||||
|
saveCategory (body, options, callback) { |
||||
|
const id = md5(body) |
||||
|
this.cache[id] = body |
||||
|
|
||||
|
fetch('customCategory.php?action=save', { |
||||
|
method: 'POST', |
||||
|
body |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
getTemplate (id, options, callback) { |
||||
|
callback(null, '') |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class CustomCategoryEditor { |
||||
|
constructor (repository) { |
||||
|
this.repository = repository |
||||
|
editors.push(this) |
||||
|
} |
||||
|
|
||||
|
load (id, callback) { |
||||
|
this.repository.getCategory(id, {}, |
||||
|
(err, category, content) => { |
||||
|
this.content = content |
||||
|
callback(err, content) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
edit () { |
||||
|
if (this.window) { |
||||
|
this.window.focused = true |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
this.window = new Window({ |
||||
|
title: 'Custom Category' |
||||
|
}) |
||||
|
|
||||
|
this.window.on('close', () => { |
||||
|
this.window = null |
||||
|
}) |
||||
|
|
||||
|
this.textarea = document.createElement('textarea') |
||||
|
this.textarea.spellcheck = false |
||||
|
this.window.content.appendChild(this.textarea) |
||||
|
if (this.content !== undefined) { |
||||
|
this.textarea.value = this.content |
||||
|
} |
||||
|
|
||||
|
const controls = document.createElement('div') |
||||
|
controls.className = 'controls' |
||||
|
this.window.content.appendChild(controls) |
||||
|
|
||||
|
const input = document.createElement('input') |
||||
|
input.type = 'submit' |
||||
|
input.value = lang('apply-keep') |
||||
|
controls.appendChild(input) |
||||
|
|
||||
|
const inputClose = document.createElement('input') |
||||
|
inputClose.type = 'button' |
||||
|
inputClose.value = lang('apply-close') |
||||
|
inputClose.onclick = () => { |
||||
|
this.submit() |
||||
|
this.window.close() |
||||
|
} |
||||
|
controls.appendChild(inputClose) |
||||
|
|
||||
|
const icons = document.createElement('div') |
||||
|
icons.className = 'actions' |
||||
|
controls.appendChild(icons) |
||||
|
|
||||
|
this.inputDownload = document.createElement('a') |
||||
|
this.textarea.onchange = () => this.updateDownload() |
||||
|
this.updateDownload() |
||||
|
this.inputDownload.title = lang('download') |
||||
|
this.inputDownload.innerHTML = '<i class="fas fa-download"></i>' |
||||
|
icons.appendChild(this.inputDownload) |
||||
|
|
||||
|
const tutorial = document.createElement('span') |
||||
|
tutorial.className = 'tip-tutorial' |
||||
|
let text = lang('tip-tutorial') |
||||
|
text = text.replace('[', '<a target="_blank" href="https://github.com/plepe/OpenStreetBrowser/blob/master/doc/Tutorial.md">') |
||||
|
text = text.replace(']', '</a>') |
||||
|
tutorial.innerHTML = text |
||||
|
controls.appendChild(tutorial) |
||||
|
|
||||
|
input.onclick = (e) => { |
||||
|
this.submit() |
||||
|
e.preventDefault() |
||||
|
} |
||||
|
|
||||
|
this.window.show() |
||||
|
} |
||||
|
|
||||
|
submit () { |
||||
|
const err = customCategoryTest(this.textarea.value) |
||||
|
if (err) { |
||||
|
return global.alert(err) |
||||
|
} |
||||
|
|
||||
|
this.applyContent(this.textarea.value) |
||||
|
} |
||||
|
|
||||
|
applyContent (content) { |
||||
|
this.content = content |
||||
|
this.repository.saveCategory(this.content, {}, () => {}) |
||||
|
|
||||
|
if (this.textarea) { |
||||
|
this.textarea.value = content |
||||
|
this.updateDownload() |
||||
|
} |
||||
|
|
||||
|
const id = md5(content) |
||||
|
this.id = id |
||||
|
|
||||
|
if (this.category) { |
||||
|
this.category.remove() |
||||
|
this.category = null |
||||
|
} |
||||
|
|
||||
|
OpenStreetBrowserLoader.getCategory('custom/' + id, {}, (err, category) => { |
||||
|
if (err) { |
||||
|
return global.alert(err) |
||||
|
} |
||||
|
|
||||
|
this.category = category |
||||
|
this.category.setParentDom(document.getElementById('contentListAddCategories')) |
||||
|
|
||||
|
this.category.open() |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
updateDownload () { |
||||
|
const file = new Blob([this.textarea.value], { type: 'application/yaml' }) |
||||
|
this.inputDownload.href = URL.createObjectURL(file) |
||||
|
this.inputDownload.download = md5(this.textarea.value) + '.yaml' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
function editCustomCategory (id, category) { |
||||
|
let done = editors.filter(editor => { |
||||
|
if (editor.id === id) { |
||||
|
editor.edit() |
||||
|
return true |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
if (!done.length) { |
||||
|
const editor = new CustomCategoryEditor(repository) |
||||
|
editor.load(id, (err) => { |
||||
|
if (err) { return global.alert(err) } |
||||
|
editor.category = category |
||||
|
editor.edit() |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
hooks.register('browser-more-categories', (browser, parameters) => { |
||||
|
const content = browser.dom |
||||
|
|
||||
|
if (!Object.keys(parameters).length) { |
||||
|
let block = document.createElement('div') |
||||
|
block.setAttribute('weight', 0) |
||||
|
content.appendChild(block) |
||||
|
|
||||
|
let header = document.createElement('h4') |
||||
|
header.innerHTML = lang('customCategory:header') |
||||
|
block.appendChild(header) |
||||
|
|
||||
|
let ul = document.createElement('ul') |
||||
|
|
||||
|
let li = document.createElement('li') |
||||
|
let a = document.createElement('a') |
||||
|
a.innerHTML = lang('customCategory:create') |
||||
|
a.href = '#' |
||||
|
a.onclick = (e) => { |
||||
|
const editor = new CustomCategoryEditor(repository) |
||||
|
editor.edit() |
||||
|
browser.close() |
||||
|
e.preventDefault() |
||||
|
} |
||||
|
li.appendChild(a) |
||||
|
ul.appendChild(li) |
||||
|
|
||||
|
li = document.createElement('li') |
||||
|
a = document.createElement('a') |
||||
|
a.innerHTML = lang('customCategory:list') |
||||
|
a.href = '#more-categories?custom=list' |
||||
|
li.appendChild(a) |
||||
|
ul.appendChild(li) |
||||
|
|
||||
|
block.appendChild(ul) |
||||
|
browser.catchLinks() |
||||
|
} |
||||
|
else if (parameters.custom === 'list') { |
||||
|
customCategoriesList(browser, parameters) |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
function customCategoriesList (browser, options) { |
||||
|
browser.dom.innerHTML = '<i class="fa fa-spinner fa-pulse fa-fw"></i> ' + lang('loading') |
||||
|
|
||||
|
repository.listCategories({}, |
||||
|
(err, result) => { |
||||
|
browser.dom.innerHTML = '' |
||||
|
|
||||
|
const ul = document.createElement('ul') |
||||
|
browser.dom.appendChild(ul) |
||||
|
|
||||
|
result.forEach(cat => { |
||||
|
const li = document.createElement('li') |
||||
|
|
||||
|
const a = document.createElement('a') |
||||
|
a.href = '#categories=custom/' + cat.id |
||||
|
a.appendChild(document.createTextNode(cat.name)) |
||||
|
li.appendChild(a) |
||||
|
|
||||
|
const edit = document.createElement('a') |
||||
|
edit.onclick = (e) => { |
||||
|
editCustomCategory(cat.id) |
||||
|
e.preventDefault() |
||||
|
} |
||||
|
edit.innerHTML = ' <i class="fa fa-pen"></i>' |
||||
|
li.appendChild(edit) |
||||
|
|
||||
|
ul.appendChild(li) |
||||
|
}) |
||||
|
|
||||
|
browser.catchLinks() |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
const repository = new CustomCategoryRepository() |
||||
|
hooks.register('init', () => { |
||||
|
OpenStreetBrowserLoader.registerRepository('custom', repository) |
||||
|
}) |
||||
|
|
||||
|
hooks.register('category-overpass-init', (category) => { |
||||
|
const m = category.id.match(/^custom\/(.*)$/) |
||||
|
if (m) { |
||||
|
const id = m[1] |
||||
|
|
||||
|
if (category.tabEdit) { |
||||
|
category.tools.remove(this.category.tabEdit) |
||||
|
} |
||||
|
|
||||
|
category.tabEdit = new tabs.Tab({ |
||||
|
id: 'edit', |
||||
|
weight: 9 |
||||
|
}) |
||||
|
category.tools.add(category.tabEdit) |
||||
|
category.tabEdit.header.innerHTML = '<i class="fa fa-pen"></i>' |
||||
|
category.tabEdit.header.title = lang('edit') |
||||
|
category.tabEdit.on('select', () => { |
||||
|
category.tabEdit.unselect() |
||||
|
editCustomCategory(id, category) |
||||
|
}) |
||||
|
|
||||
|
if (!category.tabShare) { |
||||
|
const url = location.origin + location.pathname + '#categories=custom/' + id |
||||
|
|
||||
|
category.tabShare = new tabs.Tab({ |
||||
|
id: 'share', |
||||
|
weight: 10 |
||||
|
}) |
||||
|
category.tools.add(category.tabShare) |
||||
|
category.shareLink = document.createElement('a') |
||||
|
category.shareLink.href = url |
||||
|
category.shareLink.innerHTML = '<i class="fa fa-share-alt"></i>' |
||||
|
|
||||
|
category.tabShare.header.appendChild(category.shareLink) |
||||
|
category.tabShare.header.className = 'share-button' |
||||
|
category.tabShare.on('select', () => { |
||||
|
category.tabShare.unselect() |
||||
|
navigator.clipboard.writeText(url) |
||||
|
|
||||
|
const notify = document.createElement('div') |
||||
|
notify.className = 'notify' |
||||
|
notify.innerHTML = lang('copied-clipboard') |
||||
|
category.tabShare.header.appendChild(notify) |
||||
|
global.setTimeout(() => category.tabShare.header.removeChild(notify), 2000) |
||||
|
}) |
||||
|
} |
||||
|
} else { |
||||
|
if (category.tabClone) { |
||||
|
category.tools.remove(this.category.tabClone) |
||||
|
} |
||||
|
|
||||
|
category.tabClone = new tabs.Tab({ |
||||
|
id: 'clone', |
||||
|
weight: 9 |
||||
|
}) |
||||
|
category.tools.add(category.tabClone) |
||||
|
category.tabClone.header.innerHTML = '<i class="fa fa-clone"></i>' |
||||
|
category.tabClone.header.title = lang('customCategory:clone') |
||||
|
category.tabClone.on('select', () => { |
||||
|
category.tabClone.unselect() |
||||
|
|
||||
|
const clone = new CustomCategoryEditor(repository) |
||||
|
clone.edit() |
||||
|
|
||||
|
category.repository.file_get_contents(category.data.fileName, {}, |
||||
|
(err, content) => { |
||||
|
if (category.data.format === 'json') { |
||||
|
content = JSON.parse(content) |
||||
|
content = jsonMultilineStrings.join(content, { exclude: [ [ 'const' ], [ 'filter' ] ] }) |
||||
|
content = yaml.dump(content, { |
||||
|
lineWidth: 9999 |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
clone.applyContent(content) |
||||
|
category.close() |
||||
|
} |
||||
|
) |
||||
|
}) |
||||
|
|
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
function customCategoryTest (value) { |
||||
|
if (!value) { |
||||
|
return new Error('Empty category') |
||||
|
} |
||||
|
|
||||
|
let data |
||||
|
try { |
||||
|
data = yaml.load(value) |
||||
|
} |
||||
|
catch (e) { |
||||
|
return e |
||||
|
} |
||||
|
|
||||
|
if (!data || typeof data !== 'object') { |
||||
|
return new Error('Data can not be parsed into an object') |
||||
|
} |
||||
|
|
||||
|
const fields = ['feature', 'memberFeature'] |
||||
|
for (let i1 = 0; i1 < fields.length; i1++) { |
||||
|
const k1 = fields[i1] |
||||
|
if (data[k1]) { |
||||
|
for (k2 in data[k1]) { |
||||
|
const err = customCategoryTestCompile(data[k1][k2]) |
||||
|
if (err) { |
||||
|
return new Error('Compiling /' + k1 + '/' + k2 + ': ' + err.message) |
||||
|
} |
||||
|
|
||||
|
if (k2 === 'style' || k2.match(/^style:/)) { |
||||
|
for (const k3 in data[k1][k2]) { |
||||
|
const err = customCategoryTestCompile(data[k1][k2][k3]) |
||||
|
if (err) { |
||||
|
return new Error('Compiling /' + k1 + '/' + k2 + '/' + k3 + ': ' + err.message) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function customCategoryTestCompile (data) { |
||||
|
if (typeof data !== 'string' || data.search('{') === -1) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
let template |
||||
|
try { |
||||
|
template = OverpassLayer.twig.twig({ data }) |
||||
|
} |
||||
|
catch (e) { |
||||
|
return e |
||||
|
} |
||||
|
|
||||
|
const fakeOb = { |
||||
|
id: 'n1', |
||||
|
sublayer_id: 'main', |
||||
|
osm_id: 1, |
||||
|
type: 'node', |
||||
|
tags: { |
||||
|
foo: 'bar' |
||||
|
}, |
||||
|
map: { |
||||
|
zoom: 15, |
||||
|
metersPerPixel: 0.8 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
template.render(fakeOb) |
||||
|
} |
||||
|
catch (e) { |
||||
|
return e |
||||
|
} |
||||
|
} |
@ -0,0 +1,99 @@ |
|||||
|
<?php |
||||
|
class CustomCategoryRepository { |
||||
|
function clearCache () { |
||||
|
} |
||||
|
|
||||
|
function getCategory ($id, $options=[]) { |
||||
|
global $db; |
||||
|
|
||||
|
$stmt = $db->prepare("select content from customCategory where id=:id"); |
||||
|
$stmt->bindValue(':id', $id, PDO::PARAM_STR); |
||||
|
if ($stmt->execute()) { |
||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC); |
||||
|
$result = $row['content']; |
||||
|
$stmt->closeCursor(); |
||||
|
|
||||
|
return $result; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function recordAccess ($id) { |
||||
|
global $db; |
||||
|
|
||||
|
if (!isset($_SESSION['customCategoryAccess'])) { |
||||
|
$_SESSION['customCategoryAccess'] = []; |
||||
|
} |
||||
|
|
||||
|
// update access per session only once a day
|
||||
|
if (array_key_exists($id, $_SESSION['customCategoryAccess']) && $_SESSION['customCategoryAccess'][$id] > time() - 86400) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
$_SESSION['customCategoryAccess'][$id] = time(); |
||||
|
|
||||
|
$stmt = $db->prepare("insert into customCategoryAccess (id) values (:id)"); |
||||
|
$stmt->bindValue(':id', $id); |
||||
|
$stmt->execute(); |
||||
|
} |
||||
|
|
||||
|
function saveCategory ($content) { |
||||
|
global $db; |
||||
|
|
||||
|
$id = md5($content); |
||||
|
|
||||
|
switch ($db->getAttribute(PDO::ATTR_DRIVER_NAME)) { |
||||
|
case 'mysql': |
||||
|
$sqlAction = "insert ignore"; |
||||
|
break; |
||||
|
case 'sqlite': |
||||
|
default: |
||||
|
$sqlAction = "insert or ignore"; |
||||
|
} |
||||
|
|
||||
|
$stmt = $db->prepare("{$sqlAction} into customCategory (id, content) values (:id, :content)"); |
||||
|
$stmt->bindValue(':id', $id, PDO::PARAM_STR); |
||||
|
$stmt->bindValue(':content', $content, PDO::PARAM_STR); |
||||
|
$result = $stmt->execute(); |
||||
|
|
||||
|
return $id; |
||||
|
} |
||||
|
|
||||
|
function list ($options=[]) { |
||||
|
global $db; |
||||
|
|
||||
|
// $sqlCalcAge: the age of the access in days
|
||||
|
switch ($db->getAttribute(PDO::ATTR_DRIVER_NAME)) { |
||||
|
case 'mysql': |
||||
|
$sqlCalcAge = "datediff(now(), ts)"; |
||||
|
break; |
||||
|
case 'sqlite': |
||||
|
$sqlCalcAge = "julianday('now')-julianday(ts)"; |
||||
|
} |
||||
|
|
||||
|
// the popularity column counts every acess with declining value over time,
|
||||
|
// it halves every year.
|
||||
|
$stmt = $db->prepare("select customCategory.id, customCategory.created, customCategory.content, t.accessCount, t.popularity, t.lastAccess from customCategory left join (select id, count(id) accessCount, sum(1/(({$sqlCalcAge})/365.25+1)) popularity, max(ts) lastAccess from customCategoryAccess group by id) t on customCategory.id=t.id order by popularity desc, created desc limit 25"); |
||||
|
$stmt->execute(); |
||||
|
$data = $stmt->fetchAll(PDO::FETCH_ASSOC); |
||||
|
$data = array_map(function ($d) { |
||||
|
$d['popularity'] = (float)$d['popularity']; |
||||
|
$d['accessCount'] = (int)$d['accessCount']; |
||||
|
|
||||
|
$content = yaml_parse($d['content']); |
||||
|
if ($content && is_array($content) && array_key_exists('name', $content)) { |
||||
|
$d['name'] = lang($content['name']); |
||||
|
} |
||||
|
else { |
||||
|
$d['name'] = 'Custom ' . substr($d['id'], 0, 6); |
||||
|
} |
||||
|
|
||||
|
unset($d['content']); |
||||
|
return $d; |
||||
|
}, $data); |
||||
|
|
||||
|
$stmt->closeCursor(); |
||||
|
return $data; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$customCategoryRepository = new CustomCategoryRepository(); |
@ -0,0 +1,9 @@ |
|||||
|
<?php |
||||
|
register_hook('ajax_start', function () { |
||||
|
global $db; |
||||
|
global $db_conf; |
||||
|
|
||||
|
if ($db_conf) { |
||||
|
$db = new PDO($db_conf['dsn'], $db_conf['username'], $db_conf['password']); |
||||
|
} |
||||
|
}); |
@ -0,0 +1,7 @@ |
|||||
|
module.exports = function (dom, attribute='weight') { |
||||
|
const list = Array.from(dom.children).sort( |
||||
|
(a, b) => (a.getAttribute(attribute) || 0) - (b.getAttribute(attribute) || 0) |
||||
|
) |
||||
|
|
||||
|
list.forEach(el => dom.appendChild(el)) |
||||
|
} |
@ -0,0 +1,35 @@ |
|||||
|
const tabs = require('modulekit-tabs') |
||||
|
|
||||
|
const Browser = require('./Browser') |
||||
|
|
||||
|
let tab |
||||
|
|
||||
|
function moreCategoriesIndex () { |
||||
|
let content = tab.content |
||||
|
|
||||
|
content.innerHTML = '<h3>' + lang('more_categories') + '</h3>' |
||||
|
|
||||
|
const dom = document.createElement('div') |
||||
|
content.appendChild(dom) |
||||
|
|
||||
|
const browser = new Browser('more-categories', dom) |
||||
|
browser.buildPage({}) |
||||
|
|
||||
|
browser.on('close', () => tab.unselect()) |
||||
|
} |
||||
|
|
||||
|
register_hook('init', function (callback) { |
||||
|
tab = new tabs.Tab({ |
||||
|
id: 'moreCategories' |
||||
|
}) |
||||
|
global.tabs.add(tab) |
||||
|
|
||||
|
tab.header.innerHTML = '<i class="fa fa-plus" aria-hidden="true"></i>' |
||||
|
tab.header.title = lang('more_categories') |
||||
|
|
||||
|
tab.on('select', () => { |
||||
|
tab.content.innerHTML = '' |
||||
|
moreCategoriesIndex() |
||||
|
}) |
||||
|
}) |
||||
|
|
@ -0,0 +1,44 @@ |
|||||
|
<?php |
||||
|
register_hook("get-repositories", function ($result) { |
||||
|
global $repositoriesGitea; |
||||
|
|
||||
|
if (isset($repositoriesGitea)) { |
||||
|
$d1 = opendir($repositoriesGitea['path']); |
||||
|
while ($f1 = readdir($d1)) { |
||||
|
if (substr($f1, 0, 1) !== '.') { |
||||
|
$d2 = opendir("{$repositoriesGitea['path']}/{$f1}"); |
||||
|
while ($f2 = readdir($d2)) { |
||||
|
if (substr($f2, 0, 1) !== '.') { |
||||
|
$f2id = substr($f2, 0, -4); |
||||
|
|
||||
|
$r = array( |
||||
|
'path' => "{$repositoriesGitea['path']}/{$f1}/{$f2}", |
||||
|
'type' => 'git', |
||||
|
'group' => 'gitea', |
||||
|
); |
||||
|
|
||||
|
if (array_key_exists('url', $repositoriesGitea)) { |
||||
|
$r['repositoryUrl'] = "{$repositoriesGitea['url']}/{{ repositoryId }}"; |
||||
|
$r['categoryUrl'] = "{$repositoriesGitea['url']}/{{ repositoryId }}/src/branch/{{ branchId }}/{{ categoryId }}.{{ categoryFormat }}"; |
||||
|
} |
||||
|
|
||||
|
$result["{$f1}/{$f2id}"] = $r; |
||||
|
} |
||||
|
} |
||||
|
closedir($d2); |
||||
|
} |
||||
|
} |
||||
|
closedir($d1); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
register_hook('init', function () { |
||||
|
global $repositoriesGitea; |
||||
|
|
||||
|
if (isset($repositoriesGitea) && array_key_exists('url', $repositoriesGitea)) { |
||||
|
$d = array('repositoriesGitea' => array( |
||||
|
'url' => $repositoriesGitea['url'], |
||||
|
)); |
||||
|
html_export_var($d); |
||||
|
} |
||||
|
}); |
Write
Preview
Loading…
Cancel
Save
Reference in new issue