Compare commits

...

16 Commits

Author SHA1 Message Date
7853c6e285 Convert admin page to Vue 2023-12-16 09:19:40 +01:00
f674b0d934 Standardize admin page 2023-12-16 09:18:28 +01:00
d767e335f9 Bump font awesome to 6.5.0 2023-12-07 20:33:52 +01:00
3611f2206f Swapping to objects vue branch 2023-11-19 18:25:52 +01:00
828d32b0ef Revert changes on MainAppPage: splitting getParams 2023-11-19 18:10:00 +01:00
f5d193e42b Split dependencies into modules 2023-11-19 01:03:21 +01:00
c45a19e6bf Move masks 2023-11-11 17:32:47 +01:00
f86dadfc7d Convert project to webpack 2023-11-11 17:23:33 +01:00
9d676c339b Admin: reject ID value 0 2023-11-11 17:18:35 +01:00
2f3a3f9561 Move php classes 2023-11-11 17:12:41 +01:00
55e40f76a1 Remove librairies css 2023-11-11 17:07:54 +01:00
4e9fb52318 Removing local librairies 2 2023-11-11 15:43:47 +01:00
850d2e7235 Removing local librairies 2023-11-11 15:43:24 +01:00
97645b3476 Move files (again) 2023-11-07 19:41:28 +01:00
ab914a391f Restore language folder 2023-10-20 20:56:11 +02:00
842e02f4bb Move files to follow webpack structure 2023-10-20 20:47:26 +02:00
182 changed files with 9156 additions and 3562 deletions

21
.gitignore vendored
View File

@@ -1,14 +1,7 @@
/settings.php
/style/.sass-cache/
/files/**/*.jpg
/files/**/*.JPG
/files/**/*.jpeg
/files/**/*.JPEG
/files/**/*.png
/files/**/*.PNG
/files/**/*.mov
/files/**/*.MOV
/geo/*.geojson
/spot_cron.sh
/vendor/*
/log.html
/vendor/
/config/settings.php
/files/
/geo/
/node_modules/
/log.html
/dist/

12
build/paths.js Normal file
View File

@@ -0,0 +1,12 @@
const path = require('path')
module.exports = {
SRC: path.resolve(__dirname, '..', 'src'),
DIST: path.resolve(__dirname, '..', 'dist'),
ASSETS: path.resolve(__dirname, '..', 'assets'),
IMAGES: path.resolve(__dirname, '..', 'src/images'),
STYLES: path.resolve(__dirname, '..', 'src/styles'),
LIB: path.resolve(__dirname, '..', 'lib'),
MASKS: path.resolve(__dirname, '..', 'src/masks'),
ROOT: path.resolve(__dirname, '..')
}

111
build/webpack.common.js Normal file
View File

@@ -0,0 +1,111 @@
const path = require('path');
const { SRC, DIST, ASSETS, LIB } = require('./paths');
var webpack = require("webpack");
const CopyWebpackPlugin = require('copy-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const SymlinkWebpackPlugin = require('symlink-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader')
module.exports = {
entry: {
app: path.resolve(SRC, 'scripts', 'app.js')
},
output: {
path: DIST,
filename: '[name].js',
publicPath: './' //meaning dist/
},
devtool: "inline-source-map",
module: {
rules: [{
test: /\.vue$/,
loader: 'vue-loader'
}, {
test: /\.js$/,
exclude: file => (/node_modules/.test(file) && !/\.vue\.js/.test(file)),
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
}
},
}, {
test: /\.html$/i,
loader: "html-loader",
}, {
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource'
}, {
test: /\.s[ac]ss$/i,
use: [
'vue-style-loader',
'css-loader',
'resolve-url-loader',
{
loader: 'sass-loader',
options: {
implementation: require.resolve('sass'),
sourceMap: true
}
}
]
}, {
test: /\.css$/i,
use: ["vue-style-loader", "css-loader"],
}, {
test: /\.(png|svg|jpg|jpeg|gif)$/i,
use: {
loader: "url-loader",
options: {
limit: 1 * 1024,
//name: "images/[name].[hash:7].[ext]"
name: "images/[name].[ext]"
}
}
/*type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8*1024
}
}*/
}
]
},
plugins: [
new webpack.ProvidePlugin({
$: require.resolve('jquery'),
jQuery: require.resolve('jquery'),
//L: require.resolve('leaflet')
}),
new CopyWebpackPlugin({
patterns: [{
from: 'geo/',
to: path.resolve(DIST, 'geo')
}, {
from: path.resolve(LIB, 'index.php'),
to: 'index.php'
}, {
from: path.resolve(SRC, 'images/icons'),
to: 'images/icons'
}]
}),
new SymlinkWebpackPlugin({ origin: '../files/', symlink: 'files' }),
new CleanWebpackPlugin(),
new webpack.DefinePlugin({
__VUE_OPTIONS_API__: 'true',
__VUE_PROD_DEVTOOLS__: 'false'
}),
new VueLoaderPlugin()
],
resolve: {
extensions: ['', '.js'],
alias: {
"@scripts": path.resolve(SRC, "scripts"),
'load-image': 'blueimp-load-image/js/load-image.js',
'load-image-meta': 'blueimp-load-image/js/load-image-meta.js',
'load-image-exif': 'blueimp-load-image/js/load-image-exif.js',
'canvas-to-blob': 'blueimp-canvas-to-blob/js/canvas-to-blob.js',
'jquery-ui/ui/widget': 'blueimp-file-upload/js/vendor/jquery.ui.widget.js'
}
}
};

6
build/webpack.dev.js Normal file
View File

@@ -0,0 +1,6 @@
const { merge } = require('webpack-merge')
module.exports = merge(require('./webpack.common.js'), {
mode: 'development',
watch: true
})

5
build/webpack.prod.js Normal file
View File

@@ -0,0 +1,5 @@
const { merge } = require('webpack-merge')
module.exports = merge(require('./webpack.common.js'), {
mode: 'production'
})

4
cli/cron.sh Normal file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
wget -qO- https://spot.lutran.fr/index.php?a=update_project > /dev/null
#Crontab job: 0 * * * * . /var/www/spot/spot_cron.sh > /dev/null

View File

@@ -9,15 +9,15 @@
}
],
"require": {
"franzz/objects": "dev-composer",
"franzz/objects": "dev-vue",
"phpmailer/phpmailer": "^6.5"
},
"autoload": {
"psr-4": {
"Franzz\\Spot\\": "inc/"
"Franzz\\Spot\\": "lib/"
},
"files": [
"settings.php"
"config/settings.php"
]
}
}

22
composer.lock generated
View File

