forked from Public/pics
Compare commits
12 Commits
refactor-t
...
elaborate-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a211e3ae4a | ||
| 7d82a4a924 | |||
| b7a37c85f6 | |||
| 0ec0de4414 | |||
| 69417c36ed | |||
| f2d8a32e67 | |||
| 4863561129 | |||
| 8474d3b2b2 | |||
| 3bf69fd21f | |||
| 70e6001c85 | |||
| 4402521051 | |||
| 889302cd36 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,5 +1,2 @@
|
||||
.DS_Store
|
||||
composer.lock
|
||||
config.php
|
||||
hashru.sublime-project
|
||||
hashru.sublime-workspace
|
||||
composer-setup.php
|
||||
composer.phar
|
||||
|
||||
14
README.md
14
README.md
@@ -13,12 +13,22 @@ The Kabuki codebase requires the following PHP extensions to be enabled for full
|
||||
|
||||
## Setup
|
||||
|
||||
Copy `config.php.dist` to `config.php` and set-up the constants contained in the file.
|
||||
Copy `config.php.dist` to `config.php` and set-up the constants contained in the file. For development, consider starting from `config-dev.php.dist`.
|
||||
|
||||
Ensure you have a MySQL database running with credentials matching your `config.php`. For development, consider the /dev/docker-compose.yml file.
|
||||
|
||||
Run `composer install`. If you do not have composer installed globally, run it from the project directory as follows:
|
||||
|
||||
```
|
||||
wget -O composer-setup.php https://getcomposer.org/installer
|
||||
php composer-setup.php --install-dir=.
|
||||
php ./composer.phar install
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
For development purposes, simply run the `server` script provided in the root of this repository.
|
||||
This will start a PHP development server on `hashru.local:8080`.
|
||||
This will start a PHP development server on `127.0.0.1:8080`.
|
||||
|
||||
For a production environment, please set up a proper PHP-FPM environment instead.
|
||||
|
||||
|
||||
36
config-dev.php.dist
Normal file
36
config-dev.php.dist
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* config.php
|
||||
* Contains general settings for the project.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
const DEBUG = true;
|
||||
const CACHE_ENABLED = true;
|
||||
const CACHE_KEY_PREFIX = 'hashru_';
|
||||
|
||||
// Basedir and base URL of the project.
|
||||
const BASEDIR = __DIR__;
|
||||
const BASEURL = 'http://127.0.0.1:8080'; // no trailing /
|
||||
|
||||
// Reply-To e-mail header address
|
||||
const REPLY_TO_ADDRESS = 'no-reply@my.domain.tld';
|
||||
|
||||
// Assets dir and url, where assets are plentiful. (In wwwroot!)
|
||||
const ASSETSDIR = BASEDIR . '/public/assets';
|
||||
const ASSETSURL = BASEURL . '/assets';
|
||||
|
||||
// Thumbs dir and url, where thumbnails for assets reside.
|
||||
const THUMBSDIR = BASEDIR . '/public/thumbs';
|
||||
const THUMBSURL = BASEURL . '/thumbs';
|
||||
|
||||
// Database server, username, password, name
|
||||
const DB_SERVER = '127.0.0.1';
|
||||
const DB_USER = 'hashru';
|
||||
const DB_PASS = 'hashru';
|
||||
const DB_NAME = 'hashru_pics';
|
||||
const DB_LOG_QUERIES = false;
|
||||
|
||||
const SITE_TITLE = 'HashRU Pics';
|
||||
const SITE_SLOGAN = 'Nijmeegs Nerdclubje';
|
||||
@@ -47,9 +47,13 @@ class ManageErrors extends HTMLController
|
||||
'parse' => [
|
||||
'type' => 'function',
|
||||
'data' => function($row) {
|
||||
return $row['message'] . '<br><div><a onclick="this.parentNode.childNodes[1].style.display=\'block\';this.style.display=\'none\';">Show debug info</a>' .
|
||||
'<pre style="display: none">' . $row['debug_info'] . '</pre></div>' .
|
||||
'<small><a href="' . BASEURL . $row['request_uri'] . '">' . $row['request_uri'] . '</a></small>';
|
||||
return $row['message'] . '<br>' .
|
||||
'<div><a onclick="this.parentNode.childNodes[1].style.display=\'block\';this.style.display=\'none\';">Show debug info</a>' .
|
||||
'<pre style="display: none">' . htmlspecialchars($row['debug_info']) .
|
||||
'</pre></div>' .
|
||||
'<small><a href="' . BASEURL .
|
||||
htmlspecialchars($row['request_uri']) . '">' .
|
||||
htmlspecialchars($row['request_uri']) . '</a></small>';
|
||||
}
|
||||
],
|
||||
'header' => 'Message / URL',
|
||||
@@ -85,7 +89,7 @@ class ManageErrors extends HTMLController
|
||||
'header' => 'UID',
|
||||
'is_sortable' => true,
|
||||
'parse' => [
|
||||
'link' => BASEURL . '/member/?id={ID_USER}',
|
||||
'link' => BASEURL . '/edituser/?id={ID_USER}',
|
||||
'data' => 'id_user',
|
||||
],
|
||||
],
|
||||
|
||||
11
dev/docker-compose.yml
Normal file
11
dev/docker-compose.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
version: '3'
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:latest
|
||||
ports:
|
||||
- 3306:3306
|
||||
environment:
|
||||
MYSQL_USER: 'hashru'
|
||||
MYSQL_PASSWORD: 'hashru'
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
|
||||
MYSQL_DATABASE: 'hashru_pics'
|
||||
@@ -606,14 +606,12 @@ class Asset
|
||||
FROM assets_tags AS t
|
||||
INNER JOIN assets AS a ON a.id_asset = t.id_asset
|
||||
WHERE t.id_tag = {int:id_tag} AND
|
||||
a.date_captured <= {datetime:date_captured} AND
|
||||
a.id_asset != {int:id_asset}
|
||||
ORDER BY a.date_captured DESC'
|
||||
(a.date_captured, a.id_asset) < ({datetime:date_captured}, {int:id_asset})
|
||||
ORDER BY a.date_captured DESC, a.id_asset DESC'
|
||||
: '
|
||||
FROM assets AS a
|
||||
WHERE date_captured >= {datetime:date_captured} AND
|
||||
a.id_asset != {int:id_asset}
|
||||
ORDER BY date_captured ASC')
|
||||
WHERE (a.date_captured, a.id_asset) > ({datetime:date_captured}, {int:id_asset})
|
||||
ORDER BY date_captured ASC, a.id_asset ASC')
|
||||
. '
|
||||
LIMIT 1',
|
||||
[
|
||||
@@ -639,14 +637,12 @@ class Asset
|
||||
FROM assets_tags AS t
|
||||
INNER JOIN assets AS a ON a.id_asset = t.id_asset
|
||||
WHERE t.id_tag = {int:id_tag} AND
|
||||
a.date_captured >= {datetime:date_captured} AND
|
||||
a.id_asset != {int:id_asset}
|
||||
ORDER BY a.date_captured ASC'
|
||||
(a.date_captured, a.id_asset) > ({datetime:date_captured}, {int:id_asset})
|
||||
ORDER BY a.date_captured ASC, a.id_asset ASC'
|
||||
: '
|
||||
FROM assets AS a
|
||||
WHERE date_captured <= {datetime:date_captured} AND
|
||||
a.id_asset != {int:id_asset}
|
||||
ORDER BY date_captured DESC')
|
||||
WHERE (a.date_captured, a.id_asset) < ({datetime:date_captured}, {int:id_asset})
|
||||
ORDER BY date_captured DESC, a.id_asset DESC')
|
||||
. '
|
||||
LIMIT 1',
|
||||
[
|
||||
|
||||
@@ -96,7 +96,9 @@ class EXIF
|
||||
elseif (!empty($exif['Make']))
|
||||
$meta['camera'] = trim($exif['Make']);
|
||||
|
||||
if (!empty($exif['DateTimeDigitized']))
|
||||
if (!empty($exif['DateTimeOriginal']))
|
||||
$meta['created_timestamp'] = self::toUnixTime($exif['DateTimeOriginal']);
|
||||
elseif (!empty($exif['DateTimeDigitized']))
|
||||
$meta['created_timestamp'] = self::toUnixTime($exif['DateTimeDigitized']);
|
||||
|
||||
return new self($meta);
|
||||
|
||||
@@ -228,9 +228,9 @@ class GenericTable
|
||||
// Timestamps get custom treatment.
|
||||
case 'timestamp':
|
||||
if (empty($options['data']['pattern']) || $options['data']['pattern'] === 'long')
|
||||
$pattern = '%F %H:%M';
|
||||
$pattern = 'Y-m-d H:i';
|
||||
elseif ($options['data']['pattern'] === 'short')
|
||||
$pattern = '%F';
|
||||
$pattern = 'Y-m-d';
|
||||
else
|
||||
$pattern = $options['data']['pattern'];
|
||||
|
||||
@@ -242,7 +242,7 @@ class GenericTable
|
||||
if (isset($options['data']['if_null']) && $timestamp == 0)
|
||||
$value = $options['data']['if_null'];
|
||||
else
|
||||
$value = strftime($pattern, $timestamp);
|
||||
$value = date($pattern, $timestamp);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,10 @@ provided that the following conditions are met:
|
||||
|
||||
'use strict';
|
||||
|
||||
function AutoSuggest(opt) {
|
||||
if (typeof opt.inputElement === "undefined" || typeof opt.listElement === "undefined" || typeof opt.baseUrl === "undefined" || typeof opt.appendCallback === "undefined") {
|
||||
class AutoSuggest {
|
||||
constructor(opt) {
|
||||
if (typeof opt.inputElement === "undefined" || typeof opt.listElement === "undefined" ||
|
||||
typeof opt.baseUrl === "undefined" || typeof opt.appendCallback === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -24,53 +26,46 @@ function AutoSuggest(opt) {
|
||||
this.appendCallback = opt.appendCallback;
|
||||
this.baseurl = opt.baseUrl;
|
||||
|
||||
var self = this;
|
||||
this.input.addEventListener('keydown', function(event) {
|
||||
self.doSelection(event);
|
||||
}, false);
|
||||
this.input.addEventListener('keyup', function(event) {
|
||||
self.onType(this, event);
|
||||
}, false);
|
||||
}
|
||||
this.input.addEventListener('keydown', event => this.doSelection(event), false);
|
||||
this.input.addEventListener('keyup', event => this.onType(event), false);
|
||||
}
|
||||
|
||||
AutoSuggest.prototype.doSelection = function(event) {
|
||||
doSelection(event) {
|
||||
if (typeof this.container === "undefined" || this.container.children.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.keyCode) {
|
||||
case 13: // Enter
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
event.preventDefault();
|
||||
this.container.children[this.selectedIndex].click();
|
||||
break;
|
||||
|
||||
case 38: // Arrow up
|
||||
case 40: // Arrow down
|
||||
case 'ArrowUp':
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
this.findSelectedElement().className = '';
|
||||
this.selectedIndex += event.keyCode === 38 ? -1 : 1;
|
||||
this.selectedIndex += event.key === 'ArrowUp' ? -1 : 1;
|
||||
if (this.selectedIndex < 0) {
|
||||
this.selectedIndex = this.container.children.length - 1;
|
||||
} else if (this.selectedIndex === this.container.children.length) {
|
||||
this.selectedIndex = 0;
|
||||
}
|
||||
var new_el = this.findSelectedElement().className = 'selected';
|
||||
let new_el = this.findSelectedElement().className = 'selected';
|
||||
break;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
AutoSuggest.prototype.findSelectedElement = function() {
|
||||
findSelectedElement() {
|
||||
return this.container.children[this.selectedIndex];
|
||||
};
|
||||
};
|
||||
|
||||
AutoSuggest.prototype.onType = function(input, event) {
|
||||
if (event.keyCode === 13 || event.keyCode === 38 || event.keyCode === 40) {
|
||||
onType(event) {
|
||||
if (['Enter', 'ArrowDown', 'ArrowUp'].indexOf(event.key) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var tokens = input.value.split(/\s+/).filter(function(token) {
|
||||
return token.length >= 2;
|
||||
});
|
||||
let tokens = event.target.value.split(/\s+/).filter(token => token.length >= 2);
|
||||
|
||||
if (tokens.length === 0) {
|
||||
if (typeof this.container !== "undefined") {
|
||||
@@ -79,17 +74,17 @@ AutoSuggest.prototype.onType = function(input, event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var request_uri = this.baseurl + '/suggest/?type=tags&data=' + window.encodeURIComponent(tokens.join(" "));
|
||||
var request = new HttpRequest('get', request_uri, {}, this.onReceive, this);
|
||||
};
|
||||
let request_uri = this.baseurl + '/suggest/?type=tags&data=' + window.encodeURIComponent(tokens.join(" "));
|
||||
let request = new HttpRequest('get', request_uri, {}, this.onReceive, this);
|
||||
};
|
||||
|
||||
AutoSuggest.prototype.onReceive = function(response, self) {
|
||||
onReceive(response, self) {
|
||||
self.openContainer();
|
||||
self.clearContainer();
|
||||
self.fillContainer(response);
|
||||
};
|
||||
};
|
||||
|
||||
AutoSuggest.prototype.openContainer = function() {
|
||||
openContainer() {
|
||||
if (this.container) {
|
||||
if (!this.container.parentNode) {
|
||||
this.input.parentNode.appendChild(this.container);
|
||||
@@ -101,76 +96,82 @@ AutoSuggest.prototype.openContainer = function() {
|
||||
this.container.className = 'autosuggest';
|
||||
this.input.parentNode.appendChild(this.container);
|
||||
return this.container;
|
||||
};
|
||||
};
|
||||
|
||||
AutoSuggest.prototype.clearContainer = function() {
|
||||
clearContainer() {
|
||||
while (this.container.children.length > 0) {
|
||||
this.container.removeChild(this.container.children[0]);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
AutoSuggest.prototype.clearInput = function() {
|
||||
clearInput() {
|
||||
this.input.value = "";
|
||||
this.input.focus();
|
||||
};
|
||||
};
|
||||
|
||||
AutoSuggest.prototype.closeContainer = function() {
|
||||
closeContainer() {
|
||||
this.container.parentNode.removeChild(this.container);
|
||||
};
|
||||
};
|
||||
|
||||
AutoSuggest.prototype.fillContainer = function(response) {
|
||||
var self = this;
|
||||
fillContainer(response) {
|
||||
this.selectedIndex = 0;
|
||||
response.items.forEach(function(item, i) {
|
||||
var node = document.createElement('li');
|
||||
var text = document.createTextNode(item.label);
|
||||
|
||||
let query = this.input.value.trim().replace(/[\-\[\]{}()*+?.,\\\/^\$|#]/g, ' ');
|
||||
let query_tokens = query.split(/ +/).sort((a,b) => a.length - b.length);
|
||||
|
||||
response.items.forEach((item, i) => {
|
||||
let node = document.createElement('li');
|
||||
node.innerHTML = this.highlightMatches(query_tokens, item.label);
|
||||
node.jsondata = item;
|
||||
node.addEventListener('click', function(event) {
|
||||
self.appendCallback(this.jsondata);
|
||||
self.closeContainer();
|
||||
self.clearInput();
|
||||
node.addEventListener('click', event => {
|
||||
this.appendCallback(event.target.jsondata);
|
||||
this.closeContainer();
|
||||
this.clearInput();
|
||||
});
|
||||
node.appendChild(text);
|
||||
self.container.appendChild(node);
|
||||
if (self.container.children.length === 1) {
|
||||
this.container.appendChild(node);
|
||||
if (this.container.children.length === 1) {
|
||||
node.className = 'selected';
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
function TagAutoSuggest(opt) {
|
||||
AutoSuggest.prototype.constructor.call(this, opt);
|
||||
this.type = "tags";
|
||||
highlightMatches(query_tokens, item) {
|
||||
let itemTokens = item.split(/ +/);
|
||||
let queryTokens = new RegExp('(' + query_tokens.join('\|') + ')', 'i');
|
||||
itemTokens.forEach((token, index) => {
|
||||
item = item.replace(token, token.replace(queryTokens, ($1, match) => '<strong>' + match + '</strong>'));
|
||||
});
|
||||
return item;
|
||||
};
|
||||
}
|
||||
|
||||
TagAutoSuggest.prototype = Object.create(AutoSuggest.prototype);
|
||||
class TagAutoSuggest extends AutoSuggest {
|
||||
constructor(opt) {
|
||||
super(opt);
|
||||
this.type = "tags";
|
||||
}
|
||||
|
||||
TagAutoSuggest.prototype.constructor = TagAutoSuggest;
|
||||
|
||||
TagAutoSuggest.prototype.fillContainer = function(response) {
|
||||
fillContainer(response) {
|
||||
if (response.items.length > 0) {
|
||||
AutoSuggest.prototype.fillContainer.call(this, response);
|
||||
super.fillContainer.call(this, response);
|
||||
} else {
|
||||
var node = document.createElement('li')
|
||||
let node = document.createElement('li')
|
||||
node.innerHTML = "<em>Tag does not exist yet. Create it?</em>";
|
||||
|
||||
var self = this;
|
||||
node.addEventListener('click', function(event) {
|
||||
self.createNewTag(function(response) {
|
||||
self.appendCallback(response);
|
||||
});
|
||||
self.closeContainer();
|
||||
self.clearInput();
|
||||
node.addEventListener('click', event => {
|
||||
this.createNewTag(response => this.appendCallback(response));
|
||||
this.closeContainer();
|
||||
this.clearInput();
|
||||
});
|
||||
|
||||
self.container.appendChild(node);
|
||||
this.container.appendChild(node);
|
||||
this.selectedIndex = 0;
|
||||
node.className = 'selected';
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
TagAutoSuggest.prototype.createNewTag = function(callback) {
|
||||
var request_uri = this.baseurl + '/suggest/?type=createtag';
|
||||
var request = new HttpRequest('post', request_uri, 'tag=' + encodeURIComponent(this.input.value), callback, this);
|
||||
createNewTag(callback) {
|
||||
let request_uri = this.baseurl + '/suggest/?type=createtag';
|
||||
let request = new HttpRequest('post', request_uri, 'tag=' + encodeURIComponent(this.input.value), callback, this);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user