@@ -4,15 +4,15 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "164c903fea5bdcfb36cf6ea31ec0c307",
"content-hash": "12bb836a394b645df50c14652a2ae5bf",
"packages": [
{
"name": "franzz/objects",
"version": "dev-composer",
"version": "dev-vue",
"dist": {
"type": "path",
"url": "../objects",
"reference": "e1cf78b992a6f52742d6834f7508c0ef373ac860"
"reference": "a9f8601384a0078cf8531576768e2c5bad4bbf95"
},
"type": "library",
"autoload": {
@@ -27,16 +27,16 @@
},
{
"name": "phpmailer/phpmailer",
"version": "v6.8.0",
"version": "v6.8.1",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
"reference": "df16b615e371d81fb79e506277faea67a1be18f1"
"reference": "e88da8d679acc3824ff231fdc553565b802ac016"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/df16b615e371d81fb79e506277faea67a1be18f1",
"reference": "df16b615e371d81fb79e506277faea67a1be18f1",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/e88da8d679acc3824ff231fdc553565b802ac016",
"reference": "e88da8d679acc3824ff231fdc553565b802ac016",
"shasum": ""
},
"require": {
@@ -46,13 +46,13 @@
"php": ">=5.5.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.2",
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
"doctrine/annotations": "^1.2.6 || ^1.13.3",
"php-parallel-lint/php-console-highlighter": "^1.0.0",
"php-parallel-lint/php-parallel-lint": "^1.3.2",
"phpcompatibility/php-compatibility": "^9.3.5",
"roave/security-advisories": "dev-latest",
"squizlabs/php_codesniffer": "^3.7.1",
"squizlabs/php_codesniffer": "^3.7.2",
"yoast/phpunit-polyfills": "^1.0.4"
},
"suggest": {
@@ -95,7 +95,7 @@
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"support": {
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.8.0"
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.8.1"
},
"funding": [
{
@@ -103,7 +103,7 @@
"type": "github"
}
],
"time": "2023-03-06T14:43:22+00:00"
"time": "2023-08-29T08:26:30+00:00"
}
],
"packages-dev": [],

View File

@@ -2,6 +2,7 @@ locale = en_NZ
page_og_desc = Keep contact with François when he is off hiking
error_commit_db = Issue committing to DB
unknown_field = Field "$0" is unknown
impossible_value = Value "$0" is not possible for field "$1"
nav_back = Back
@@ -10,6 +11,9 @@ admin_config = Config
admin_upload = Upload
save = Save
admin_save_success = Saved
admin_create_success= Created
admin_delete_success= Deleted
no_auth = No authorization
track_main = Main track
track_off-track = Off-track
@@ -63,7 +67,7 @@ id_project = Project ID
project = Project
projects = Projects
new_project = New Project
update_project = Update Project
update_project = Update project messages
hikes = Hikes
mode = Mode
mode_previz = Project in preparation
@@ -75,6 +79,7 @@ end = End
feeds = Feeds
id_feed = Feed ID
ref_feed_id = Ref. Feed ID
new_feed = New feed
id_spot = Spot ID
name = Name
status = Status

View File

@@ -2,6 +2,7 @@ locale = es_ES
page_og_desc = Mantente en contacto con François durante sus aventuras a la montaña
error_commit_db = Error SQL
unknown_field = Campo "$0" desconocido
impossible_value = Valor "$0" no es posible para campo "$1"
nav_back = Atrás
@@ -10,6 +11,9 @@ admin_config = Configuración
admin_upload = Cargar
save = Guardar
admin_save_success = Guardado
admin_create_success= Creado
admin_delete_success= Eliminado
no_auth = No autorización
track_main = Camino principal
track_off-track = Variante
@@ -63,7 +67,7 @@ id_project = Proyecto ID
project = Proyecto
projects = Proyectos
new_project = Nuevo proyecto
update_project = Actualizar el proyecto
update_project = Actualizar los mensajes del proyecto
hikes = Senderos
mode = Modo
mode_previz = Proyecto en preparación
@@ -75,6 +79,7 @@ end = Fin
feeds = Feeds
id_feed = ID Feed
ref_feed_id = ID Feed ref.
new_feed = Nuevo feed
id_spot = ID Spot
name = Descripción
status = Estado

View File

@@ -2,6 +2,7 @@ locale = fr_CH
page_og_desc = Gardez le contact avec François lorsqu'il part sur les chemins
error_commit_db = Error lors de la requête SQL
unknown_field = Champ "$0" inconnu
impossible_value = La valeur "$0" n'est pas possible pour le champ "$1"
nav_back = Retour
@@ -10,6 +11,9 @@ admin_config = Paramètres
admin_upload = Uploader
save = Sauvegarder
admin_save_success = Sauvegardé
admin_create_success= Créé
admin_delete_success= Supprimé
no_auth = Pas d'authorisation
track_main = Trajet principal
track_off-track = Variante
@@ -63,7 +67,7 @@ id_project = ID projet
project = Projet
projects = Projets
new_project = Nouveau projet
update_project = Mettre à jour le projet
update_project = Mettre à jour les messages du projet
hikes = Randonnées
mode = Mode
mode_previz = Projet en cours de préparation
@@ -75,6 +79,7 @@ end = Arrivée
feeds = Feeds
id_feed = ID Feed
ref_feed_id = ID Feed ref.
new_feed = Nouveau feed
id_spot = ID Spot
name = Description
status = Statut

View File

@@ -36,18 +36,18 @@ class Converter extends PhpObject {
public static function isGeoJsonValid($sCodeName) {
$bResult = false;
$sGpxFilePath = Geo::getFilePath($sCodeName, Gpx::EXT);
$sGeoJsonFilePath = Geo::getFilePath($sCodeName, GeoJson::EXT);
$sGpxFilePath = Gpx::getFilePath($sCodeName);
$sGeoJsonFilePath = GeoJson::getFilePath($sCodeName);
//No need to generate if gpx is missing
if(!file_exists($sGpxFilePath) || file_exists($sGeoJsonFilePath) && filemtime($sGeoJsonFilePath) > filemtime(Geo::getFilePath($sCodeName, Gpx::EXT))) $bResult = true;
if(!file_exists($sGpxFilePath) || file_exists($sGeoJsonFilePath) && filemtime($sGeoJsonFilePath) > filemtime(Gpx::getFilePath($sCodeName))) $bResult = true;
return $bResult;
}
}
class Geo extends PhpObject {
const GEO_FOLDER = 'geo/';
const GEO_FOLDER = '../geo/';
const OPT_SIMPLE = 'simplification';
protected $asTracks;
@@ -55,12 +55,16 @@ class Geo extends PhpObject {
public function __construct($sCodeName) {
parent::__construct(get_class($this), Settings::DEBUG, PhpObject::MODE_HTML);
$this->sFilePath = self::getFilePath($sCodeName, static::EXT);
$this->sFilePath = self::getFilePath($sCodeName);
$this->asTracks = array();
}
public static function getFilePath($sCodeName, $sExt) {
return self::GEO_FOLDER.$sCodeName.$sExt;
public static function getFilePath($sCodeName) {
return self::GEO_FOLDER.$sCodeName.static::EXT;
}
public static function getDistFilePath($sCodeName) {
return 'geo/'.$sCodeName.static::EXT;
}
public function getLog() {

View File

@@ -144,8 +144,8 @@ class Project extends PhpObject {
if($sCodeName != '' && !Converter::isGeoJsonValid($sCodeName)) Converter::convertToGeoJson($sCodeName);
$asProject['geofilepath'] = Spot::addTimestampToFilePath(Geo::getFilePath($sCodeName, GeoJson::EXT));
$asProject['gpxfilepath'] = Spot::addTimestampToFilePath(Geo::getFilePath($sCodeName, Gpx::EXT));
$asProject['geofilepath'] = Spot::addTimestampToFilePath(GeoJson::getDistFilePath($sCodeName));
$asProject['gpxfilepath'] = Spot::addTimestampToFilePath(Gpx::getDistFilePath($sCodeName));
$asProject['codename'] = $sCodeName;
}
return $bSpecificProj?$asProject:$asProjects;

View File

@@ -28,6 +28,8 @@ use \Settings;
* - Posts (table `posts`):
* - site_time: timestamp in Site Time
* - timezone: Local Timezone
* - Users (table `users`):
* - timezone: Site Timezone (stored user's timezone for emails)
*/
class Spot extends Main
@@ -40,6 +42,8 @@ class Spot extends Main
const DEFAULT_LANG = 'en';
const MAIN_PAGE = 'index';
private Project $oProject;
private Media $oMedia;
private User $oUser;
@@ -158,8 +162,8 @@ class Spot extends Main
);
}
public function getAppMainPage()
{
public function getAppMainPage() {
//Cache Page List
$asPages = array_diff($this->asMasks, array('email_update', 'email_conf'));
if(!$this->oUser->checkUserClearance(User::CLEARANCE_ADMIN)) {
@@ -175,23 +179,16 @@ class Spot extends Main
'user' => $this->oUser->getUserInfo()
),
'consts' => array(
'server' => $this->asContext['serv_name'],
'modes' => Project::MODES,
'clearances' => User::CLEARANCES,
'default_timezone' => Settings::TIMEZONE
)
),
'index',
self::MAIN_PAGE,
array(
'language' => $this->oLang->getLanguage(),
'host_url' => $this->asContext['serv_name'],
'filepath_css' => self::addTimestampToFilePath('style/spot.css'),
'filepath_js_d3' => self::addTimestampToFilePath('script/d3.min.js'),
'filepath_js_leaflet' => self::addTimestampToFilePath('script/leaflet.min.js'),
'filepath_js_jquery' => self::addTimestampToFilePath('script/jquery.min.js'),
'filepath_js_jquery_mods' => self::addTimestampToFilePath('script/jquery.mods.js'),
'filepath_js_spot' => self::addTimestampToFilePath('script/spot.js'),
'filepath_js_lightbox' => self::addTimestampToFilePath('script/lightbox.js')
'language' => $this->oLang->getLanguage(),
'filepath_css' => self::addTimestampToFilePath('spot.css'),
'filepath_js' => self::addTimestampToFilePath('../dist/app.js'),
),
$asPages
);
@@ -652,6 +649,8 @@ class Spot extends Main
$sDesc = '';
$asResult = array();
if($this->oDb->isId($sField) && $sValue <= 0) return self::getJsonResult(false, $this->oLang->getTranslation('impossible_value', [$sValue, $sField]));
switch($sType) {
case 'project':
$oProject = new Project($this->oDb, $iId);
@@ -710,40 +709,66 @@ class Spot extends Main
return self::getJsonResult($bSuccess, $sDesc, array($sType=>array($asResult)));
}
public function delAdminSettings($sType, $iId) {
public function createAdminSettings($sType) {
$bSuccess = false;
$sDesc = '';
$asResult = array();
switch($sType) {
case 'project':
$oProject = new Project($this->oDb);
$iNewProjectId = $oProject->createProjectId();
$oFeed = new Feed($this->oDb);
$oFeed->createFeedId($iNewProjectId);
$bSuccess = $iNewProjectId > 0;
$asResult = array(
'project' => array($oProject->getProject()),
'feed' => array($oFeed->getFeed())
);
break;
case 'feed':
$oFeed = new Feed($this->oDb);
$iNewFeedId = $oFeed->createFeedId($this->oProject->getProjectId());
$bSuccess = $iNewFeedId > 0;
$asResult = array(
'feed' => array($oFeed->getFeed())
);
break;
}
return self::getJsonResult($bSuccess, $sDesc, $asResult);
}
public function deleteAdminSettings($sType, $iId) {
$bSuccess = false;
$sDesc = '';
$asResult = array();
switch($sType) {
case 'project':
$oProject = new Project($this->oDb, $iId);
$asResult = $oProject->delete();
$sDesc = $asResult['project'][0]['desc'];
$bSuccess = $asResult['project'][0]['del'];
break;
case 'feed':
$oFeed = new Feed($this->oDb, $iId);
$asResult = array('feed'=>array($oFeed->delete()));
$asResult = array('feed' => array($oFeed->delete()));
$sDesc = $asResult['feed'][0]['desc'];
$bSuccess = $asResult['feed'][0]['del'];
break;
case 'user':
$asResult = array('user' => array($this->oUser->removeUser($iId)));
$sDesc = $asResult['user'][0]['desc'];
$bSuccess = $asResult['user'][0]['result'];
break;
}
$bSuccess = ($sDesc=='');
return self::getJsonResult($bSuccess, $sDesc, $asResult);
}
public function createProject() {
$oProject = new Project($this->oDb);
$iNewProjectId = $oProject->createProjectId();
$oFeed = new Feed($this->oDb);
$oFeed->createFeedId($iNewProjectId);
return self::getJsonResult($iNewProjectId>0, '', array(
'project' => array($oProject->getProject()),
'feed' => array($oFeed->getFeed())
));
}
public static function decToDms($dValue, $sType) {
if($sType=='lat') $sDirection = ($dValue >= 0)?'N':'S'; //Latitude
else $sDirection = ($dValue >= 0)?'E':'W'; //Longitude

View File

@@ -25,7 +25,12 @@ class Uploader extends UploadHandler
$this->oMedia = &$oMedia;
$this->oLang = &$oLang;
$this->sBody = '';
parent::__construct(array('image_versions'=>array(), 'accept_file_types'=>'/\.(gif|jpe?g|png|mov|mp4)$/i'));
parent::__construct(array(
'upload_dir' => Media::MEDIA_FOLDER,
'image_versions' => array(),
'accept_file_types' => '/\.(gif|jpe?g|png|mov|mp4)$/i'
));
}
protected function validate($uploaded_file, $file, $error, $index, $content_range) {

View File

@@ -20,6 +20,7 @@ class User extends PhpObject {
//Cookie
const COOKIE_ID_USER = 'subscriber';
const COOKIE_DURATION = 60 * 60 * 24 * 365; //1 year
/**
* Database Handle
* @var Db
@@ -33,7 +34,7 @@ class User extends PhpObject {
public function __construct(Db &$oDb) {
parent::__construct(__CLASS__);
$this->oDb = &$oDb;
$this->iUserId = 0;
$this->setUserId(0);
$this->asUserInfo = array(
'id' => 0,
Db::getId(self::USER_TABLE) => 0,
@@ -47,6 +48,51 @@ class User extends PhpObject {
$this->checkUserCookie();
}
public function getUserId() {
return $this->iUserId;
}
public function setUserId($iUserId) {
$this->iUserId = 0;
if($iUserId > 0) {
$asUser = $this->getActiveUserInfo($iUserId);
if(!empty($asUser)) {
$this->iUserId = $iUserId;
$this->asUserInfo = $asUser;
}
}
}
public function getUserInfo() {
return $this->asUserInfo;
}
public function getActiveUserInfo($iUserId) {
$asUsersInfo = array();
if($iUserId > 0) $asUsersInfo = $this->getActiveUsersInfo($iUserId);
return empty($asUsersInfo)?array():array_shift($asUsersInfo);
}
public function getActiveUsersInfo($iUserId=-1) {
//Mapping between user fields and DB fields
$asSelect = array_keys($this->asUserInfo);
$asSelect[array_search('id', $asSelect)] = Db::getId(self::USER_TABLE)." AS id";
//Non-admin cannot access clearance info
if(!$this->checkUserClearance(self::CLEARANCE_ADMIN)) unset($asSelect['clearance']);
$asInfo = array(
'select' => $asSelect,
'from' => self::USER_TABLE,
'constraint'=> array('active'=>self::USER_ACTIVE)
);
if($iUserId != -1) $asInfo['constraint'][Db::getId(self::USER_TABLE)] = $iUserId;
return $this->oDb->selectRows($asInfo);
}
public function getLang() {
return $this->asUserInfo['language'];
}
@@ -95,20 +141,25 @@ class User extends PhpObject {
return Spot::getResult($bSuccess, $sDesc);
}
public function removeUser() {
public function removeUser($iUserId=0) {
$iUserId = ($iUserId > 0)?$iUserId:$this->getUserId();
$bSelf = ($iUserId == $this->getUserId());
$bSuccess = false;
$sDesc = '';
if($this->iUserId > 0) {
$iUserId = $this->oDb->updateRow(self::USER_TABLE, $this->getUserId(), array('active'=>self::USER_INACTIVE));
if($iUserId==0) $sDesc = 'lang:error_commit_db';
else {
$sDesc = 'lang:nl_unsubscribed';
$this->updateCookie(-60 * 60); //Set Cookie in the past, deleting it
$bSuccess = true;
if($bSelf || $this->checkUserClearance(self::CLEARANCE_ADMIN)) {
if($this->getUserId() > 0) {
$iUserId = $this->oDb->updateRow(self::USER_TABLE, $iUserId, array('active' => self::USER_INACTIVE));
if($iUserId==0) $sDesc = 'lang:error_commit_db';
else {
$sDesc = 'lang:nl_unsubscribed';
if($bSelf) $this->updateCookie(-60 * 60); //Set Cookie in the past, deleting it
$bSuccess = true;
}
}
else $sDesc = 'lang:nl_unknown_email';
}
else $sDesc = 'lang:nl_unknown_email';
else $sDesc = 'lang:no_auth';
return Spot::getResult($bSuccess, $sDesc);
}
@@ -131,49 +182,6 @@ class User extends PhpObject {
}
}
public function getUserId() {
return $this->iUserId;
}
public function setUserId($iUserId) {
$this->iUserId = 0;
$asUser = $this->getActiveUserInfo($iUserId);
if(!empty($asUser)) {
$this->iUserId = $iUserId;
$this->asUserInfo = $asUser;
}
}
public function getUserInfo() {
return $this->asUserInfo;
}
public function getActiveUserInfo($iUserId) {
$asUsersInfo = array();
if($iUserId > 0) $asUsersInfo = $this->getActiveUsersInfo($iUserId);
return empty($asUsersInfo)?array():array_shift($asUsersInfo);
}
public function getActiveUsersInfo($iUserId=-1) {
//Mapping between user fields and DB fields
$asSelect = array_keys($this->asUserInfo);
$asSelect[array_search('id', $asSelect)] = Db::getId(self::USER_TABLE)." AS id";
//Non-admin cannot access clearance info
if(!$this->checkUserClearance(self::CLEARANCE_ADMIN)) unset($asSelect['clearance']);
$asInfo = array(
'select' => $asSelect,
'from' => self::USER_TABLE,
'constraint'=> array('active'=>self::USER_ACTIVE)
);
if($iUserId != -1) $asInfo['constraint'][Db::getId(self::USER_TABLE)] = $iUserId;
return $this->oDb->selectRows($asInfo);
}
public function checkUserClearance($iClearance)
{
return ($this->asUserInfo['clearance'] >= $iClearance);

View File

@@ -5,7 +5,8 @@
//Start buffering
ob_start();
$oLoader = require __DIR__.'/vendor/autoload.php';
//Run from /dist/
$oLoader = require __DIR__.'/../vendor/autoload.php';
use Franzz\Objects\ToolBox;
use Franzz\Objects\Main;
@@ -70,17 +71,17 @@ if($sAction!='')
case 'add_comment':
$sResult = $oSpot->addComment($iId, $sContent);
break;
case 'admin_new':
$sResult = $oSpot->createProject();
break;
case 'admin_get':
$sResult = $oSpot->getAdminSettings();
break;
case 'admin_set':
$sResult = $oSpot->setAdminSettings($sType, $iId, $sField, $oValue);
break;
case 'admin_del':
$sResult = $oSpot->delAdminSettings($sType, $iId);
case 'admin_create':
$sResult = $oSpot->createAdminSettings($sType);
break;
case 'admin_delete':
$sResult = $oSpot->deleteAdminSettings($sType, $iId);
break;
case 'generate_cron':
$sResult = $oSpot->genCronFile();

View File

@@ -1,212 +0,0 @@
<div id="admin">
<a name="back" class="button" href="[#]host_url[#]"><i class="fa fa-back push"></i>[#]lang:nav_back[#]</a>
<h1>[#]lang:projects[#]</h1>
<div id="project_section">
<table>
<thead>
<tr>
<th>[#]lang:id_project[#]</th>
<th>[#]lang:project[#]</th>
<th>[#]lang:mode[#]</th>
<th>[#]lang:code_name[#]</th>
<th>[#]lang:start[#]</th>
<th>[#]lang:end[#]</th>
<th>[#]lang:delete[#]</th>
</tr>
</thead>
<tbody></tbody>
</table>
<div id="new"></div>
</div>
<h1>[#]lang:feeds[#]</h1>
<div id="feed_section">
<table>
<thead>
<tr>
<th>[#]lang:id_feed[#]</th>
<th>[#]lang:ref_feed_id[#]</th>
<th>[#]lang:id_spot[#]</th>
<th>[#]lang:id_project[#]</th>
<th>[#]lang:name[#]</th>
<th>[#]lang:status[#]</th>
<th>[#]lang:last_update[#]</th>
<th>[#]lang:delete[#]</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<h1>Spots</h1>
<div id="spot_section">
<table>
<thead>
<tr>
<th>[#]lang:id_spot[#]</th>
<th>[#]lang:ref_spot_id[#]</th>
<th>[#]lang:name[#]</th>
<th>[#]lang:model[#]</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<h1>[#]lang:active_users[#]</h1>
<div id="user_section">
<table>
<thead>
<tr>
<th>[#]lang:id_user[#]</th>
<th>[#]lang:user_name[#]</th>
<th>[#]lang:language[#]</th>
<th>[#]lang:time_zone[#]</th>
<th>[#]lang:clearance[#]</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<h1>[#]lang:toolbox[#]</h1>
<div id="toolbox"></div>
<div id="feedback" class="feedback"></div>
</div>
<script type="text/javascript">
oSpot.pageInit = function(asHash) {
self.get('admin_get', setProjects);
$('#new').addButton('new', self.lang('new_project'), 'new', createProject);
$('#toolbox').addButton('refresh', self.lang('update_project'), 'refresh', updateProject);
};
oSpot.onFeedback = function(sType, sMsg, asContext) {
delete asContext.a;
delete asContext.t;
sMsg += ' (';
$.each(asContext, function(sKey, sElem) {
sMsg += sKey+'='+sElem+' / ' ;
});
sMsg = sMsg.slice(0, -3)+')';
$('#feedback').append($('<p>', {'class': sType}).text(sMsg));
};
function setProjects(asElemTypes) {
var aoEvents = [{on:'change', callback:commit}, {on:'keyup', callback:waitAndCommit}];
var aoChangeEvent = [aoEvents[0]];
$.each(asElemTypes, function(sElemType, aoElems) {
$.each(aoElems, function(iKey, oElem) {
var sElemId = sElemType+'_'+oElem.id;
var bNew = ($('#'+sElemId).length == 0);
var $Elem = (bNew?$('<tr>', {'id': sElemId}):$('#'+sElemId))
.data('type', sElemType)
.data('id', oElem.id);
if(oElem.del) $Elem.remove();
else if(!bNew) {
$Elem.find('input').each(function(iKey, oInput){
var $Input = $(oInput);
if($Input.attr('name') in oElem && $Input.attr('type')!='date') $Input.val(oElem[$Input.attr('name')]);
});
}
else {
$Elem.append($('<td>').text(oElem.id || ''));
switch(sElemType) {
case 'project':
$Elem
.append($('<td>').addInput('text', 'name', oElem.name, aoEvents))
.append($('<td>', {'class': 'mode'}).text(oElem.mode))
.append($('<td>').addInput('text', 'codename', oElem.codename, aoEvents))
.append($('<td>').addInput('date', 'active_from', oElem.active_from, aoChangeEvent))
.append($('<td>').addInput('date', 'active_to', oElem.active_to, aoChangeEvent))
.append($('<td>').addButton('close fa-lg', '', 'del_proj', del));
break;
case 'feed':
$Elem
.append($('<td>').addInput('text', 'ref_feed_id', oElem.ref_feed_id, aoEvents))
.append($('<td>').addInput('number', 'id_spot', oElem.id_spot, aoEvents))
.append($('<td>').addInput('number', 'id_project', oElem.id_project, aoEvents))
.append($('<td>').text(oElem.name))
.append($('<td>').text(oElem.status))
.append($('<td>').text(oElem.last_update))
.append($('<td>').addButton('close fa-lg', '', 'del_feed', del));
break;
case 'spot':
$Elem
.append($('<td>').text(oElem.ref_spot_id))
.append($('<td>').text(oElem.name))
.append($('<td>').text(oElem.model))
break;
case 'user':
$Elem
.append($('<td>').text(oElem.name))
.append($('<td>').text(oElem.language))
.append($('<td>').text(oElem.timezone))
.append($('<td>').addInput('number', 'clearance', oElem.clearance, aoEvents))
break;
}
$Elem.appendTo($('#'+sElemType+'_section').find('table tbody'));
}
});
});
}
function createProject() {
self.get('admin_new', setProjects);
}
function updateProject() {
self.get(
'update_project',
function(asData, sMsg){oSpot.onFeedback('success', sMsg, {'update':'project'});},
{},
function(sMsg){oSpot.onFeedback('error', sMsg, {'update':'project'});}
);
}
function commit(event, $This) {
$This = $This || $(this);
if(typeof self.tmp('wait') != 'undefined') clearTimeout(self.tmp('wait'));
var sOldVal = $This.data('old_value');
var sNewVal = $This.val();
if(sOldVal!=sNewVal) {
$This.data('old_value', sNewVal);
var $Record = $This.closest('tr');
var asInputs = {type: $Record.data('type'), id: $Record.data('id'), field: $This.attr('name'), value: sNewVal};
self.get(
'admin_set',
function(asData){
oSpot.onFeedback('success', self.lang('admin_save_success'), asInputs);
setProjects(asData);
},
asInputs,
function(sError){
$This.data('old_value', sOldVal);
oSpot.onFeedback('error', sError, asInputs);
}
);
}
}
function waitAndCommit(event) {
if(typeof self.tmp('wait') != 'undefined') clearTimeout(self.tmp('wait'));
self.tmp('wait', setTimeout(()=>{commit(event,$(this));}, 2000));
}
function del() {
var $Record = $(this).closest('tr');
var asInputs = {type: $Record.data('type'), id: $Record.data('id')};
self.get(
'admin_del',
function(asData){
oSpot.onFeedback('success', self.lang('admin_save_success'), asInputs);
setProjects(asData);
},
asInputs,
function(sError){
oSpot.onFeedback('error', sError, asInputs);
}
);
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -1,70 +0,0 @@
<div id="upload">
<a name="back" class="button" href="[#]host_url[#]"><i class="fa fa-back push"></i>[#]lang:nav_back[#]</a>
<h1>[#]lang:upload_title[#]</h1>
<input id="fileupload" type="file" name="files[]" multiple>
<div id="progress">
<div class="bar" style="width: 0%;"></div>
</div>
<div id="comments"></div>
<div id="status"></div>
</div>
<script type="text/javascript">
oSpot.pageInit = function(asHash) {
var asProject = self.vars(['projects', self.vars('default_project_codename')]);
self.tmp('status-box', $('#status'));
if(asProject.editable) {
$('#fileupload')
.attr('data-url', self.getActionLink('upload'))
.fileupload({
dataType: 'json',
formData: {t: self.consts.timezone},
acceptFileTypes: /(\.|\/)(gif|jpe?g|png|mov)$/i,
done: function (e, asData) {
$.each(asData.result.files, function(iKey, oFile) {
var bError = ('error' in oFile);
//Feedback
addStatus(bError?oFile.error:(self.lang('upload_success', [oFile.name])));
//Comments
if(!bError) addCommentBox(oFile.id, oFile.thumbnail);
});
},
progressall: function (e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
$('#progress .bar').css('width', progress+'%');
}
});
}
else addStatus(self.lang('upload_mode_archived', [asProject.name]), true);
};
function addCommentBox(iMediaId, sThumbnailPath) {
$('#comments').append($('<div>', {'class':'comment'})
.append($('<img>', {'class':'thumb', 'src':sThumbnailPath}))
.append($('<form>')
.append($('<input>', {'class':'content', 'name':'content', 'type':'text'}))
.append($('<input>', {'class':'id', 'name':'id', 'type':'hidden', 'value':iMediaId}))
.append($('<button>', {'class':'save', 'type':'button'})
.click(function(){
var $Form = $(this).parent();
oSpot.get(
'add_comment',
function(asData){addStatus(self.lang('media_comment_update', asData.filename));},
{id:$Form.find('.id').val(), content:$Form.find('.content').val()},
function(sMsgId){addStatus(self.lang(sMsgId));},
);
})
.text(self.lang('save'))
)
)
);
}
function addStatus(sMsg, bClear) {
bClear = bClear || false;
if(bClear) self.tmp('status-box').empty();
self.tmp('status-box').append($('<p>').text(sMsg));
}
</script>

5821
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

50
package.json Normal file
View File

@@ -0,0 +1,50 @@
{
"devDependencies": {
"@babel/core": "^7.23.2",
"@babel/preset-env": "^7.23.2",
"@vue-leaflet/vue-leaflet": "^0.10.1",
"babel-loader": "^9.1.3",
"resolve-url-loader": "^5.0.0",
"symlink-webpack-plugin": "^1.1.0",
"vue-loader": "^17.3.1",
"vue-template-compiler": "^2.7.15",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4"
},
"name": "spot",
"description": "FindMeSpot & GPX integration",
"version": "2.0.0",
"main": "index.js",
"private": true,
"scripts": {
"dev": "webpack --config build/webpack.dev.js",
"prod": "webpack --config build/webpack.prod.js"
},
"keywords": [],
"author": "Franzz",
"dependencies": {
"autosize": "^6.0.1",
"blueimp-file-upload": "^10.32.0",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.8.1",
"d3": "^7.8.5",
"file-loader": "^6.2.0",
"html-loader": "^4.2.0",
"jquery": "^3.7.1",
"jquery-mousewheel": "^3.1.13",
"jquery.waitforimages": "^2.4.0",
"leaflet": "^1.9.4",
"leaflet-geometryutil": "^0.10.2",
"leaflet.heightgraph": "^1.4.0",
"lightbox2": "^2.11.4",
"resize-observer-polyfill": "^1.5.1",
"sass": "^1.69.4",
"sass-loader": "^13.3.2",
"simplebar": "^6.2.5",
"style-loader": "^3.3.3",
"url-loader": "^4.1.1",
"vue": "^3.3.8",
"vue-style-loader": "^4.1.3"
}
}

View File

@@ -1,6 +1,8 @@
# Spot Project
[Spot](https://www.findmespot.com) & GPX integration
## Dependencies
* npm 18+
* composer
* php-mbstring
* php-imagick
* php-gd
@@ -17,11 +19,13 @@
* max_file_uploads = 50
## Getting started
1. Clone Git onto web server
2. Install dependencies & update php.ini parameters
3. Copy timezone data: mariadb_tzinfo_to_sql /usr/share/zoneinfo | mariadb -u root mysql
4. Copy settings-sample.php to settings.php and populate
5. Go to #admin and create a new project, feed & maps
6. Add a GPX file named <project_codename>.gpx to /geo/
2. composer install
3. npm run dev
4. Update php.ini parameters
5. Copy timezone data: mariadb_tzinfo_to_sql /usr/share/zoneinfo | mariadb -u root mysql
6. Copy settings-sample.php to settings.php and populate
7. Go to #admin and create a new project, feed & maps
8. Add a GPX file named <project_codename>.gpx to /geo/
## To Do List
* ECMA import/export
* Add mail frequency slider

2
script/d3.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

23
script/leaflet.min.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,468 +0,0 @@
function Spot(asGlobals)
{
self = this;
this.consts = asGlobals.consts;
this.consts.hash_sep = '-';
this.consts.title = 'Spotty';
this.consts.default_page = 'project';
this.consts.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || this.consts.default_timezone;
/* Initialization */
this.init = function()
{
//Variables & constants from php
self.vars('tmp', 'object');
self.vars('page', 'string');
self.updateVars(asGlobals.vars);
//page elem
self.elem = {};
self.elem.container = $('#container');
self.elem.main = $('#main');
self.resetTmpFunctions();
//On Key down
$('html').on('keydown', function(oEvent){self.onKeydown(oEvent);});
//on window resize
$(window).on('resize', function(){self.onResize();});
//Setup menu
//self.initMenu();
//Hash management
$(window)
.bind('hashchange', self.onHashChange)
.trigger('hashchange');
};
this.updateVars = function(asVars)
{
$.each(asVars, function(sKey, oValue){self.vars(sKey, oValue)});
};
/* Variable Management */
this.vars = function(oVarName, oValue)
{
var asVarName = (typeof oVarName == 'object')?oVarName:[oVarName];
//Set, name & type / default value (init)
if(typeof oValue !== 'undefined') setElem(self.vars, copyArray(asVarName), oValue);
//Get, only name parameter
return getElem(self.vars, asVarName);
};
this.tmp = function(sVarName, oValue)
{
var asVarName = (typeof sVarName == 'object')?sVarName:[sVarName];
asVarName.unshift('tmp');
return self.vars(asVarName, oValue);
};
/* Interface with server */
this.get = function(sAction, fOnSuccess, oVars, fOnError, fonProgress)
{
if(!oVars) oVars = {};
fOnError = fOnError || function(sError) {console.log(sError);};
fonProgress = fonProgress || function(sState){};
fonProgress('start');
oVars['a'] = sAction;
oVars['t'] = self.consts.timezone;
return $.ajax(
{
url: self.consts.process_page,
data: oVars,
dataType: 'json'
})
.done(function(oData)
{
fonProgress('done');
if(oData.desc.substr(0, self.consts.lang_prefix.length)==self.consts.lang_prefix) oData.desc = self.lang(oData.desc.substr(5));
if(oData.result==self.consts.error) fOnError(oData.desc);
else fOnSuccess(oData.data, oData.desc);
})
.fail(function(jqXHR, textStatus, errorThrown)
{
fonProgress('fail');
fOnError(textStatus+' '+errorThrown);
});
};
this.lang = function(sKey, asParams) {
var sParamType = $.type(asParams);
if(sParamType == 'undefined') asParams = [];
else if($.type(asParams) != 'array') asParams = [asParams];
var sLang = '';
if(sKey in self.consts.lang) {
sLang = self.consts.lang[sKey];
for(i in asParams) sLang = sLang.replace('$'+i, asParams[i]);
}
else {
console.log('missing translation: '+sKey);
sLang = sKey;
}
return sLang;
};
/* Page Switch - Trigger & Event catching */
this.onHashChange = function()
{
var asHash = self.getHash();
if(asHash.hash !='' && asHash.page != '') self.switchPage(asHash); //page switching
else if(self.vars('page')=='') self.setHash(self.consts.default_page); //first page
};
this.getHash = function()
{
var sHash = self.hash();
var asHash = sHash.split(self.consts.hash_sep);
var sPage = asHash.shift() || '';
return {hash:sHash, page:sPage, items:asHash};
};
this.setHash = function(sPage, asItems, bReboot)
{
bReboot = bReboot || false;
sPage = sPage || '';
asItems = asItems || [];
if(typeof asItems == 'string') asItems = [asItems];
if(sPage != '')
{
var sItems = (asItems.length > 0)?self.consts.hash_sep+asItems.join(self.consts.hash_sep):'';
self.hash(sPage+sItems, bReboot);
}
};
this.hash = function(hash, bReboot)
{
bReboot = bReboot || false;
if(!hash) return window.location.hash.slice(1);
else window.location.hash = '#'+hash;
if(bReboot) location.reload();
};
this.updateHash = function(sType, iId) {
sType = sType || '';
iId = iId || 0;
var asHash = self.getHash();
if(iId) self.setHash(asHash.page, [asHash.items[0], sType, iId]);
};
this.flushHash = function(asTypes) {
asTypes = asTypes || [];
var asHash = self.getHash();
if(asHash.items.length > 1 && (asTypes.length == 0 || asTypes.indexOf(asHash.items[1]) != -1)) self.setHash(asHash.page, [asHash.items[0]]);
};
/* Page Switch - DOM Replacement */
this.getActionLink = function(sAction, oVars)
{
if(!oVars) oVars = {};
sVars = '';
for(i in oVars)
{
sVars += '&'+i+'='+oVars[i];
}
return self.consts.process_page+'?a='+sAction+sVars;
};
this.resetTmpFunctions = function()
{
self.pageInit = function(asHash){console.log('no init for the page: '+asHash.page)};
self.onSamePageMove = function(asHash){return false};
self.onQuitPage = function(){return true};
self.onResize = function(){};
self.onFeedback = function(sType, sMsg){};
self.onKeydown = function(oEvent){};
};
this.switchPage = function(asHash)
{
var sPageName = asHash.page;
var bSamePage = (self.vars('page') == sPageName);
var bFirstPage = (self.vars('page') == '');
if(!self.consts.pages[sPageName]) { //Page does not exist
if(bFirstPage) self.setHash(self.consts.default_page);
else self.setHash(self.vars('page'), self.vars(['hash', 'items']));
}
else if(self.onQuitPage(bSamePage) && !bSamePage || self.onSamePageMove(asHash))
{
//Delete tmp variables
self.vars('tmp', {});
//disable tmp functions
self.resetTmpFunctions();
//Officially a new page
self.vars('page', sPageName);
self.vars('hash', asHash);
//Update Page Title
this.setPageTitle(sPageName+' '+(asHash.items[0] || ''));
//Replacing DOM
var $Dom = $(self.consts.pages[sPageName]);
if(bFirstPage)
{
self.splash($Dom, asHash, bFirstPage); //first page
}
else
{
self.elem.main.stop().fadeTo('fast', 0, function(){self.splash($Dom, asHash, bFirstPage);}); //Switching page
}
}
else if(bSamePage) self.vars('hash', asHash);
};
this.setPageTitle = function(sTitle) {
document.title = self.consts.title+' - '+sTitle;
};
this.splash = function($Dom, asHash, bFirstPage)
{
//Switch main content
self.elem.main.empty().html($Dom);
//Page Bootstrap
self.pageInit(asHash, bFirstPage);
//Show main
var $FadeInElem = bFirstPage?self.elem.container:self.elem.main;
$FadeInElem.hide().fadeTo('slow', 1);
};
this.getNaturalDuration = function(iHours) {
var iTimeMinutes = 0, iTimeHours = 0, iTimeDays = Math.floor(iHours/8); //8 hours a day
if(iTimeDays > 1) iTimeDays = Math.round(iTimeDays * 2) / 2; //Round down to the closest half day
else {
iTimeDays = 0;
iTimeHours = Math.floor(iHours);
iHours -= iTimeHours;
iTimeMinutes = Math.floor(iHours * 4) * 15; //Round down to the closest 15 minutes
}
return '~ '
+(iTimeDays>0?(iTimeDays+(iTimeDays%2==0?'':'½')+' '+self.lang(iTimeDays>1?'unit_days':'unit_day')):'') //Days
+((iTimeHours>0 || iTimeDays==0)?iTimeHours+self.lang('unit_hour'):'') //Hours
+((iTimeDays>0 || iTimeMinutes==0)?'':iTimeMinutes) //Minutes
};
this.checkClearance = function(sClearance) {
return (self.vars(['user', 'clearance']) >= sClearance);
};
}
/* Common Functions */
function copyArray(asArray)
{
return asArray.slice(0); //trick to copy array
}
function getElem(aoAnchor, asPath)
{
return (typeof asPath == 'object' && asPath.length > 1)?getElem(aoAnchor[asPath.shift()], asPath):aoAnchor[(typeof asPath == 'object')?asPath.shift():asPath];
}
function setElem(aoAnchor, asPath, oValue)
{
var asTypes = {boolean:false, string:'', integer:0, int:0, array:[], object:{}};
if(typeof asPath == 'object' && asPath.length > 1)
{
var nextlevel = asPath.shift();
if(!(nextlevel in aoAnchor)) aoAnchor[nextlevel] = {}; //Creating a new level
if(typeof aoAnchor[nextlevel] !== 'object') debug('Error - setElem() : Already existing path at level "'+nextlevel+'". Cancelling setElem() action');
return setElem(aoAnchor[nextlevel], asPath, oValue);
}
else
{
var sKey = (typeof asPath == 'object')?asPath.shift():asPath;
return aoAnchor[sKey] = (!(sKey in aoAnchor) && (oValue in asTypes))?asTypes[oValue]:oValue;
}
}
$.prototype.addInput = function(sType, sName, sValue, aoEvents)
{
aoEvents = aoEvents || [];
var $Input = $('<input>', {type: sType, name: sName, value: sValue}).data('old_value', sValue);
$.each(aoEvents, function(iIndex, aoEvent) {
$Input.on(aoEvent.on, aoEvent.callback);
});
return $(this).append($Input);
};
$.prototype.addButton = function(sIcon, sText, sName, fOnClick, sClass)
{
sText = sText || '';
sClass = sClass || '';
var $Btn = $('<button>', {name: sName, 'class':sClass})
.addIcon('fa-'+sIcon, (sText != ''))
.append(sText)
.click(fOnClick);
return $(this).append($Btn);
};
$.prototype.addIcon = function(sIcon, bMargin, sStyle)
{
bMargin = bMargin || false;
sStyle = sStyle || '';
return $(this).append($('<i>', {'class':'fa'+sStyle+' '+sIcon+(bMargin?' push':'')}));
};
$.prototype.defaultVal = function(sDefaultValue)
{
$(this)
.data('default_value', sDefaultValue)
.val(sDefaultValue)
.addClass('defaultText')
.focus(function()
{
var $This = $(this);
if($This.val() == $This.data('default_value')) $This.val('').removeClass('defaultText');
})
.blur(function()
{
var $This = $(this);
if($This.val() == '') $This.val($This.data('default_value')).addClass('defaultText');
});
};
$.prototype.checkForm = function(sSelector)
{
sSelector = sSelector || 'input[type="text"], textarea';
var $This = $(this);
var bOk = true;
$This.find(sSelector).each(function()
{
$This = $(this);
bOk = bOk && $This.val()!='' && $This.val()!=$This.data('default_value');
});
return bOk;
};
$.prototype.cascadingDown = function(sDuration)
{
return $(this).slideDown(sDuration, function(){$(this).next().cascadingDown(sDuration);});
};
$.prototype.hoverSwap = function(sDefault, sHover)
{
return $(this)
.data('default', sDefault)
.data('hover', sHover)
.hover(function(){
var $This = $(this),
sHover = $This.data('hover');
sDefault = $This.data('default');
if(sDefault!='' && sHover != '') {
$This.fadeOut('fast', function() {
var $This = $(this);
$This.text((sDefault==$This.text())?sHover:sDefault).fadeIn('fast');
});
}
})
.text(sDefault);
};
$.prototype.onSwipe = function(fOnStart, fOnMove, fOnEnd){
return $(this)
.on('dragstart', (e) => {
e.preventDefault();
})
.on('mousedown touchstart', (e) => {
var $This = $(this);
var oPos = getDragPosition(e);
$This.data('x-start', oPos.x);
$This.data('y-start', oPos.y);
$This.data('x-move', oPos.x);
$This.data('y-move', oPos.y);
$This.data('moving', true).addClass('moving');
fOnStart({
xStart: $This.data('x-start'),
yStart: $This.data('y-start')
});
})
.on('touchmove mousemove', (e) => {
var $This = $(this);
if($This.data('moving')) {
var oPos = getDragPosition(e);
$This.data('x-move', oPos.x);
$This.data('y-move', oPos.y);
fOnMove({
xStart: $This.data('x-start'),
yStart: $This.data('y-start'),
xMove: $This.data('x-move'),
yMove: $This.data('y-move')
});
}
})
.on('mouseup mouseleave touchend', (e) => {
var $This = $(this);
if($This.data('moving')) {
$This.data('moving', false).removeClass('moving');
fOnEnd({
xStart: $This.data('x-start'),
yStart: $This.data('y-start'),
xEnd: $This.data('x-move'),
yEnd: $This.data('y-move')
});
}
});
};
function getDragPosition(oEvent) {
let bMouse = oEvent.type.includes('mouse');
return {
x: bMouse?oEvent.pageX:oEvent.touches[0].clientX,
y: bMouse?oEvent.pageY:oEvent.touches[0].clientY
};
}
function copyTextToClipboard(text) {
if(!navigator.clipboard) {
var textArea = document.createElement('textarea');
textArea.value = text;
// Avoid scrolling to bottom
textArea.style.top = '0';
textArea.style.left = '0';
textArea.style.position = 'fixed';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
var successful = document.execCommand('copy');
if(!successful) console.error('Fallback: Oops, unable to copy', text);
} catch (err) {
console.error('Fallback: Oops, unable to copy', err);
}
document.body.removeChild(textArea);
return;
}
navigator.clipboard.writeText(text).then(
function() {},
function(err) {
console.error('Async: Could not copy text: ', err);
}
);
}

67
src/Spot.vue Normal file
View File

@@ -0,0 +1,67 @@
<script>
import Project from './components/project.vue';
import Admin from './components/admin.vue';
const aoRoutes = {
'project': Project,
'admin': Admin
};
export default {
data() {
return {
//spot: window.oSpot,
hash: {}
};
},
inject: ['spot'],
computed: {
currentView() {
this.spot.vars('page', this.hash.page);
return aoRoutes[this.hash.page];
}
},
mounted() {
window.addEventListener('hashchange', () => {this.onHashChange();});
var oEvent = new Event('hashchange');
window.dispatchEvent(oEvent);
}
,
methods: {
_hash(hash, bReboot) {
bReboot = bReboot || false;
if(!hash) return window.location.hash.slice(1);
else window.location.hash = '#'+hash;
if(bReboot) location.reload();
},
onHashChange() {
let asHash = this.getHash();
if(asHash.hash !='' && asHash.page != '') this.hash = asHash;
else if(!this.hash.page) this.setHash(this.spot.consts.default_page);
},
getHash() {
let sHash = this._hash();
let asHash = sHash.split(this.spot.consts.hash_sep);
let sPage = asHash.shift() || '';
return {hash:sHash, page:sPage, items:asHash};
},
setHash(sPage, asItems, bReboot) {
bReboot = bReboot || false;
sPage = sPage || '';
asItems = asItems || [];
if(typeof asItems == 'string') asItems = [asItems];
if(sPage != '') {
let sItems = (asItems.length > 0)?this.spot.consts.hash_sep+asItems.join(this.spot.consts.hash_sep):'';
this._hash(sPage+sItems, bReboot);
}
}
}
}
</script>
<template>
<div id="#main">
<component :is="currentView" />
</div>
</template>

231
src/components/admin.vue Normal file
View File

@@ -0,0 +1,231 @@
<script>
import SpotButton from './spotButton.vue';
import AdminInput from './adminInput.vue';
export default {
components: {
SpotButton,
AdminInput
},
data() {
return {
elems: {},
feedbacks: []
};
},
inject: ['spot'],
methods: {
l(id) {
return this.spot.lang(id);
},
async setProjects() {
let aoElemTypes = await this.spot.get2('admin_get');
for(const [sType, aoElems] of Object.entries(aoElemTypes)) {
this.elems[sType] = {};
for(const [iKey, oElem] of Object.entries(aoElems)) {
oElem.type = sType;
this.elems[sType][oElem.id] = oElem;
}
}
},
createElem(sType) {
this.spot.get2('admin_create', {type: sType})
.then((aoNewElemTypes) => {
console.log(aoNewElemTypes);
for(const [sType, aoNewElems] of Object.entries(aoNewElemTypes)) {
for(const [iKey, oNewElem] of Object.entries(aoNewElems)) {
oNewElem.type = sType;
this.elems[sType][oNewElem.id] = oNewElem;
this.spot.onFeedback('success', this.spot.lang('admin_create_success'), {'create':sType});
}
}
})
.catch((sMsg) => {console.log(sMsg);this.spot.onFeedback('error', sMsg, {'create':sType});});
},
deleteElem(oElem) {
const asInputs = {
type: oElem.type,
id: oElem.id
};
this.spot.get(
'admin_delete',
(asData) => {
delete this.elems[asInputs.type][asInputs.id];
this.spot.onFeedback('success', this.spot.lang('admin_delete_success'), asInputs);
},
asInputs,
(sError) => {
this.spot.onFeedback('error', sError, asInputs);
}
);
},
updateElem(oElem, oEvent) {
if(typeof this.spot.tmp('wait') != 'undefined') clearTimeout(this.spot.tmp('wait'));
let sOldVal = this.elems[oElem.type][oElem.id][oEvent.target.name];
let sNewVal = oEvent.target.value;
if(sOldVal != sNewVal) {
let asInputs = {
type: oElem.type,
id: oElem.id,
field: oEvent.target.name,
value: sNewVal
};
this.spot.get2('admin_set', asInputs)
.then((asData) => {
this.elems[oElem.type][oElem.id][oEvent.target.name] = sNewVal;
this.spot.onFeedback('success', this.spot.lang('admin_save_success'), asInputs);
})
.catch((sError) => {
oEvent.target.value = sOldVal;
this.spot.onFeedback('error', sError, asInputs);
});
}
},
queue(oElem, oEvent) {
if(typeof this.spot.tmp('wait') != 'undefined') clearTimeout(this.spot.tmp('wait'));
this.spot.tmp('wait', setTimeout(() => {this.updateElem(oElem, oEvent);}, 2000));
},
updateProject() {
this.spot.get2('update_project')
.then((asData, sMsg) => {this.spot.onFeedback('success', sMsg, {'update':'project'});})
.catch((sMsg) => {this.spot.onFeedback('error', sMsg, {'update':'project'});});
}
},
mounted() {
this.spot.addPage('admin', {
onFeedback: (sType, sMsg, asContext) => {
delete asContext.a;
delete asContext.t;
sMsg += ' (';
for(const [sKey, sElem] of Object.entries(asContext)) {
sMsg += sKey+'='+sElem+' / ' ;
}
sMsg = sMsg.slice(0, -3)+')';
this.feedbacks.push({type:sType, msg:sMsg});
}
});
this.setProjects();
}
}
</script>
<template>
<div id="admin">
<a name="back" class="button" href="#project"><i class="fa fa-back push"></i>{{ l('nav_back') }}</a>
<h1>{{ l('projects') }}</h1>
<div id="project_section">
<table>
<thead>
<tr>
<th>{{ l('id_project') }}</th>
<th>{{ l('project') }}</th>
<th>{{ l('mode') }}</th>
<th>{{ l('code_name') }}</th>
<th>{{ l('start') }}</th>
<th>{{ l('end') }}</th>
<th>{{ l('delete') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="project in elems.project">
<td>{{ project.id }}</td>
<td><AdminInput :type="'text'" :name="'name'" :elem="project" /></td>
<td>{{ project.mode }}</td>
<td><AdminInput :type="'text'" :name="'codename'" :elem="project" /></td>
<td><AdminInput :type="'date'" :name="'active_from'" :elem="project" /></td>
<td><AdminInput :type="'date'" :name="'active_to'" :elem="project" /></td>
<td><SpotButton :iconClass="'close fa-lg'" @click="deleteElem(project)" /></td>
</tr>
</tbody>
</table>
<SpotButton :buttonClass="'new'" :buttonText="l('new_project')" :iconClass="'new'" @click="createElem('project')" />
</div>
<h1>{{ l('feeds') }}</h1>
<div id="feed_section">
<table>
<thead>
<tr>
<th>{{ l('id_feed') }}</th>
<th>{{ l('ref_feed_id') }}</th>
<th>{{ l('id_spot') }}</th>
<th>{{ l('id_project') }}</th>
<th>{{ l('name') }}</th>
<th>{{ l('status') }}</th>
<th>{{ l('last_update') }}</th>
<th>{{ l('delete') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="feed in elems.feed">
<td>{{ feed.id }}</td>
<td><AdminInput :type="'text'" :name="'ref_feed_id'" :elem="feed" /></td>
<td><AdminInput :type="'number'" :name="'id_spot'" :elem="feed" /></td>
<td><AdminInput :type="'number'" :name="'id_project'" :elem="feed" /></td>
<td>{{ feed.name }}</td>
<td>{{ feed.status }}</td>
<td>{{ feed.last_update }}</td>
<td><SpotButton :iconClass="'close fa-lg'" @click="deleteElem(feed)" /></td>
</tr>
</tbody>
</table>
<SpotButton :buttonClass="'new'" :buttonText="l('new_feed')" :iconClass="'new'" @click="createElem('feed')" />
</div>
<h1>Spots</h1>
<div id="spot_section">
<table>
<thead>
<tr>
<th>{{ l('id_spot') }}</th>
<th>{{ l('ref_spot_id') }}</th>
<th>{{ l('name') }}</th>
<th>{{ l('model') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="spot in elems.spot">
<td>{{ spot.id }}</td>
<td>{{ spot.ref_spot_id }}</td>
<td>{{ spot.name }}</td>
<td>{{ spot.model }}</td>
</tr>
</tbody>
</table>
</div>
<h1>{{ l('active_users') }}</h1>
<div id="user_section">
<table>
<thead>
<tr>
<th>{{ l('id_user') }}</th>
<th>{{ l('user_name') }}</th>
<th>{{ l('language') }}</th>
<th>{{ l('time_zone') }}</th>
<th>{{ l('clearance') }}</th>
<th>{{ l('delete') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="user in elems.user">
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.language }}</td>
<td>{{ user.timezone }}</td>
<td><AdminInput :type="'number'" :name="'clearance'" :elem="user" /></td>
<td><SpotButton :iconClass="'close fa-lg'" @click="deleteElem(user)" /></td>
</tr>
</tbody>
</table>
</div>
<h1>{{ l('toolbox') }}</h1>
<div id="toolbox">
<SpotButton :buttonClass="'refresh'" :buttonText="l('update_project')" :iconClass="'refresh'" @click="updateProject" />
</div>
<div id="feedback" class="feedback">
<p v-for="feedback in feedbacks" :class="feedback.type">{{ feedback.msg }}</p>
</div>
</div>
</template>

View File

@@ -0,0 +1,17 @@
<script>
export default {
props: {
type: String,
name: String,
elem: Object
},
computed: {
value() {
return this.elem[this.name];
}
}
}
</script>
<template>
<input :type="type" :name="name" :value="value" @change="$parent.updateElem(elem, $event)" @keyup="$parent.queue(elem, $event)" />
</template>

View File

@@ -0,0 +1,44 @@
<script>
//Leaflet
import 'leaflet';
import 'leaflet-geometryutil';
import 'leaflet.heightgraph';
import '../scripts/leaflet.helpers';
import { LMap, LTileLayer } from "@vue-leaflet/vue-leaflet";
export default {
components: {
LMap,
LTileLayer
},
data() {
return {
server: this.spot.consts.server,
zoom: 13
};
},
inject: ['spot'],
mounted() {
if(this.$parent.hash.items.length==0) this.$parent.setHash(this.$parent.hash.page, [this.spot.vars('default_project_codename')]);
}
}
</script>
<template>
<div id="projects">
<div id="background"></div>
<div id="submap">
<div class="loader fa fa-fw fa-map flicker" id="map_loading"></div>
</div>
<div id="map">
<l-map ref="map" v-model:zoom="zoom" :center="[47.41322, -1.219482]">
<l-tile-layer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
layer-type="base"
name="OpenStreetMap"
></l-tile-layer>
</l-map>
</div>
</div>
<div id="mobile" class="mobile"></div>
</template>

View File

@@ -0,0 +1,22 @@
<script>
import SpotIcon from './spotIcon.vue';
export default {
components: {
SpotIcon
},
props: {
buttonClass: String,
buttonText: String,
iconClass: String
},
data() {
return {
margin: !!this.buttonText
}
}
}
</script>
<template>
<button :class="buttonClass"><SpotIcon :iconClass="iconClass" :margin="margin" />{{ buttonText }}</button>
</template>

View File

@@ -0,0 +1,17 @@
<script>
export default {
props: {
iconClass: String,
margin: Boolean,
otherClasses: String
},
data() {
return {
classNames: 'fa fa-'+this.iconClass+(this.margin?' push':'')+(this.otherClasses?' '+this.otherClasses:'')
}
}
}
</script>
<template>
<i :class="classNames"></i>
</template>

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 821 B

After

Width:  |  Height:  |  Size: 821 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

Before

Width:  |  Height:  |  Size: 261 KiB

After

Width:  |  Height:  |  Size: 261 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -7,7 +7,7 @@
<meta property="og:title" content="Spotty" />
<meta property="og:description" content="[#]lang:page_og_desc[#]" />
<meta property="og:type" content="website" />
<meta property="og:url" content="[#]host_url[#]" />
<meta property="og:url" content="[#]server[#]" />
<meta property="og:image" content="images/ogp.png" />
<meta property="og:locale" content="[#]lang:locale[#]" />
<link rel="apple-touch-icon" sizes="180x180" href="images/icons/apple-touch-icon.png?v=GvmqYyKwbb">
@@ -19,22 +19,13 @@
<meta name="msapplication-TileColor" content="#00a300">
<meta name="msapplication-config" content="images/icons/browserconfig.xml?v=GvmqYyKwbb">
<meta name="theme-color" content="#ffffff">
<link type="text/css" href="[#]filepath_css[#]" rel="stylesheet" media="all" />
<script type="text/javascript" src="[#]filepath_js_d3[#]"></script>
<script type="text/javascript" src="[#]filepath_js_leaflet[#]"></script>
<script type="text/javascript" src="[#]filepath_js_jquery[#]"></script>
<script type="text/javascript" src="[#]filepath_js_jquery_mods[#]"></script>
<script type="text/javascript" src="[#]filepath_js_spot[#]"></script>
<script type="text/javascript" src="[#]filepath_js_lightbox[#]"></script>
<script type="text/javascript">
var oSpot = new Spot([#]GLOBAL_VARS[#]);
$(document).ready(oSpot.init);
</script>
<title></title>
<script type="text/javascript">window.params = [#]GLOBAL_VARS[#];</script>
<title>Spotty</title>
</head>
<body>
<div id="container">
<div id="main"></div>
</div>
<script type="module" src="[#]filepath_js[#]"></script>
</body>
</html>

57
src/masks/project.html Normal file
View File

@@ -0,0 +1,57 @@
<div id="projects">
<div id="background"></div>
<div id="submap">
<div class="loader fa fa-fw fa-map flicker" id="map_loading"></div>
</div>
<div id="map"></div>
<div id="settings">
<div id="settings-panel">
<div class="settings-header">
<div class="logo"><img width="289" height="72" src="images/logo_black.png" alt="Spotty" /></div>
<div id="last_update"><p><span><img src="images/spot-logo-only.svg" alt="" /></span><abbr></abbr></p></div>
</div>
<div class="settings-sections">
<div id="settings-sections-scrollbox">
<div class="settings-section">
<h1><i class="fa fa-fw push fa-project"></i>[#]lang:hikes[#]</h1>
<div id="settings-projects"></div>
</div>
<div class="settings-section">
<h1><i class="fa fa-fw push fa-map"></i>[#]lang:maps[#]</h1>
<div id="layers"></div>
</div>
<div class="settings-section newsletter">
<h1><i class="fa fa-fw push fa-newsletter"></i>[#]lang:newsletter[#]</h1>
<input type="email" name="email" id="email" placeholder="[#]lang:nl_email_placeholder[#]" /><button id="nl_btn"><span><i class="fa"></i></span></button>
<div id="settings-feedback" class="feedback"></div>
<div id="nl_desc"></div>
</div>
<div class="settings-section admin" id="admin_link">
<h1><i class="fa fa-fw push fa-admin"></i>[#]lang:admin[#]</h1>
<a class="button" id="admin_config" name="admin_config" href="#admin"><i class="fa fa-config push"></i>[#]lang:admin_config[#]</a>
<a class="button" id="admin_upload" name="admin_upload" href="#upload"><i class="fa fa-upload push"></i>[#]lang:admin_upload[#]</a>
</div>
</div>
</div>
<div class="settings-footer"><a href="https://git.lutran.fr/franzz/spot" title="[#]lang:credits_git[#]" target="_blank" rel="noopener"><i class="fa fa-credits push"></i>[#]lang:credits_project[#]</a> [#]lang:credits_license[#]</div>
</div>
</div>
<div id="feed">
<div id="feed-panel">
<div id="poster"></div>
<div id="posts_list"></div>
<div id="loading"></div>
</div>
</div>
<div id="elems">
<div id="settings-button" class="spot-control"><i class="fa fa-menu"></i></div>
<div id="feed-button" class="spot-control"><i class="fa"></i></div>
<div id="legend" class="leaflet-control-layers leaflet-control leaflet-control-layers-expanded">
<div class="track"><span class="line main"></span><span class="desc">[#]lang:track_main[#]</span></div>
<div class="track"><span class="line off-track"></span><span class="desc">[#]lang:track_off-track[#]</span></div>
<div class="track"><span class="line hitchhiking"></span><span class="desc">[#]lang:track_hitchhiking[#]</span></div>
</div>
<div id="title" class="leaflet-control-layers leaflet-control leaflet-control-layers-expanded leaflet-control-inline"><span id="project_name" class=""></span></div>
</div>
</div>
<div id="mobile" class="mobile"></div>

10
src/masks/upload.html Normal file
View File

@@ -0,0 +1,10 @@
<div id="upload">
<a name="back" class="button" href="[#]server[#]"><i class="fa fa-back push"></i>[#]lang:nav_back[#]</a>
<h1>[#]lang:upload_title[#]</h1>
<input id="fileupload" type="file" name="files[]" multiple>
<div id="progress">
<div class="bar" style="width: 0%;"></div>
</div>
<div id="comments"></div>
<div id="status"></div>
</div>

41
src/scripts/app.js Normal file
View File

@@ -0,0 +1,41 @@
//jQuery
import './jquery.helpers.js';
//Common
import { copyArray, getElem, setElem, getDragPosition, copyTextToClipboard } from './common.js';
window.copyArray = copyArray;
window.getElem = getElem;
window.setElem = setElem;
window.getDragPosition = getDragPosition;
window.copyTextToClipboard = copyTextToClipboard;
import Css from './../styles/spot.scss';
import LogoText from '../images/logo_black.png';
import Logo from '../images/spot-logo-only.svg';
console.log(Logo);
//Masks
import Spot from './spot.js';
//import Project from './page.project.js';
//import Upload from './page.upload.js';
//import Admin from './page.admin.js';
window.oSpot = new Spot(params);
//let oProject = new Project(oSpot);
//oSpot.addPage('project', oProject);
//let oUpload = new Upload(oSpot);
//oSpot.addPage('upload', oUpload);
//let oAdmin = new Admin(oSpot);
//oSpot.addPage('admin', oAdmin);
//$(() => {oSpot.init();});
import { createApp } from 'vue';
import SpotVue from '../Spot.vue';
const oSpotVue = createApp(SpotVue);
oSpotVue.provide('spot', window.oSpot);
oSpotVue.mount('#main');

68
src/scripts/common.js Normal file
View File

@@ -0,0 +1,68 @@
/* Common Functions */
export function copyArray(asArray)
{
return asArray.slice(0); //trick to copy array
}
export function getElem(aoAnchor, asPath)
{
return (typeof asPath == 'object' && asPath.length > 1)?getElem(aoAnchor[asPath.shift()], asPath):aoAnchor[(typeof asPath == 'object')?asPath.shift():asPath];
}
export function setElem(aoAnchor, asPath, oValue)
{
var asTypes = {boolean:false, string:'', integer:0, int:0, array:[], object:{}};
if(typeof asPath == 'object' && asPath.length > 1)
{
var nextlevel = asPath.shift();
if(!(nextlevel in aoAnchor)) aoAnchor[nextlevel] = {}; //Creating a new level
if(typeof aoAnchor[nextlevel] !== 'object') debug('Error - setElem() : Already existing path at level "'+nextlevel+'". Cancelling setElem() action');
return setElem(aoAnchor[nextlevel], asPath, oValue);
}
else
{
var sKey = (typeof asPath == 'object')?asPath.shift():asPath;
return aoAnchor[sKey] = (!(sKey in aoAnchor) && (oValue in asTypes))?asTypes[oValue]:oValue;
}
}
export function getDragPosition(oEvent) {
let bMouse = oEvent.type.includes('mouse');
return {
x: bMouse?oEvent.pageX:oEvent.touches[0].clientX,
y: bMouse?oEvent.pageY:oEvent.touches[0].clientY
};
}
export function copyTextToClipboard(text) {
if(!navigator.clipboard) {
var textArea = document.createElement('textarea');
textArea.value = text;
// Avoid scrolling to bottom
textArea.style.top = '0';
textArea.style.left = '0';
textArea.style.position = 'fixed';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
var successful = document.execCommand('copy');
if(!successful) console.error('Fallback: Oops, unable to copy', text);
} catch (err) {
console.error('Fallback: Oops, unable to copy', err);
}
document.body.removeChild(textArea);
return;
}
navigator.clipboard.writeText(text).then(
function() {},
function(err) {
console.error('Async: Could not copy text: ', err);
}
);
}

View File

@@ -0,0 +1,129 @@
$.prototype.addInput = function(sType, sName, sValue, aoEvents) {
aoEvents = aoEvents || [];
let $Input = $('<input>', {type: sType, name: sName, value: sValue}).data('old_value', sValue);
$.each(aoEvents, (iIndex, aoEvent) => {
$Input.on(aoEvent.on, aoEvent.callback);
});
return $(this).append($Input);
};
$.prototype.addButton = function(sIcon, sText, sName, fOnClick, sClass)
{
sText = sText || '';
sClass = sClass || '';
var $Btn = $('<button>', {name: sName, 'class':sClass})
.addIcon('fa-'+sIcon, (sText != ''))
.append(sText)
.click(fOnClick);
return $(this).append($Btn);
};
$.prototype.addIcon = function(sIcon, bMargin, sStyle)
{
bMargin = bMargin || false;
sStyle = sStyle || '';
return $(this).append($('<i>', {'class':'fa'+sStyle+' '+sIcon+(bMargin?' push':'')}));
};
$.prototype.defaultVal = function(sDefaultValue)
{
$(this)
.data('default_value', sDefaultValue)
.val(sDefaultValue)
.addClass('defaultText')
.focus(function()
{
var $This = $(this);
if($This.val() == $This.data('default_value')) $This.val('').removeClass('defaultText');
})
.blur(function()
{
var $This = $(this);
if($This.val() == '') $This.val($This.data('default_value')).addClass('defaultText');
});
};
$.prototype.checkForm = function(sSelector)
{
sSelector = sSelector || 'input[type="text"], textarea';
var $This = $(this);
var bOk = true;
$This.find(sSelector).each(function()
{
$This = $(this);
bOk = bOk && $This.val()!='' && $This.val()!=$This.data('default_value');
});
return bOk;
};
$.prototype.cascadingDown = function(sDuration)
{
return $(this).slideDown(sDuration, function(){$(this).next().cascadingDown(sDuration);});
};
$.prototype.hoverSwap = function(sDefault, sHover)
{
return $(this)
.data('default', sDefault)
.data('hover', sHover)
.hover(function(){
var $This = $(this),
sHover = $This.data('hover');
sDefault = $This.data('default');
if(sDefault!='' && sHover != '') {
$This.fadeOut('fast', function() {
var $This = $(this);
$This.text((sDefault==$This.text())?sHover:sDefault).fadeIn('fast');
});
}
})
.text(sDefault);
};
$.prototype.onSwipe = function(fOnStart, fOnMove, fOnEnd){
return $(this)
.on('dragstart', (e) => {
e.preventDefault();
})
.on('mousedown touchstart', (e) => {
var $This = $(this);
var oPos = getDragPosition(e);
$This.data('x-start', oPos.x);
$This.data('y-start', oPos.y);
$This.data('x-move', oPos.x);
$This.data('y-move', oPos.y);
$This.data('moving', true).addClass('moving');
fOnStart({
xStart: $This.data('x-start'),
yStart: $This.data('y-start')
});
})
.on('touchmove mousemove', (e) => {
var $This = $(this);
if($This.data('moving')) {
var oPos = getDragPosition(e);
$This.data('x-move', oPos.x);
$This.data('y-move', oPos.y);
fOnMove({
xStart: $This.data('x-start'),
yStart: $This.data('y-start'),
xMove: $This.data('x-move'),
yMove: $This.data('y-move')
});
}
})
.on('mouseup mouseleave touchend', (e) => {
var $This = $(this);
if($This.data('moving')) {
$This.data('moving', false).removeClass('moving');
fOnEnd({
xStart: $This.data('x-start'),
yStart: $This.data('y-start'),
xEnd: $This.data('x-move'),
yEnd: $This.data('y-move')
});
}
});
};

View File

@@ -0,0 +1,14 @@
/* Additional Leaflet functions */
L.Map.include({
setOffsetView: function (iOffsetRatioX, oCenter, iZoomLevel) {
var oCenter = (typeof oCenter == 'object')?$.extend({}, oCenter):this.getCenter();
iZoomLevel = iZoomLevel || this.getZoom();
var oBounds = this.getBounds();
var iOffsetX = (oBounds.getEast() - oBounds.getWest()) * iOffsetRatioX / ( 2 * Math.pow(2, iZoomLevel - this.getZoom()));
oCenter.lng = oCenter.lng - iOffsetX;
this.setView(oCenter, iZoomLevel);
}
});

View File

@@ -1,5 +1,5 @@
/*!
* Lightbox v2.11.3
* Lightbox v2.11.4
* by Lokesh Dhakar
*
* More info:
@@ -46,8 +46,6 @@
fadeDuration: 600,
fitImagesInViewport: true,
imageFadeDuration: 600,
// maxWidth: 800,
// maxHeight: 600,
positionFromTop: 50,
resizeDuration: 700,
showImageNumberLabel: true,
@@ -62,10 +60,9 @@
to prevent xss and other injection attacks.
*/
sanitizeTitle: false
//ADDED-START
, hasVideo: true
, hasVideo: true
, onMediaChange: (oMedia) => {}
//ADDED-END
, onClosing: () => {}
};
Lightbox.prototype.option = function(options) {
@@ -147,7 +144,6 @@
</div>\
').appendTo($('body'));
// Cache jQuery objects
this.$lightbox = $('#lightbox');
this.$overlay = $('#lightboxOverlay');
@@ -155,8 +151,7 @@
this.$container = this.$lightbox.find('.lb-container');
this.$image = this.$lightbox.find('.lb-image');
this.$nav = this.$lightbox.find('.lb-nav');
//ADDED-START
if(self.options.hasVideo) {
this.$video = $('<video class="lb-video" controls autoplay></video>');
this.$image.after(this.$video);
@@ -166,8 +161,7 @@
bottom: parseInt(this.$video.css('border-bottom-width'), 10),
left: parseInt(this.$video.css('border-left-width'), 10)
};
}
//ADDED-END
}
// Store css values for future lookup
this.containerPadding = {
@@ -185,11 +179,8 @@
};
// Attach event handlers to the newly minted DOM elements
//ADDED-START
//this.$overlay.hide().on('click', function() {
this.$overlay.hide().add(this.$lightbox.find('.lb-dataContainer')).on('click', function() {
//ADDED-END
self.end();
this.$overlay.hide().add(this.$lightbox.find('.lb-dataContainer')).on('click', function() {
self.end();
return false;
});
@@ -250,9 +241,13 @@
});
this.$lightbox.find('.lb-loader, .lb-close').on('click', function() {
self.end();
return false;
this.$lightbox.find('.lb-loader, .lb-close').on('click keyup', function(e) {
// If mouse click OR 'enter' or 'space' keypress, close LB
if (
e.type === 'click' || (e.type === 'keyup' && (e.which === 13 || e.which === 32))) {
self.end();
return false;
}
});
};
@@ -265,7 +260,6 @@
this.sizeOverlay();
//ADDED-START
//Manage Zoom Event
this.$nav.mousewheel((e) => {
var asImg = self.album[this.currentImageIndex];
@@ -324,7 +318,6 @@
self.$image.css('--translate-x', fTransX + 'px');
self.$image.css('--translate-y', fTransY + 'px');
}
//ADDED-END
this.album = [];
var imageNumber = 0;
@@ -349,7 +342,7 @@
// If image is part of a set
$links = $($link.prop('tagName') + '[rel="' + $link.attr('rel') + '"]');
for (var j = 0; j < $links.length; j = ++j) {
this.addToAlbum($($links[j]));
this.addToAlbum($($links[j]));
if ($links[j] === $link[0]) {
imageNumber = j;
}
@@ -358,14 +351,7 @@
}
// Position Lightbox
//ADDED-START
//var top = $window.scrollTop() + this.options.positionFromTop;
//var left = $window.scrollLeft();
this.$lightbox/*.css({
top: top + 'px',
left: left + 'px'
})*/.fadeIn(this.options.fadeDuration);
//ADDED-END
this.$lightbox.fadeIn(this.options.fadeDuration);
// Disable scrolling of the page while open
if (this.options.disableScrolling) {
@@ -379,20 +365,18 @@
this.album.push({
alt: $link.attr('data-alt'),
link: $link.attr('href'),
title: $link.attr('data-title') || $link.attr('title')
//ADDED-START
, orientation: $link.attr('data-orientation')
, type: $link.attr('data-type')
, id: $link.attr('data-id')
, $Media: $link.attr('data-type')=='video'?this.$video:this.$image
, width: $link.find('img').attr('width')
, height: $link.find('img').attr('height')
, set: $link.attr('data-lightbox') || $link.attr('rel')
//ADDED-END
title: $link.attr('data-title') || $link.attr('title'),
orientation: $link.attr('data-orientation'),
type: $link.attr('data-type'),
id: $link.attr('data-id'),
$Media: $link.attr('data-type')=='video'?this.$video:this.$image,
width: $link.find('img').attr('width'),
height: $link.find('img').attr('height'),
set: $link.attr('data-lightbox') || $link.attr('rel')
});
}
//ADDED-START
Lightbox.prototype.getMaxSizes = function(iMediaWidth, iMediaHeight, sMediaType) {
var iWindowWidth = $(window).width();
var iWindowHeight = $(window).height();
@@ -473,20 +457,20 @@
// Hide most UI elements in preparation for the animated resizing of the lightbox.
Lightbox.prototype.changeImage = function(imageNumber) {
var self = this;
var filename = this.album[imageNumber].link;
// Disable keyboard nav during transitions
this.disableKeyboardNav();
var self = this;
var filename = this.album[imageNumber].link;
// Show loading state
this.$overlay.fadeIn(this.options.fadeDuration);
$('.lb-loader').fadeIn('slow');
// Disable keyboard nav during transitions
this.disableKeyboardNav();
this.$lightbox.find('.lb-image, .lb-video, .lb-nav, .lb-prev, .lb-next, .lb-number, .lb-caption, .lb-close').hide();
// Show loading state
this.$overlay.fadeIn(this.options.fadeDuration);
$('.lb-loader').fadeIn('slow');
this.$lightbox.find('.lb-image, .lb-video, .lb-nav, .lb-prev, .lb-next, .lb-number, .lb-caption, .lb-close').hide();
this.$image.css({'--scale': '1', '--translate-x': '0', '--translate-y': '0'});
self.$lightbox.find('.lb-dataContainer').css({width:'200px', height:'30px'});
this.$outerContainer.addClass('animating');
this.$outerContainer.addClass('animating');
this.$container.removeClass('moveable moving');
this.options.onMediaChange(self.album[imageNumber]);
@@ -494,6 +478,7 @@
var $hasVideoNav = this.$container.hasClass('lb-video-nav');
switch(self.album[imageNumber].type) {
case 'video':
self.$image.removeAttr('src');
this.$video.on('loadedmetadata', function(){
self.album[imageNumber].width = this.videoWidth;
self.album[imageNumber].height = this.videoHeight;
@@ -506,7 +491,7 @@
if(!$hasVideoNav) this.$container.addClass('lb-video-nav');
break;
case 'image':
this.$video.attr('src', '');
this.$video.trigger('pause').removeAttr('src');
if($hasVideoNav) this.$container.removeClass('lb-video-nav');
// When image to show is preloaded, we send the width and height to sizeContainer()
@@ -534,31 +519,19 @@
break;
}
this.currentImageIndex = imageNumber;
this.currentImageIndex = imageNumber;
};
//ADDED-END
// Stretch overlay to fit the viewport
Lightbox.prototype.sizeOverlay = function(e) {
//var self = this;
/*
/*
We use a setTimeout 0 to pause JS execution and let the rendering catch-up.
Why do this? If the `disableScrolling` option is set to true, a class is added to the body
tag that disables scrolling and hides the scrollbar. We want to make sure the scrollbar is
hidden before we measure the document width, as the presence of the scrollbar will affect the
number.
*/
//ADDED-START
/*
setTimeout(function() {
self.$overlay
.width($(document).width())
.height($(document).height());
}, 0);
*/
if(e) {
if(e) {
if(typeof oResizeTimer != 'undefined') clearTimeout(oResizeTimer);
oResizeTimer = setTimeout(
() => {
@@ -573,17 +546,16 @@
},
200
);
}
//ADDED-END
}
};
// Animate the size of the lightbox to fit the image we are showing
// This method also shows the the image.
//ADDED-START
//Lightbox.prototype.sizeContainer = function(imageWidth, imageHeight) {
Lightbox.prototype.sizeContainer = function(imageWidth, imageHeight, media) {
media = media || 'image';
//ADDED-END
Lightbox.prototype.sizeContainer = function(imageWidth, imageHeight, media) {
media = media || 'image';
//ADDED-END
var self = this;
var oldWidth = this.$outerContainer.outerWidth();
@@ -597,19 +569,13 @@
//ADDED-END
function postResize() {
//ADDED-START
//self.$lightbox.find('.lb-dataContainer').width(newWidth);
if(self.$lightbox.hasClass('vertical')) self.$lightbox.find('.lb-dataContainer').width(newWidth);
else self.$lightbox.find('.lb-dataContainer').height(newHeight);
//ADDED-END
self.$lightbox.find('.lb-prevLink').height(newHeight);
self.$lightbox.find('.lb-nextLink').height(newHeight);
// Set focus on one of the two root nodes so keyboard events are captured.
//ADDED-START
//self.$overlay.focus();
self.$overlay.trigger( 'focus' );
//ADDED-END
self.$overlay.trigger('focus');
self.showImage();
}
@@ -630,11 +596,8 @@
Lightbox.prototype.showImage = function() {
this.$lightbox.find('.lb-loader').stop(true).hide();
//ADDED-START
//this.$lightbox.find('.lb-image').fadeIn(this.options.imageFadeDuration);
if(this.options.hasVideo && this.album[this.currentImageIndex].type == 'video') this.$lightbox.find('.lb-video').fadeIn(this.options.imageFadeDuration);
else this.$lightbox.find('.lb-image').fadeIn(this.options.imageFadeDuration);
//ADDED-END
this.updateNav();
this.updateDetails();
@@ -691,29 +654,10 @@
$caption.text(this.album[this.currentImageIndex].title);
} else {
$caption.html(this.album[this.currentImageIndex].title);
}
//ADDED-START
//$caption.fadeIn('fast');
}
$caption.add(this.$lightbox.find('.lb-close')).fadeIn('fast');
//ADDED-END
}
//ADDED-START
/*
//ADDED-END
if (this.album.length > 1 && this.options.showImageNumberLabel) {
var labelText = this.imageCountLabel(this.currentImageIndex + 1, this.album.length);
//ADDED-START
//this.$lightbox.find('.lb-number').text(labelText).fadeIn('fast');
this.$lightbox.find('.lb-number').empty().append(labelText).fadeIn('fast');
//ADDED-END
} else {
this.$lightbox.find('.lb-number').hide();
}
//ADDED-START
*/
//ADDED-END
this.$outerContainer.removeClass('animating');
this.$lightbox.find('.lb-dataContainer').fadeIn(this.options.resizeDuration, function() {
@@ -734,9 +678,7 @@
};
Lightbox.prototype.enableKeyboardNav = function() {
//ADDED-START
this.disableKeyboardNav();
//ADDED-END
this.disableKeyboardNav();
this.$lightbox.on('keyup.keyboard', $.proxy(this.keyboardAction, this));
this.$overlay.on('keyup.keyboard', $.proxy(this.keyboardAction, this));
};
@@ -775,25 +717,24 @@
Lightbox.prototype.end = function() {
this.disableKeyboardNav();
//ADDED-START
if(this.options.hasVideo) {
var $lbContainer = this.$lightbox.find('.lb-container');
var $hasVideoNav = $lbContainer.hasClass('lb-video-nav');
this.$video.attr('src', '');
if(this.options.hasVideo) {
var $lbContainer = this.$lightbox.find('.lb-container');
var $hasVideoNav = $lbContainer.hasClass('lb-video-nav');
this.$video.trigger('pause').removeAttr('src');
if($hasVideoNav) $lbContainer.removeClass('lb-video-nav');
}
oSpot.flushHash();
//ADDED-END
if($hasVideoNav) $lbContainer.removeClass('lb-video-nav');
}
$(window).off('resize', this.sizeOverlay);
this.$nav.off('mousewheel');
this.$nav.off('mousewheel');
this.$lightbox.fadeOut(this.options.fadeDuration);
this.$overlay.fadeOut(this.options.fadeDuration);
if (this.options.disableScrolling) {
$('body').removeClass('lb-disable-scrolling');
}
this.options.onClosing();
};
return new Lightbox();

165
src/scripts/page.admin.js Normal file
View File

@@ -0,0 +1,165 @@
export default class Admin {
constructor(oSpot) {
this.spot = oSpot;
}
pageInit(asHash) {
this.spot.get('admin_get', (asElemTypes) => {this.setProjects(asElemTypes);});
$('#new').addButton('new', this.spot.lang('new_project'), 'new', () => {this.createProject();});
$('#toolbox').addButton('refresh', this.spot.lang('update_project'), 'refresh', () => {this.updateProject();});
}
onFeedback(sType, sMsg, asContext) {
delete asContext.a;
delete asContext.t;
sMsg += ' (';
$.each(asContext, function(sKey, sElem) {
sMsg += sKey+'='+sElem+' / ' ;
});
sMsg = sMsg.slice(0, -3)+')';
$('#feedback').append($('<p>', {'class': sType}).text(sMsg));
}
setProjects(asElemTypes) {
let aoEvents = [
{
on:'change',
callback: (oEvent) => {this.commit(oEvent);}
},
{
on:'keyup',
callback: (oEvent) => {this.waitAndCommit(oEvent);}
}
];
let aoChangeEvent = [aoEvents[0]];
$.each(asElemTypes, (sElemType, aoElems) => {
$.each(aoElems, (iKey, oElem) => {
var sElemId = sElemType+'_'+oElem.id;
var bNew = ($('#'+sElemId).length == 0);
var $Elem = (bNew?$('<tr>', {'id': sElemId}):$('#'+sElemId))
.data('type', sElemType)
.data('id', oElem.id);
if(oElem.del) $Elem.remove();
else if(!bNew) {
$Elem.find('input').each((iKey, oInput) => {
var $Input = $(oInput);
if($Input.attr('name') in oElem && $Input.attr('type')!='date') $Input.val(oElem[$Input.attr('name')]);
});
}
else {
$Elem.append($('<td>').text(oElem.id || ''));
switch(sElemType) {
case 'project':
$Elem
.append($('<td>').addInput('text', 'name', oElem.name, aoEvents))
.append($('<td>', {'class': 'mode'}).text(oElem.mode))
.append($('<td>').addInput('text', 'codename', oElem.codename, aoEvents))
.append($('<td>').addInput('date', 'active_from', oElem.active_from, aoChangeEvent))
.append($('<td>').addInput('date', 'active_to', oElem.active_to, aoChangeEvent))
.append($('<td>').addButton('close fa-lg', '', 'del_proj', (oEvent)=>{this.del(oEvent);}));
break;
case 'feed':
$Elem
.append($('<td>').addInput('text', 'ref_feed_id', oElem.ref_feed_id, aoEvents))
.append($('<td>').addInput('number', 'id_spot', oElem.id_spot, aoEvents))
.append($('<td>').addInput('number', 'id_project', oElem.id_project, aoEvents))
.append($('<td>').text(oElem.name))
.append($('<td>').text(oElem.status))
.append($('<td>').text(oElem.last_update))
.append($('<td>').addButton('close fa-lg', '', 'del_feed', (oEvent)=>{this.del(oEvent);}));
break;
case 'spot':
$Elem
.append($('<td>').text(oElem.ref_spot_id))
.append($('<td>').text(oElem.name))
.append($('<td>').text(oElem.model))
break;
case 'user':
$Elem
.append($('<td>').text(oElem.name))
.append($('<td>').text(oElem.language))
.append($('<td>').text(oElem.timezone))
.append($('<td>').addInput('number', 'clearance', oElem.clearance, aoEvents))
break;
}
$Elem.appendTo($('#'+sElemType+'_section').find('table tbody'));
}
});
});
}
createProject() {
this.spot.get('admin_new', this.setProjects);
}
updateProject() {
this.spot.get(
'update_project',
(asData, sMsg) => {this.spot.onFeedback('success', sMsg, {'update':'project'});},
{},
(sMsg) => {this.spot.onFeedback('error', sMsg, {'update':'project'});}
);
}
commit(oEvent) {
let $Elem = $(oEvent.currentTarget);
if(typeof this.spot.tmp('wait') != 'undefined') clearTimeout(this.spot.tmp('wait'));
var sOldVal = $Elem.data('old_value');
var sNewVal = $Elem.val();
if(sOldVal != sNewVal) {
$Elem.data('old_value', sNewVal);
var $Record = $Elem.closest('tr');
var asInputs = {
type: $Record.data('type'),
id: $Record.data('id'),
field: $Elem.attr('name'),
value: sNewVal
};
this.spot.get(
'admin_set',
(asData) => {
this.spot.onFeedback('success', this.spot.lang('admin_save_success'), asInputs);
this.setProjects(asData);
},
asInputs,
(sError) => {
$Elem.data('old_value', sOldVal);
this.spot.onFeedback('error', sError, asInputs);
}
);
}
}
waitAndCommit(oEvent) {
if(typeof this.spot.tmp('wait') != 'undefined') clearTimeout(this.spot.tmp('wait'));
this.spot.tmp('wait', setTimeout(() => {this.commit(oEvent);}, 2000));
}
del(oEvent) {
var $Record = $(oEvent.currentTarget).closest('tr');
var asInputs = {
type: $Record.data('type'),
id: $Record.data('id')
};
this.spot.get(
'admin_del',
(asData) => {
this.spot.onFeedback('success', this.spot.lang('admin_save_success'), asInputs);
this.setProjects(asData);
},
asInputs,
(sError) => {
this.spot.onFeedback('error', sError, asInputs);
}
);
}
}

1176
src/scripts/page.project.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,77 @@
import "blueimp-file-upload/js/vendor/jquery.ui.widget.js";
import "blueimp-file-upload/js/jquery.iframe-transport.js";
import "blueimp-file-upload/js/jquery.fileupload.js";
//import "blueimp-file-upload/js/jquery.fileupload-image.js";
export default class Upload {
constructor(oSpot) {
this.spot = oSpot;
}
pageInit(asHash) {
let asProject = this.spot.vars(['projects', this.spot.vars('default_project_codename')]);
this.spot.tmp('status-box', $('#status'));
if(asProject.editable) {
$('#fileupload')
.attr('data-url', this.spot.getActionLink('upload'))
.fileupload({
dataType: 'json',
formData: {t: this.spot.consts.timezone},
acceptFileTypes: /(\.|\/)(gif|jpe?g|png|mov)$/i,
done: (e, asData) => {
$.each(asData.result.files, (iKey, oFile) => {
let bError = ('error' in oFile);
//Feedback
this.addStatus(bError?oFile.error:(this.spot.lang('upload_success', [oFile.name])));
//Comments
if(!bError) this.addCommentBox(oFile.id, oFile.thumbnail);
});
},
progressall: (e, data) => {
let progress = parseInt(data.loaded / data.total * 100, 10);
$('#progress .bar').css('width', progress+'%');
}
});
}
else this.addStatus(this.spot.lang('upload_mode_archived', [asProject.name]), true);
}
addCommentBox(iMediaId, sThumbnailPath) {
$('#comments').append($('<div>', {'class':'comment'})
.append($('<img>', {'class':'thumb', 'src':sThumbnailPath}))
.append($('<form>')
.append($('<input>', {'class':'content', 'name':'content', 'type':'text'}))
.append($('<input>', {'class':'id', 'name':'id', 'type':'hidden', 'value':iMediaId}))
.append($('<button>', {'class':'save', 'type':'button'})
.on('click', (oEvent) => {
var $Form = $(oEvent.currentTarget).parent();
this.spot.get(
'add_comment',
(asData) => {
this.addStatus(this.spot.lang('media_comment_update', asData.filename));
},
{
id: $Form.find('.id').val(),
content: $Form.find('.content').val()
},
(sMsgId) => {
this.addStatus(this.spot.lang(sMsgId));
}
);
})
.text(this.spot.lang('save'))
)
)
)
}
addStatus(sMsg, bClear) {
bClear = bClear || false;
if(bClear) this.spot.tmp('status-box').empty();
this.spot.tmp('status-box').append($('<p>').text(sMsg));
}
}

294
src/scripts/spot.js Executable file
View File

@@ -0,0 +1,294 @@
export default class Spot {
constructor(asGlobals) {
this.consts = asGlobals.consts;
this.consts.hash_sep = '-';
this.consts.title = 'Spotty';
this.consts.default_page = 'project';
this.consts.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || this.consts.default_timezone;
this.pages = {};
//Variables & constants from php
this.vars('tmp', 'object');
this.vars('page', 'string');
$.each(asGlobals.vars, (sKey, oValue) => {this.vars(sKey, oValue)});
//page elem
this.elem = {};
}
/* Initialization */
init() {
this.elem.container = $('#container');
this.elem.main = $('#main');
//On Key down
$('html').on('keydown', (oEvent) => {this.onKeydown(oEvent);});
//on window resize
$(window).on('resize', () => {this.onResize();});
//Hash management
$(window)
.on('hashchange', () => {this.onHashChange();})
.trigger('hashchange');
}
/* Variable Management */
vars(oVarName, oValue) {
var asVarName = (typeof oVarName == 'object')?oVarName:[oVarName];
//Set, name & type / default value (init)
if(typeof oValue !== 'undefined') setElem(this.vars, copyArray(asVarName), oValue);
//Get, only name parameter
return getElem(this.vars, asVarName);
}
tmp(sVarName, oValue) {
var asVarName = (typeof sVarName == 'object')?sVarName:[sVarName];
asVarName.unshift('tmp');
return this.vars(asVarName, oValue);
}
/* Interface with server */
get(sAction, fOnSuccess, oVars, fOnError, fonProgress) {
oVars = oVars || {};
fOnError = fOnError || function(sError) {console.log(sError);};
fonProgress = fonProgress || function(sState){};
fonProgress('start');
oVars['a'] = sAction;
oVars['t'] = this.consts.timezone;
return $.ajax(
{
url: this.consts.process_page,
data: oVars,
dataType: 'json'
})
.done((oData) => {
fonProgress('done');
if(oData.desc.substr(0, this.consts.lang_prefix.length)==this.consts.lang_prefix) oData.desc = this.lang(oData.desc.substr(5));
if(oData.result==this.consts.error) fOnError(oData.desc);
else if(fOnSuccess) fOnSuccess(oData.data, oData.desc);
})
.fail((jqXHR, textStatus, errorThrown) => {
fonProgress('fail');
fOnError(textStatus+' '+errorThrown);
});
}
async get2(sAction, oVars) {
oVars = oVars || {};
oVars['a'] = sAction;
oVars['t'] = this.consts.timezone;
let oUrl = new URL(this.consts.server+this.consts.process_page);
oUrl.search = new URLSearchParams(oVars).toString();
try {
let oUrl = new URL(this.consts.server+this.consts.process_page);
oUrl.search = new URLSearchParams(oVars).toString();
const oRequest = await fetch(oUrl, {method: 'GET', /*body: JSON.stringify(oVars),*/ headers: {"Content-Type": "application/json"}});
if(!oRequest.ok) throw new Error('Error HTTP '+oRequest.status+': '+oRequest.statusText);
else {
let oResponse = await oRequest.json();
if(oResponse.desc.substr(0, this.consts.lang_prefix.length)==this.consts.lang_prefix) oResponse.desc = this.lang(oData.desc.substr(this.consts.lang_prefix.length));
if(oResponse.result == this.consts.error) return Promise.reject(oResponse.desc);
else return Promise.resolve(oResponse.data, oResponse.desc);
}
}
catch(oError) {
throw oError;
}
}
lang(sKey, asParams) {
asParams = asParams || [];
if(typeof asParams == 'string') asParams = [asParams];
var sLang = '';
if(sKey in this.consts.lang) {
sLang = this.consts.lang[sKey];
for(let i in asParams) sLang = sLang.replace('$'+i, asParams[i]);
}
else {
console.log('missing translation: '+sKey);
sLang = sKey;
}
return sLang;
}
/* Page Switch - Trigger & Event catching */
onHashChange() {
var asHash = this.getHash();
if(asHash.hash !='' && asHash.page != '') this.switchPage(asHash); //page switching
else if(this.vars('page')=='') this.setHash(this.consts.default_page); //first page
}
getHash() {
var sHash = this.hash();
var asHash = sHash.split(this.consts.hash_sep);
var sPage = asHash.shift() || '';
return {hash:sHash, page:sPage, items:asHash};
}
setHash(sPage, asItems, bReboot) {
bReboot = bReboot || false;
sPage = sPage || '';
asItems = asItems || [];
if(typeof asItems == 'string') asItems = [asItems];
if(sPage != '') {
var sItems = (asItems.length > 0)?this.consts.hash_sep+asItems.join(this.consts.hash_sep):'';
this.hash(sPage+sItems, bReboot);
}
}
hash(hash, bReboot) {
bReboot = bReboot || false;
if(!hash) return window.location.hash.slice(1);
else window.location.hash = '#'+hash;
if(bReboot) location.reload();
}
updateHash(sType, iId) {
sType = sType || '';
iId = iId || 0;
var asHash = this.getHash();
if(iId) this.setHash(asHash.page, [asHash.items[0], sType, iId]);
}
flushHash(asTypes) {
asTypes = asTypes || [];
var asHash = this.getHash();
if(asHash.items.length > 1 && (asTypes.length == 0 || asTypes.indexOf(asHash.items[1]) != -1)) this.setHash(asHash.page, [asHash.items[0]]);
}
/* Page Events */
pageInit(asHash) {
let sPage = this.vars('page');
if(this.pages[sPage].pageInit) this.pages[sPage].pageInit(asHash);
else console.log('no init for the page: '+asHash.page);
}
onSamePageMove(asHash) {
let sPage = this.vars('page');
return (this.pages[sPage] && this.pages[sPage].onSamePageMove)?this.pages[sPage].onSamePageMove(asHash):false;
}
onQuitPage() {
let sPage = this.vars('page');
return (this.pages[sPage] && this.pages[sPage].onQuitPage)?this.pages[sPage].onQuitPage():true;
}
onResize() {
let sPage = this.vars('page');
if(this.pages[sPage].onResize) this.pages[sPage].onResize();
}
onFeedback(sType, sMsg, asContext) {
asContext = asContext || {};
let sPage = this.vars('page');
if(this.pages[sPage].onFeedback) this.pages[sPage].onFeedback(sType, sMsg, asContext);
else console.log({type:sType, msg:sMsg, context:asContext});
}
onKeydown(oEvent) {
let sPage = this.vars('page');
if(this.pages[sPage].onKeydown) this.pages[sPage].onKeydown(oEvent);
}
/* Page Switch - DOM Replacement */
getActionLink(sAction, oVars) {
if(!oVars) oVars = {};
let sVars = '';
for(i in oVars) sVars += '&'+i+'='+oVars[i];
return this.consts.process_page+'?a='+sAction+sVars;
}
addPage(sPage, oPage) {
this.pages[sPage] = oPage;
}
switchPage(asHash) {
var sPageName = asHash.page;
var bSamePage = (this.vars('page') == sPageName);
var bFirstPage = (this.vars('page') == '');
if(!this.consts.pages[sPageName]) { //Page does not exist
if(bFirstPage) this.setHash(this.consts.default_page);
else this.setHash(this.vars('page'), this.vars(['hash', 'items']));
}
else if(this.onQuitPage(bSamePage) && !bSamePage || this.onSamePageMove(asHash))
{
//Delete tmp variables
this.vars('tmp', {});
//Officially a new page
this.vars('page', sPageName);
this.vars('hash', asHash);
//Update Page Title
this.setPageTitle(sPageName+' '+(asHash.items[0] || ''));
//Replacing DOM
var $Dom = $(this.consts.pages[sPageName]);
if(bFirstPage) this.splash($Dom, asHash, bFirstPage); //first page
else this.elem.main.stop().fadeTo('fast', 0, () => {this.splash($Dom, asHash, bFirstPage);}); //Switching page
}
else if(bSamePage) this.vars('hash', asHash);
}
splash($Dom, asHash, bFirstPage)
{
//Switch main content
this.elem.main.empty().html($Dom);
//Page Bootstrap
this.pageInit(asHash, bFirstPage);
//Show main
var $FadeInElem = bFirstPage?this.elem.container:this.elem.main;
$FadeInElem.hide().fadeTo('slow', 1);
}
setPageTitle(sTitle) {
document.title = this.consts.title+' - '+sTitle;
}
getNaturalDuration(iHours) {
var iTimeMinutes = 0, iTimeHours = 0, iTimeDays = Math.floor(iHours/8); //8 hours a day
if(iTimeDays > 1) iTimeDays = Math.round(iTimeDays * 2) / 2; //Round down to the closest half day
else {
iTimeDays = 0;
iTimeHours = Math.floor(iHours);
iHours -= iTimeHours;
iTimeMinutes = Math.floor(iHours * 4) * 15; //Round down to the closest 15 minutes
}
return '~ '
+(iTimeDays>0?(iTimeDays+(iTimeDays%2==0?'':'½')+' '+this.lang(iTimeDays>1?'unit_days':'unit_day')):'') //Days
+((iTimeHours>0 || iTimeDays==0)?iTimeHours+this.lang('unit_hour'):'') //Hours
+((iTimeDays>0 || iTimeMinutes==0)?'':iTimeMinutes) //Minutes
}
checkClearance(sClearance) {
return (this.vars(['user', 'clearance']) >= sClearance);
}
}

View File

@@ -1,4 +1,4 @@
$fa-font-path: "fa/fonts";
$fa-font-path: 'fonts';
$fa-css-prefix: fa;
@import 'fa/solid';

View File

@@ -1,4 +1,4 @@
/* Google Fonts - Ubuntu v15 - https://fonts.googleapis.com/css?family=Ubuntu:400,700&subset=latin-ext&display=swap */
/* Google Fonts - Ubuntu v20 - https://fonts.googleapis.com/css?family=Ubuntu:400,700&subset=latin-ext&display=swap */
/* cyrillic-ext */
@font-face {
@@ -6,7 +6,7 @@
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCs6KVjbNBYlgoKcg72j00.woff2) format('woff2');
src: url(https://fonts.gstatic.com/s/ubuntu/v20/4iCs6KVjbNBYlgoKcg72j00.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@@ -15,8 +15,8 @@
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCs6KVjbNBYlgoKew72j00.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
src: url(https://fonts.gstatic.com/s/ubuntu/v20/4iCs6KVjbNBYlgoKew72j00.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
@@ -24,7 +24,7 @@
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCs6KVjbNBYlgoKcw72j00.woff2) format('woff2');
src: url(https://fonts.gstatic.com/s/ubuntu/v20/4iCs6KVjbNBYlgoKcw72j00.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@@ -33,7 +33,7 @@
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCs6KVjbNBYlgoKfA72j00.woff2) format('woff2');
src: url(https://fonts.gstatic.com/s/ubuntu/v20/4iCs6KVjbNBYlgoKfA72j00.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* latin-ext */
@@ -42,8 +42,8 @@
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCs6KVjbNBYlgoKcQ72j00.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
src: url(https://fonts.gstatic.com/s/ubuntu/v20/4iCs6KVjbNBYlgoKcQ72j00.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
@@ -52,7 +52,7 @@
font-weight: 400;
font-display: swap;
src: url(fonts/4iCs6KVjbNBYlgoKfw72.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
@@ -60,7 +60,7 @@
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCv6KVjbNBYlgoCxCvjvWyNL4U.woff2) format('woff2');
src: url(https://fonts.gstatic.com/s/ubuntu/v20/4iCv6KVjbNBYlgoCxCvjvWyNL4U.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@@ -69,8 +69,8 @@
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCv6KVjbNBYlgoCxCvjtGyNL4U.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
src: url(https://fonts.gstatic.com/s/ubuntu/v20/4iCv6KVjbNBYlgoCxCvjtGyNL4U.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
@@ -78,7 +78,7 @@
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCv6KVjbNBYlgoCxCvjvGyNL4U.woff2) format('woff2');
src: url(https://fonts.gstatic.com/s/ubuntu/v20/4iCv6KVjbNBYlgoCxCvjvGyNL4U.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@@ -87,7 +87,7 @@
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCv6KVjbNBYlgoCxCvjs2yNL4U.woff2) format('woff2');
src: url(https://fonts.gstatic.com/s/ubuntu/v20/4iCv6KVjbNBYlgoCxCvjs2yNL4U.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* latin-ext */
@@ -96,8 +96,8 @@
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCv6KVjbNBYlgoCxCvjvmyNL4U.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
src: url(https://fonts.gstatic.com/s/ubuntu/v20/4iCv6KVjbNBYlgoCxCvjvmyNL4U.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
@@ -106,5 +106,5 @@
font-weight: 700;
font-display: swap;
src: url(fonts/4iCv6KVjbNBYlgoCxCvjsGyN.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

View File

@@ -9,8 +9,8 @@ $stroke-width-mouse-focus : 1;
$stroke-width-height-focus: 2;
$stroke-width-axis : 2;
@import 'leaflet/leaflet';
@import 'leaflet/leaflet_heightgraph';
@import '../../node_modules/leaflet/dist/leaflet.css';
@import '../../node_modules/leaflet.heightgraph/src/L.Control.Heightgraph.css';
/* Leaflet fixes */
.leaflet-container {
@@ -108,3 +108,7 @@ $stroke-width-axis : 2;
}
}
}
.leaflet-default-icon-path {
background-image: none;
}

View File

@@ -1,4 +1,4 @@
@import 'lightbox/lightbox';
@import '../../node_modules/lightbox2/src/css/lightbox.css';
@mixin lightbox-icon($icon) {
background: none;

Some files were not shown because too many files have changed in this diff Show More