Compare commits
40 Commits
4e9fb52318
...
vue
| Author | SHA1 | Date | |
|---|---|---|---|
| d9bc89b7f6 | |||
| 760f38374f | |||
| b9a4bd6d2d | |||
| ea14a1ef3e | |||
| 457bab2c18 | |||
| 3571f93e41 | |||
| e878b159bf | |||
| db70593852 | |||
| 73b8e6b04f | |||
| a49f73236b | |||
| c0b7ad8000 | |||
| 83bf47287c | |||
| 4ce96e7192 | |||
| 3169b8e83e | |||
| 205855acd8 | |||
| 356d8ccd7e | |||
| 25ff80ad7a | |||
| 0cd509a99d | |||
| 3063f8b904 | |||
| 59dea2917d | |||
| 6e614042d1 | |||
| 8c812f6b0a | |||
| abacab8206 | |||
| 869b084d70 | |||
| cab899e544 | |||
| b6fc305111 | |||
| 30a81b5341 | |||
| 683670f77a | |||
| c2956ac373 | |||
| 7853c6e285 | |||
| f674b0d934 | |||
| d767e335f9 | |||
| 3611f2206f | |||
| 828d32b0ef | |||
| f5d193e42b | |||
| c45a19e6bf | |||
| f86dadfc7d | |||
| 9d676c339b | |||
| 2f3a3f9561 | |||
| 55e40f76a1 |
21
.gitignore
vendored
21
.gitignore
vendored
@@ -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
12
build/paths.js
Normal 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, '..')
|
||||
}
|
||||
115
build/webpack.common.js
Normal file
115
build/webpack.common.js
Normal file
@@ -0,0 +1,115 @@
|
||||
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')
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [/*{
|
||||
from: 'geo/',
|
||||
to: path.resolve(DIST, 'geo')
|
||||
}, {
|
||||
from: path.resolve(SRC, 'images/icons'),
|
||||
to: 'images/icons'
|
||||
}, */{
|
||||
from: path.resolve(LIB, 'index.php'),
|
||||
to: 'index.php'
|
||||
}]
|
||||
}),
|
||||
new SymlinkWebpackPlugin([
|
||||
{ origin: '../files/', symlink: 'files' },
|
||||
{ origin: '../geo/', symlink: 'geo' },
|
||||
{ origin: '../src/images/icons/', symlink: 'images/icons' }
|
||||
]),
|
||||
new CleanWebpackPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
__VUE_OPTIONS_API__: 'true',
|
||||
__VUE_PROD_DEVTOOLS__: 'false',
|
||||
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: '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
6
build/webpack.dev.js
Normal 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
5
build/webpack.prod.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const { merge } = require('webpack-merge')
|
||||
|
||||
module.exports = merge(require('./webpack.common.js'), {
|
||||
mode: 'production'
|
||||
})
|
||||
@@ -9,15 +9,15 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"franzz/objects": "dev-composer",
|
||||
"franzz/objects": "dev-vue",
|
||||
"phpmailer/phpmailer": "^6.5"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Franzz\\Spot\\": "api/"
|
||||
"Franzz\\Spot\\": "lib/"
|
||||
},
|
||||
"files": [
|
||||
"settings.php"
|
||||
"config/settings.php"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
29
composer.lock
generated
29
composer.lock
generated
@@ -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": "bcae723140735b1432caaf3070ef4e29ecb73a76"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
@@ -27,16 +27,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpmailer/phpmailer",
|
||||
"version": "v6.8.0",
|
||||
"version": "v6.10.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
||||
"reference": "df16b615e371d81fb79e506277faea67a1be18f1"
|
||||
"reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/df16b615e371d81fb79e506277faea67a1be18f1",
|
||||
"reference": "df16b615e371d81fb79e506277faea67a1be18f1",
|
||||
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
|
||||
"reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -46,16 +46,17 @@
|
||||
"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": {
|
||||
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
|
||||
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
|
||||
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
|
||||
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
|
||||
@@ -95,7 +96,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.10.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -103,7 +104,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2023-03-06T14:43:22+00:00"
|
||||
"time": "2025-04-24T15:19:31+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
@@ -114,7 +115,7 @@
|
||||
},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": [],
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.3.0"
|
||||
"platform": {},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
||||
5
config/db/update_v21_to_v22.sql
Normal file
5
config/db/update_v21_to_v22.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE mappings ADD COLUMN default_map BOOLEAN DEFAULT 0 AFTER id_project;
|
||||
ALTER TABLE mappings ADD CONSTRAINT default_on_generic_map_only CHECK (default_map = 0 OR id_project IS NULL);
|
||||
UPDATE mappings SET default_map = 1 WHERE id_map = (select id_map from maps where codename = 'satellite');
|
||||
UPDATE maps SET token = substring(pattern, locate('token=', pattern) + 6) WHERE codename = 'static_marker';
|
||||
UPDATE maps SET pattern = replace(pattern, token, '{token}') WHERE codename = 'static_marker';
|
||||
@@ -8,7 +8,7 @@ class Settings
|
||||
const DB_NAME = 'spot';
|
||||
const DB_ENC = 'utf8mb4';
|
||||
const TEXT_ENC = 'UTF-8';
|
||||
const TIMEZONE = 'Europe/Paris';
|
||||
const TIMEZONE = 'Europe/Zurich';
|
||||
const MAIL_SERVER = '';
|
||||
const MAIL_FROM = '';
|
||||
const MAIL_USER = '';
|
||||
69304
geo/snt.gpx
Executable file
69304
geo/snt.gpx
Executable file
File diff suppressed because it is too large
Load Diff
103
index.php
103
index.php
@@ -1,103 +0,0 @@
|
||||
<?php
|
||||
|
||||
/* Requests Handler */
|
||||
|
||||
//Start buffering
|
||||
ob_start();
|
||||
|
||||
$oLoader = require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
use Franzz\Objects\ToolBox;
|
||||
use Franzz\Objects\Main;
|
||||
use Franzz\Spot\Spot;
|
||||
use Franzz\Spot\User;
|
||||
|
||||
ToolBox::fixGlobalVars($argv ?? array());
|
||||
|
||||
//Available variables
|
||||
$sAction = $_REQUEST['a'] ?? '';
|
||||
$sTimezone = $_REQUEST['t'] ?? '';
|
||||
$sName = $_GET['name'] ?? '';
|
||||
$sContent = $_GET['content'] ?? '';
|
||||
$iProjectId = $_REQUEST['id_project'] ?? 0 ;
|
||||
$sField = $_REQUEST['field'] ?? '';
|
||||
$oValue = $_REQUEST['value'] ?? '';
|
||||
$iId = $_REQUEST['id'] ?? 0 ;
|
||||
$sType = $_REQUEST['type'] ?? '';
|
||||
$sEmail = $_REQUEST['email'] ?? '';
|
||||
|
||||
//Initiate class
|
||||
$oSpot = new Spot(__FILE__, $sTimezone);
|
||||
$oSpot->setProjectId($iProjectId);
|
||||
|
||||
$sResult = '';
|
||||
if($sAction!='')
|
||||
{
|
||||
switch($sAction)
|
||||
{
|
||||
case 'markers':
|
||||
$sResult = $oSpot->getMarkers();
|
||||
break;
|
||||
case 'next_feed':
|
||||
$sResult = $oSpot->getNextFeed($iId);
|
||||
break;
|
||||
case 'new_feed':
|
||||
$sResult = $oSpot->getNewFeed($iId);
|
||||
break;
|
||||
case 'add_post':
|
||||
$sResult = $oSpot->addPost($sName, $sContent);
|
||||
break;
|
||||
case 'subscribe':
|
||||
$sResult = $oSpot->subscribe($sEmail, $sName);
|
||||
break;
|
||||
case 'unsubscribe':
|
||||
$sResult = $oSpot->unsubscribe();
|
||||
break;
|
||||
case 'unsubscribe_email':
|
||||
$sResult = $oSpot->unsubscribeFromEmail($iId);
|
||||
break;
|
||||
case 'update_project':
|
||||
$sResult = $oSpot->updateProject();
|
||||
break;
|
||||
default:
|
||||
if($oSpot->checkUserClearance(User::CLEARANCE_ADMIN))
|
||||
{
|
||||
switch($sAction)
|
||||
{
|
||||
case 'upload':
|
||||
$sResult = $oSpot->upload();
|
||||
break;
|
||||
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);
|
||||
break;
|
||||
case 'generate_cron':
|
||||
$sResult = $oSpot->genCronFile();
|
||||
break;
|
||||
case 'sql':
|
||||
$sResult = $oSpot->getDbBuildScript();
|
||||
break;
|
||||
default:
|
||||
$sResult = Main::getJsonResult(false, Main::NOT_FOUND);
|
||||
}
|
||||
}
|
||||
else $sResult = Main::getJsonResult(false, Main::NOT_FOUND);
|
||||
}
|
||||
}
|
||||
else $sResult = $oSpot->getAppMainPage();
|
||||
|
||||
$sDebug = ob_get_clean();
|
||||
if(Settings::DEBUG && $sDebug!='') $oSpot->addUncaughtError($sDebug);
|
||||
|
||||
echo $sResult;
|
||||
@@ -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
|
||||
@@ -20,6 +24,7 @@ upload_title = Picture & Video Uploads
|
||||
upload_mode_archived= Project "$0" is archived. No upload allowed
|
||||
upload_success = $0 uploaded successfully
|
||||
upload_media_exist = Picture $0 already exists
|
||||
new_position = New Position
|
||||
|
||||
post_message = Message
|
||||
post_name = Name
|
||||
@@ -63,7 +68,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 +80,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
|
||||
|
||||
@@ -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
|
||||
@@ -20,6 +24,7 @@ upload_title = Cargar fotos y videos
|
||||
upload_mode_archived= El proyecto "$0" esta archivado. No se puede cargar
|
||||
upload_success = $0 ha sido subido
|
||||
upload_media_exist = La imagen $0 ya existe
|
||||
new_position = Nueva posición
|
||||
|
||||
post_message = Mensaje
|
||||
post_name = Nombre
|
||||
@@ -63,7 +68,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 +80,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
|
||||
|
||||
@@ -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
|
||||
@@ -20,6 +24,7 @@ upload_title = Uploader photos & vidéos
|
||||
upload_mode_archived= Le projet "$0" a été archivé. Aucun upload possible
|
||||
upload_success = $0 a été uploadé
|
||||
upload_media_exist = l'image $0 existe déjà
|
||||
new_position = Nouvelle position
|
||||
|
||||
post_message = Message
|
||||
post_name = Nom
|
||||
@@ -63,7 +68,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 +80,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
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
namespace Franzz\Spot;
|
||||
use Franzz\Objects\PhpObject;
|
||||
use Franzz\Objects\ToolBox;
|
||||
use \Settings;
|
||||
|
||||
/**
|
||||
* GPX to GeoJSON Converter
|
||||
@@ -35,298 +33,10 @@ class Converter extends PhpObject {
|
||||
}
|
||||
|
||||
public static function isGeoJsonValid($sCodeName) {
|
||||
$bResult = false;
|
||||
$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(Gpx::getFilePath($sCodeName))) $bResult = true;
|
||||
return $bResult;
|
||||
return !file_exists($sGpxFilePath) || file_exists($sGeoJsonFilePath) && filemtime($sGeoJsonFilePath) >= filemtime($sGpxFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
class Geo extends PhpObject {
|
||||
|
||||
const GEO_FOLDER = '../geo/';
|
||||
const OPT_SIMPLE = 'simplification';
|
||||
|
||||
protected $asTracks;
|
||||
protected $sFilePath;
|
||||
|
||||
public function __construct($sCodeName) {
|
||||
parent::__construct(get_class($this), Settings::DEBUG, PhpObject::MODE_HTML);
|
||||
$this->sFilePath = self::getFilePath($sCodeName);
|
||||
$this->asTracks = array();
|
||||
}
|
||||
|
||||
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() {
|
||||
return $this->getCleanMessageStack(PhpObject::NOTICE_TAB);
|
||||
}
|
||||
}
|
||||
|
||||
class Gpx extends Geo {
|
||||
|
||||
const EXT = '.gpx';
|
||||
|
||||
public function __construct($sCodeName) {
|
||||
parent::__construct($sCodeName);
|
||||
$this->parseFile();
|
||||
}
|
||||
|
||||
public function getTracks() {
|
||||
return $this->asTracks;
|
||||
}
|
||||
|
||||
private function parseFile() {
|
||||
$this->addNotice('Parsing: '.$this->sFilePath);
|
||||
if(!file_exists($this->sFilePath)) $this->addError($this->sFilePath.' file missing');
|
||||
else {
|
||||
$oXml = simplexml_load_file($this->sFilePath);
|
||||
|
||||
//Tracks
|
||||
$this->addNotice('Converting '.count($oXml->trk).' tracks');
|
||||
foreach($oXml->trk as $aoTrack) {
|
||||
$asTrack = array(
|
||||
'name' => (string) $aoTrack->name,
|
||||
'desc' => str_replace("\n", '', ToolBox::fixEOL((strip_tags($aoTrack->desc)))),
|
||||
'cmt' => ToolBox::fixEOL((strip_tags($aoTrack->cmt))),
|
||||
'color' => (string) $aoTrack->extensions->children('gpxx', true)->TrackExtension->DisplayColor,
|
||||
'points'=> array()
|
||||
);
|
||||
|
||||
foreach($aoTrack->trkseg as $asSegment) {
|
||||
foreach($asSegment as $asPoint) {
|
||||
$asTrack['points'][] = array(
|
||||
'lon' => (float) $asPoint['lon'],
|
||||
'lat' => (float) $asPoint['lat'],
|
||||
'ele' => (int) $asPoint->ele
|
||||
);
|
||||
}
|
||||
}
|
||||
$this->asTracks[] = $asTrack;
|
||||
}
|
||||
|
||||
//Waypoints
|
||||
$this->addNotice('Ignoring '.count($oXml->wpt).' waypoints');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GeoJson extends Geo {
|
||||
|
||||
const EXT = '.geojson';
|
||||
const MAX_FILESIZE = 2; //MB
|
||||
const MAX_DEVIATION_FLAT = 0.1; //10%
|
||||
const MAX_DEVIATION_ELEV = 0.1; //10%
|
||||
|
||||
public function __construct($sCodeName) {
|
||||
parent::__construct($sCodeName);
|
||||
}
|
||||
|
||||
public function saveFile() {
|
||||
$this->addNotice('Saving '.$this->sFilePath);
|
||||
file_put_contents($this->sFilePath, $this->buildGeoJson());
|
||||
}
|
||||
|
||||
public function isSimplicationRequired() {
|
||||
//Size in bytes
|
||||
$iFileSize = strlen($this->buildGeoJson());
|
||||
|
||||
//Convert to MB
|
||||
$iFileSize = round($iFileSize / pow(1024, 2), 2);
|
||||
|
||||
//Compare with max allowed size
|
||||
$bFileTooLarge = ($iFileSize > self::MAX_FILESIZE);
|
||||
if($bFileTooLarge) $this->addNotice('Output file is too large ('.$iFileSize.'MB > '.self::MAX_FILESIZE.'MB)');
|
||||
|
||||
return $bFileTooLarge;
|
||||
}
|
||||
|
||||
public function buildTracks($asTracks, $bSimplify=false) {
|
||||
$this->addNotice('Creating '.($bSimplify?'Simplified ':'').'GeoJson Tracks');
|
||||
|
||||
$iGlobalInvalidPointCount = 0;
|
||||
$iGlobalPointCount = 0;
|
||||
|
||||
$this->asTracks = array();
|
||||
foreach($asTracks as $asTrackProps) {
|
||||
$asOptions = $this->parseOptions($asTrackProps['cmt']);
|
||||
|
||||
//Color mapping
|
||||
switch($asTrackProps['color']) {
|
||||
case 'DarkBlue':
|
||||
$sType = 'main';
|
||||
break;
|
||||
case 'Magenta':
|
||||
if($bSimplify && $asOptions[self::OPT_SIMPLE]!='keep') {
|
||||
$this->addNotice('Ignoring Track "'.$asTrackProps['name'].' (off-track)');
|
||||
continue 2; //discard tracks
|
||||
}
|
||||
else {
|
||||
$sType = 'off-track';
|
||||
break;
|
||||
}
|
||||
case 'Red':
|
||||
$sType = 'hitchhiking';
|
||||
break;
|
||||
default:
|
||||
$this->addNotice('Ignoring Track "'.$asTrackProps['name'].' (unknown color "'.$asTrackProps['color'].'")');
|
||||
continue 2; //discard tracks
|
||||
}
|
||||
|
||||
$asTrack = array(
|
||||
'type' => 'Feature',
|
||||
'properties' => array(
|
||||
'name' => $asTrackProps['name'],
|
||||
'type' => $sType,
|
||||
'description' => $asTrackProps['desc']
|
||||
),
|
||||
'geometry' => array(
|
||||
'type' => 'LineString',
|
||||
'coordinates' => array()
|
||||
)
|
||||
);
|
||||
|
||||
//Track points
|
||||
$asTrackPoints = $asTrackProps['points'];
|
||||
$iPointCount = count($asTrackPoints);
|
||||
$iInvalidPointCount = 0;
|
||||
$asPrevPoint = array();
|
||||
foreach($asTrackPoints as $iIndex=>$asPoint) {
|
||||
$asNextPoint = ($iIndex < ($iPointCount - 1))?$asTrackPoints[$iIndex + 1]:array();
|
||||
if($bSimplify && !empty($asPrevPoint) && !empty($asNextPoint)) {
|
||||
if(!$this->isPointValid($asPrevPoint, $asPoint, $asNextPoint)) {
|
||||
$iInvalidPointCount++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$asTrack['geometry']['coordinates'][] = array_values($asPoint);
|
||||
$asPrevPoint = $asPoint;
|
||||
}
|
||||
$this->asTracks[] = $asTrack;
|
||||
|
||||
$iGlobalInvalidPointCount += $iInvalidPointCount;
|
||||
$iGlobalPointCount += $iPointCount;
|
||||
if($iInvalidPointCount > 0) $this->addNotice('Removing '.$iInvalidPointCount.'/'.$iPointCount.' points ('.round($iInvalidPointCount / $iPointCount * 100, 1).'%) from '.$asTrackProps['name']);
|
||||
}
|
||||
|
||||
if($bSimplify) $this->addNotice('Total: '.$iGlobalInvalidPointCount.'/'.$iGlobalPointCount.' points removed ('.round($iGlobalInvalidPointCount / $iGlobalPointCount * 100, 1).'%)');
|
||||
}
|
||||
|
||||
|
||||
public function sortOffTracks() {
|
||||
$this->addNotice('Sorting off-tracks');
|
||||
|
||||
//Find first & last track points
|
||||
$asTracksEnds = array();
|
||||
$asTracks = array();
|
||||
foreach($this->asTracks as $iTrackId=>$asTrack) {
|
||||
$sTrackId = 't'.$iTrackId;
|
||||
$asTracksEnds[$sTrackId] = array('first'=>reset($asTrack['geometry']['coordinates']), 'last'=>end($asTrack['geometry']['coordinates']));
|
||||
$asTracks[$sTrackId] = $asTrack;
|
||||
}
|
||||
|
||||
//Find variants close-by tracks
|
||||
$asClonedTracks = $asTracks;
|
||||
foreach($asClonedTracks as $sTrackId=>$asTrack) {
|
||||
if($asTrack['properties']['type'] != 'off-track') continue;
|
||||
|
||||
$iMinDistance = INF;
|
||||
$sConnectedTrackId = 0;
|
||||
$iPosition = 0;
|
||||
|
||||
//Test all track ending points to find the closest
|
||||
foreach($asTracksEnds as $sTrackEndId=>$asTrackEnds) {
|
||||
if($sTrackEndId != $sTrackId) {
|
||||
//Calculate distance between the last point of the track and every starting point of other tracks
|
||||
$iDistance = self::getDistance($asTracksEnds[$sTrackId]['last'], $asTrackEnds['first']);
|
||||
if($iDistance < $iMinDistance) {
|
||||
$sConnectedTrackId = $sTrackEndId;
|
||||
$iPosition = 0; //Track before the Connected Track
|
||||
$iMinDistance = $iDistance;
|
||||
}
|
||||
|
||||
//Calculate distance between the first point of the track and every ending point of other tracks
|
||||
$iDistance = self::getDistance($asTracksEnds[$sTrackId]['first'], $asTrackEnds['last']);
|
||||
if($iDistance < $iMinDistance) {
|
||||
$sConnectedTrackId = $sTrackEndId;
|
||||
$iPosition = +1; //Track after the Connected Track
|
||||
$iMinDistance = $iDistance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Move track
|
||||
unset($asTracks[$sTrackId]);
|
||||
$iOffset = array_search($sConnectedTrackId, array_keys($asTracks)) + $iPosition;
|
||||
$asTracks = array_slice($asTracks, 0, $iOffset) + array($sTrackId => $asTrack) + array_slice($asTracks, $iOffset);
|
||||
}
|
||||
|
||||
$this->asTracks = array_values($asTracks);
|
||||
}
|
||||
|
||||
private function parseOptions($sComment){
|
||||
$sComment = strip_tags(html_entity_decode($sComment));
|
||||
$asOptions = array(self::OPT_SIMPLE=>'');
|
||||
foreach(explode("\n", $sComment) as $sLine) {
|
||||
$asOptions[mb_strtolower(trim(mb_strstr($sLine, ':', true)))] = mb_strtolower(trim(mb_substr(mb_strstr($sLine, ':'), 1)));
|
||||
}
|
||||
return $asOptions;
|
||||
}
|
||||
|
||||
private function isPointValid($asPointA, $asPointO, $asPointB) {
|
||||
/* A----O Calculate angle AO^OB
|
||||
* \ If angle is within [90% Pi ; 110% Pi], O can be discarded
|
||||
* \ O is valid otherwise
|
||||
* B
|
||||
*/
|
||||
|
||||
//Path Turn Check -> -> -> ->
|
||||
//Law of Cosines (vector): angle = arccos(OA.OB / ||OA||.||OB||)
|
||||
$fVectorOA = array('lon'=>($asPointA['lon'] - $asPointO['lon']), 'lat'=> ($asPointA['lat'] - $asPointO['lat']));
|
||||
$fVectorOB = array('lon'=>($asPointB['lon'] - $asPointO['lon']), 'lat'=> ($asPointB['lat'] - $asPointO['lat']));
|
||||
|
||||
$fLengthOA = sqrt(pow($asPointA['lon'] - $asPointO['lon'], 2) + pow($asPointA['lat'] - $asPointO['lat'], 2));
|
||||
$fLengthOB = sqrt(pow($asPointO['lon'] - $asPointB['lon'], 2) + pow($asPointO['lat'] - $asPointB['lat'], 2));
|
||||
|
||||
$fVectorOAxOB = $fVectorOA['lon'] * $fVectorOB['lon'] + $fVectorOA['lat'] * $fVectorOB['lat'];
|
||||
$fAngleAOB = ($fLengthOA != 0 && $fLengthOB != 0) ? acos($fVectorOAxOB/($fLengthOA * $fLengthOB)) : 0;
|
||||
|
||||
//Elevation Check
|
||||
//Law of Cosines: angle = arccos((OB² + AO² - AB²) / (2*OB*AO))
|
||||
$fLengthAB = sqrt(pow($asPointB['ele'] - $asPointA['ele'], 2) + pow($fLengthOA + $fLengthOB, 2));
|
||||
$fLengthAO = sqrt(pow($asPointO['ele'] - $asPointA['ele'], 2) + pow($fLengthOA, 2));
|
||||
$fLengthOB = sqrt(pow($asPointB['ele'] - $asPointO['ele'], 2) + pow($fLengthOB, 2));
|
||||
$fAngleAOBElev = ($fLengthOB != 0 && $fLengthAO != 0) ? (acos((pow($fLengthOB, 2) + pow($fLengthAO, 2) - pow($fLengthAB, 2)) / (2 * $fLengthOB * $fLengthAO))) : 0;
|
||||
|
||||
return ($fAngleAOB <= (1 - self::MAX_DEVIATION_FLAT) * M_PI || $fAngleAOB >= (1 + self::MAX_DEVIATION_FLAT) * M_PI ||
|
||||
$fAngleAOBElev <= (1 - self::MAX_DEVIATION_ELEV) * M_PI || $fAngleAOBElev >= (1 + self::MAX_DEVIATION_ELEV) * M_PI);
|
||||
}
|
||||
|
||||
private function buildGeoJson() {
|
||||
return json_encode(array('type'=>'FeatureCollection', 'features'=>$this->asTracks));
|
||||
}
|
||||
|
||||
private static function getDistance($asPointA, $asPointB) {
|
||||
$fLatFrom = $asPointA[1];
|
||||
$fLonFrom = $asPointA[0];
|
||||
$fLatTo = $asPointB[1];
|
||||
$fLonTo = $asPointB[0];
|
||||
|
||||
$fRad = M_PI / 180;
|
||||
|
||||
//Calculate distance from latitude and longitude
|
||||
$fTheta = $fLonFrom - $fLonTo;
|
||||
$fDistance = sin($fLatFrom * $fRad) * sin($fLatTo * $fRad) + cos($fLatFrom * $fRad) * cos($fLatTo * $fRad) * cos($fTheta * $fRad);
|
||||
|
||||
return acos($fDistance) / $fRad * 60 * 1.853;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
lib/Feed.php
25
lib/Feed.php
@@ -168,6 +168,31 @@ class Feed extends PhpObject {
|
||||
return $bNewMsg;
|
||||
}
|
||||
|
||||
public function addManualPosition($sLat, $sLng, $iTimestamp) {
|
||||
$sTimeZone = date_default_timezone_get();
|
||||
$oDateTime = new \DateTime('@'.$iTimestamp);
|
||||
$oDateTime->setTimezone(new \DateTimeZone($sTimeZone));
|
||||
$asWeather = $this->getWeather(array($sLat, $sLng), $iTimestamp);
|
||||
|
||||
$asMsg = [
|
||||
'ref_msg_id' => $iTimestamp.'/man',
|
||||
'id_feed' => $this->getFeedId(),
|
||||
'type' => 'OK',
|
||||
'latitude' => $sLat,
|
||||
'longitude' => $sLng,
|
||||
'iso_time' => $oDateTime->format("Y-m-d\TH:i:sO"), //Incorrect ISO 8601 format, but compliant with Spot data
|
||||
'site_time' => $oDateTime->format(Db::TIMESTAMP_FORMAT),
|
||||
'timezone' => $sTimeZone,
|
||||
'unix_time' => $iTimestamp,
|
||||
'content' => '',
|
||||
'battery_state' => '',
|
||||
'posted_on' => date(Db::TIMESTAMP_FORMAT),
|
||||
];
|
||||
|
||||
$iMessageId = $this->oDb->insertRow(self::MSG_TABLE, array_merge($asMsg, $asWeather));
|
||||
return $iMessageId;
|
||||
}
|
||||
|
||||
private function updateFeed() {
|
||||
$bNewMsg = false;
|
||||
$asData = $this->retrieveFeed();
|
||||
|
||||
32
lib/Geo.php
Normal file
32
lib/Geo.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Franzz\Spot;
|
||||
use Franzz\Objects\PhpObject;
|
||||
use \Settings;
|
||||
|
||||
class Geo extends PhpObject {
|
||||
|
||||
const GEO_FOLDER = '../geo/';
|
||||
const OPT_SIMPLE = 'simplification';
|
||||
|
||||
protected $asTracks;
|
||||
protected $sFilePath;
|
||||
|
||||
public function __construct($sCodeName) {
|
||||
parent::__construct(get_class($this), Settings::DEBUG, PhpObject::MODE_HTML);
|
||||
$this->sFilePath = self::getFilePath($sCodeName);
|
||||
$this->asTracks = array();
|
||||
}
|
||||
|
||||
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() {
|
||||
return $this->getCleanMessageStack(PhpObject::NOTICE_TAB);
|
||||
}
|
||||
}
|
||||
214
lib/GeoJson.php
Normal file
214
lib/GeoJson.php
Normal file
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
|
||||
namespace Franzz\Spot;
|
||||
|
||||
class GeoJson extends Geo {
|
||||
|
||||
const EXT = '.geojson';
|
||||
const MAX_FILESIZE = 2; //MB
|
||||
const MAX_DEVIATION_FLAT = 0.1; //10%
|
||||
const MAX_DEVIATION_ELEV = 0.1; //10%
|
||||
|
||||
public function __construct($sCodeName) {
|
||||
parent::__construct($sCodeName);
|
||||
}
|
||||
|
||||
public function saveFile() {
|
||||
$this->addNotice('Saving '.$this->sFilePath);
|
||||
file_put_contents($this->sFilePath, $this->buildGeoJson());
|
||||
}
|
||||
|
||||
public function isSimplicationRequired() {
|
||||
//Size in bytes
|
||||
$iFileSize = strlen($this->buildGeoJson());
|
||||
|
||||
//Convert to MB
|
||||
$iFileSize = round($iFileSize / pow(1024, 2), 2);
|
||||
|
||||
//Compare with max allowed size
|
||||
$bFileTooLarge = ($iFileSize > self::MAX_FILESIZE);
|
||||
if($bFileTooLarge) $this->addNotice('Output file is too large ('.$iFileSize.'MB > '.self::MAX_FILESIZE.'MB)');
|
||||
|
||||
return $bFileTooLarge;
|
||||
}
|
||||
|
||||
public function buildTracks($asTracks, $bSimplify=false) {
|
||||
$this->addNotice('Creating '.($bSimplify?'Simplified ':'').'GeoJson Tracks');
|
||||
|
||||
$iGlobalInvalidPointCount = 0;
|
||||
$iGlobalPointCount = 0;
|
||||
|
||||
$this->asTracks = array();
|
||||
foreach($asTracks as $asTrackProps) {
|
||||
$asOptions = $this->parseOptions($asTrackProps['cmt']);
|
||||
|
||||
//Color mapping
|
||||
switch($asTrackProps['color']) {
|
||||
case 'DarkBlue':
|
||||
$sType = 'main';
|
||||
break;
|
||||
case 'Magenta':
|
||||
if($bSimplify && $asOptions[self::OPT_SIMPLE]!='keep') {
|
||||
$this->addNotice('Ignoring Track "'.$asTrackProps['name'].' (off-track)');
|
||||
continue 2; //discard tracks
|
||||
}
|
||||
else {
|
||||
$sType = 'off-track';
|
||||
break;
|
||||
}
|
||||
case 'Red':
|
||||
$sType = 'hitchhiking';
|
||||
break;
|
||||
default:
|
||||
$this->addNotice('Ignoring Track "'.$asTrackProps['name'].' (unknown color "'.$asTrackProps['color'].'")');
|
||||
continue 2; //discard tracks
|
||||
}
|
||||
|
||||
$asTrack = array(
|
||||
'type' => 'Feature',
|
||||
'properties' => array(
|
||||
'name' => $asTrackProps['name'],
|
||||
'type' => $sType,
|
||||
'description' => $asTrackProps['desc']
|
||||
),
|
||||
'geometry' => array(
|
||||
'type' => 'LineString',
|
||||
'coordinates' => array()
|
||||
)
|
||||
);
|
||||
|
||||
//Track points
|
||||
$asTrackPoints = $asTrackProps['points'];
|
||||
$iPointCount = count($asTrackPoints);
|
||||
$iInvalidPointCount = 0;
|
||||
$asPrevPoint = array();
|
||||
foreach($asTrackPoints as $iIndex=>$asPoint) {
|
||||
$asNextPoint = ($iIndex < ($iPointCount - 1))?$asTrackPoints[$iIndex + 1]:array();
|
||||
if($bSimplify && !empty($asPrevPoint) && !empty($asNextPoint)) {
|
||||
if(!$this->isPointValid($asPrevPoint, $asPoint, $asNextPoint)) {
|
||||
$iInvalidPointCount++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$asTrack['geometry']['coordinates'][] = array_values($asPoint);
|
||||
$asPrevPoint = $asPoint;
|
||||
}
|
||||
$this->asTracks[] = $asTrack;
|
||||
|
||||
$iGlobalInvalidPointCount += $iInvalidPointCount;
|
||||
$iGlobalPointCount += $iPointCount;
|
||||
if($iInvalidPointCount > 0) $this->addNotice('Removing '.$iInvalidPointCount.'/'.$iPointCount.' points ('.round($iInvalidPointCount / $iPointCount * 100, 1).'%) from '.$asTrackProps['name']);
|
||||
}
|
||||
|
||||
if($bSimplify) $this->addNotice('Total: '.$iGlobalInvalidPointCount.'/'.$iGlobalPointCount.' points removed ('.round($iGlobalInvalidPointCount / $iGlobalPointCount * 100, 1).'%)');
|
||||
}
|
||||
|
||||
|
||||
public function sortOffTracks() {
|
||||
$this->addNotice('Sorting off-tracks');
|
||||
|
||||
//Find first & last track points
|
||||
$asTracksEnds = array();
|
||||
$asTracks = array();
|
||||
foreach($this->asTracks as $iTrackId=>$asTrack) {
|
||||
$sTrackId = 't'.$iTrackId;
|
||||
$asTracksEnds[$sTrackId] = array('first'=>reset($asTrack['geometry']['coordinates']), 'last'=>end($asTrack['geometry']['coordinates']));
|
||||
$asTracks[$sTrackId] = $asTrack;
|
||||
}
|
||||
|
||||
//Find variants close-by tracks
|
||||
$asClonedTracks = $asTracks;
|
||||
foreach($asClonedTracks as $sTrackId=>$asTrack) {
|
||||
if($asTrack['properties']['type'] != 'off-track') continue;
|
||||
|
||||
$iMinDistance = INF;
|
||||
$sConnectedTrackId = 0;
|
||||
$iPosition = 0;
|
||||
|
||||
//Test all track ending points to find the closest
|
||||
foreach($asTracksEnds as $sTrackEndId=>$asTrackEnds) {
|
||||
if($sTrackEndId != $sTrackId) {
|
||||
//Calculate distance between the last point of the track and every starting point of other tracks
|
||||
$iDistance = self::getDistance($asTracksEnds[$sTrackId]['last'], $asTrackEnds['first']);
|
||||
if($iDistance < $iMinDistance) {
|
||||
$sConnectedTrackId = $sTrackEndId;
|
||||
$iPosition = 0; //Track before the Connected Track
|
||||
$iMinDistance = $iDistance;
|
||||
}
|
||||
|
||||
//Calculate distance between the first point of the track and every ending point of other tracks
|
||||
$iDistance = self::getDistance($asTracksEnds[$sTrackId]['first'], $asTrackEnds['last']);
|
||||
if($iDistance < $iMinDistance) {
|
||||
$sConnectedTrackId = $sTrackEndId;
|
||||
$iPosition = +1; //Track after the Connected Track
|
||||
$iMinDistance = $iDistance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Move track
|
||||
unset($asTracks[$sTrackId]);
|
||||
$iOffset = array_search($sConnectedTrackId, array_keys($asTracks)) + $iPosition;
|
||||
$asTracks = array_slice($asTracks, 0, $iOffset) + array($sTrackId => $asTrack) + array_slice($asTracks, $iOffset);
|
||||
}
|
||||
|
||||
$this->asTracks = array_values($asTracks);
|
||||
}
|
||||
|
||||
private function parseOptions($sComment){
|
||||
$sComment = strip_tags(html_entity_decode($sComment));
|
||||
$asOptions = array(self::OPT_SIMPLE=>'');
|
||||
foreach(explode("\n", $sComment) as $sLine) {
|
||||
$asOptions[mb_strtolower(trim(mb_strstr($sLine, ':', true)))] = mb_strtolower(trim(mb_substr(mb_strstr($sLine, ':'), 1)));
|
||||
}
|
||||
return $asOptions;
|
||||
}
|
||||
|
||||
private function isPointValid($asPointA, $asPointO, $asPointB) {
|
||||
/* A----O Calculate angle AO^OB
|
||||
* \ If angle is within [90% Pi ; 110% Pi], O can be discarded
|
||||
* \ O is valid otherwise
|
||||
* B
|
||||
*/
|
||||
|
||||
//Path Turn Check -> -> -> ->
|
||||
//Law of Cosines (vector): angle = arccos(OA.OB / ||OA||.||OB||)
|
||||
$fVectorOA = array('lon'=>($asPointA['lon'] - $asPointO['lon']), 'lat'=> ($asPointA['lat'] - $asPointO['lat']));
|
||||
$fVectorOB = array('lon'=>($asPointB['lon'] - $asPointO['lon']), 'lat'=> ($asPointB['lat'] - $asPointO['lat']));
|
||||
|
||||
$fLengthOA = sqrt(pow($asPointA['lon'] - $asPointO['lon'], 2) + pow($asPointA['lat'] - $asPointO['lat'], 2));
|
||||
$fLengthOB = sqrt(pow($asPointO['lon'] - $asPointB['lon'], 2) + pow($asPointO['lat'] - $asPointB['lat'], 2));
|
||||
|
||||
$fVectorOAxOB = $fVectorOA['lon'] * $fVectorOB['lon'] + $fVectorOA['lat'] * $fVectorOB['lat'];
|
||||
$fAngleAOB = ($fLengthOA != 0 && $fLengthOB != 0) ? acos($fVectorOAxOB/($fLengthOA * $fLengthOB)) : 0;
|
||||
|
||||
//Elevation Check
|
||||
//Law of Cosines: angle = arccos((OB² + AO² - AB²) / (2*OB*AO))
|
||||
$fLengthAB = sqrt(pow($asPointB['ele'] - $asPointA['ele'], 2) + pow($fLengthOA + $fLengthOB, 2));
|
||||
$fLengthAO = sqrt(pow($asPointO['ele'] - $asPointA['ele'], 2) + pow($fLengthOA, 2));
|
||||
$fLengthOB = sqrt(pow($asPointB['ele'] - $asPointO['ele'], 2) + pow($fLengthOB, 2));
|
||||
$fAngleAOBElev = ($fLengthOB != 0 && $fLengthAO != 0) ? (acos((pow($fLengthOB, 2) + pow($fLengthAO, 2) - pow($fLengthAB, 2)) / (2 * $fLengthOB * $fLengthAO))) : 0;
|
||||
|
||||
return ($fAngleAOB <= (1 - self::MAX_DEVIATION_FLAT) * M_PI || $fAngleAOB >= (1 + self::MAX_DEVIATION_FLAT) * M_PI ||
|
||||
$fAngleAOBElev <= (1 - self::MAX_DEVIATION_ELEV) * M_PI || $fAngleAOBElev >= (1 + self::MAX_DEVIATION_ELEV) * M_PI);
|
||||
}
|
||||
|
||||
private function buildGeoJson() {
|
||||
return json_encode(array('type'=>'FeatureCollection', 'features'=>$this->asTracks));
|
||||
}
|
||||
|
||||
private static function getDistance($asPointA, $asPointB) {
|
||||
$fLatFrom = $asPointA[1];
|
||||
$fLonFrom = $asPointA[0];
|
||||
$fLatTo = $asPointB[1];
|
||||
$fLonTo = $asPointB[0];
|
||||
|
||||
$fRad = M_PI / 180;
|
||||
|
||||
//Calculate distance from latitude and longitude
|
||||
$fTheta = $fLonFrom - $fLonTo;
|
||||
$fDistance = sin($fLatFrom * $fRad) * sin($fLatTo * $fRad) + cos($fLatFrom * $fRad) * cos($fLatTo * $fRad) * cos($fTheta * $fRad);
|
||||
|
||||
return acos($fDistance) / $fRad * 60 * 1.853;
|
||||
}
|
||||
}
|
||||
52
lib/Gpx.php
Normal file
52
lib/Gpx.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Franzz\Spot;
|
||||
use Franzz\Objects\ToolBox;
|
||||
|
||||
class Gpx extends Geo {
|
||||
|
||||
const EXT = '.gpx';
|
||||
|
||||
public function __construct($sCodeName) {
|
||||
parent::__construct($sCodeName);
|
||||
$this->parseFile();
|
||||
}
|
||||
|
||||
public function getTracks() {
|
||||
return $this->asTracks;
|
||||
}
|
||||
|
||||
private function parseFile() {
|
||||
$this->addNotice('Parsing: '.$this->sFilePath);
|
||||
if(!file_exists($this->sFilePath)) $this->addError($this->sFilePath.' file missing');
|
||||
else {
|
||||
$oXml = simplexml_load_file($this->sFilePath);
|
||||
|
||||
//Tracks
|
||||
$this->addNotice('Converting '.count($oXml->trk).' tracks');
|
||||
foreach($oXml->trk as $aoTrack) {
|
||||
$asTrack = array(
|
||||
'name' => (string) $aoTrack->name,
|
||||
'desc' => str_replace("\n", '', ToolBox::fixEOL((strip_tags($aoTrack->desc)))),
|
||||
'cmt' => ToolBox::fixEOL((strip_tags($aoTrack->cmt))),
|
||||
'color' => (string) $aoTrack->extensions->children('gpxx', true)->TrackExtension->DisplayColor,
|
||||
'points'=> array()
|
||||
);
|
||||
|
||||
foreach($aoTrack->trkseg as $asSegment) {
|
||||
foreach($asSegment as $asPoint) {
|
||||
$asTrack['points'][] = array(
|
||||
'lon' => (float) $asPoint['lon'],
|
||||
'lat' => (float) $asPoint['lat'],
|
||||
'ele' => (int) $asPoint->ele
|
||||
);
|
||||
}
|
||||
}
|
||||
$this->asTracks[] = $asTrack;
|
||||
}
|
||||
|
||||
//Waypoints
|
||||
$this->addNotice('Ignoring '.count($oXml->wpt).' waypoints');
|
||||
}
|
||||
}
|
||||
}
|
||||
33
lib/Map.php
33
lib/Map.php
@@ -11,13 +11,12 @@ class Map extends PhpObject {
|
||||
const MAPPING_TABLE = 'mappings';
|
||||
|
||||
private Db $oDb;
|
||||
|
||||
private $asMaps;
|
||||
|
||||
public function __construct(Db &$oDb) {
|
||||
parent::__construct(__CLASS__);
|
||||
$this->oDb = &$oDb;
|
||||
$this->setMaps();
|
||||
$this->asMaps = array();
|
||||
}
|
||||
|
||||
private function setMaps() {
|
||||
@@ -25,14 +24,36 @@ class Map extends PhpObject {
|
||||
foreach($asMaps as $asMap) $this->asMaps[$asMap['codename']] = $asMap;
|
||||
}
|
||||
|
||||
private function getMaps($sCodeName='') {
|
||||
if(empty($this->asMaps)) $this->setMaps();
|
||||
return ($sCodeName=='')?$this->asMaps:$this->asMaps[$sCodeName];
|
||||
}
|
||||
|
||||
public function getProjectMaps($iProjectId) {
|
||||
$asMappings = $this->oDb->getArrayQuery("SELECT id_map FROM mappings WHERE id_project = ".$iProjectId." OR id_project IS NULL", true);
|
||||
return array_filter($this->asMaps, function($asMap) use($asMappings) {return in_array($asMap['id_map'], $asMappings);});
|
||||
$asMappings = $this->oDb->selectRows(
|
||||
array(
|
||||
'select' => array(Db::getId(self::MAP_TABLE), 'default_map'),
|
||||
'from' => self::MAPPING_TABLE,
|
||||
'constraint'=> array("IFNULL(id_project, {$iProjectId})" => $iProjectId)
|
||||
),
|
||||
Db::getId(self::MAP_TABLE)
|
||||
);
|
||||
|
||||
$asProjectMaps = array();
|
||||
foreach($this->getMaps() as $asMap) {
|
||||
if(array_key_exists($asMap['id_map'], $asMappings)) {
|
||||
$asMap['default_map'] = $asMappings[$asMap['id_map']];
|
||||
$asProjectMaps[] = $asMap;
|
||||
}
|
||||
}
|
||||
|
||||
return $asProjectMaps;
|
||||
}
|
||||
|
||||
public function getMapUrl($sCodeName, $asParams) {
|
||||
$asParams['token'] = $this->asMaps[$sCodeName]['token'];
|
||||
return self::populateParams($this->asMaps[$sCodeName]['pattern'], $asParams);
|
||||
$asMap = $this->getMaps($sCodeName);
|
||||
$asParams['token'] = $asMap['token'];
|
||||
return self::populateParams($asMap['pattern'], $asParams);
|
||||
}
|
||||
|
||||
private static function populateParams($sUrl, $asParams) {
|
||||
|
||||
@@ -12,35 +12,26 @@ class Media extends PhpObject {
|
||||
const MEDIA_TABLE = 'medias';
|
||||
|
||||
//Media folders
|
||||
const MEDIA_FOLDER = '../files/';
|
||||
const MEDIA_FOLDER = 'files/';
|
||||
const THUMB_FOLDER = self::MEDIA_FOLDER.'thumbs/';
|
||||
|
||||
const THUMB_MAX_WIDTH = 400;
|
||||
|
||||
/**
|
||||
* Database Handle
|
||||
* @var Db
|
||||
*/
|
||||
private $oDb;
|
||||
|
||||
/**
|
||||
* Media Project
|
||||
* @var Project
|
||||
*/
|
||||
private $oProject;
|
||||
private Db $oDb;
|
||||
private Project $oProject;
|
||||
private $asMedia;
|
||||
private $asMedias;
|
||||
private $sSystemType;
|
||||
//private $sSystemType;
|
||||
|
||||
private $iMediaId;
|
||||
|
||||
public function __construct(Db &$oDb, &$oProject, $iMediaId=0) {
|
||||
public function __construct(Db &$oDb, Project &$oProject, $iMediaId=0) {
|
||||
parent::__construct(__CLASS__);
|
||||
$this->oDb = &$oDb;
|
||||
$this->oProject = &$oProject;
|
||||
$this->asMedia = array();
|
||||
$this->asMedias = array();
|
||||
$this->sSystemType = (substr(php_uname(), 0, 7) == "Windows")?'win':'unix';
|
||||
//$this->sSystemType = (substr(php_uname(), 0, 7) == "Windows")?'win':'unix';
|
||||
$this->setMediaId($iMediaId);
|
||||
}
|
||||
|
||||
|
||||
@@ -142,15 +142,19 @@ class Project extends PhpObject {
|
||||
}
|
||||
$asProject['editable'] = $this->isModeEditable($asProject['mode']);
|
||||
|
||||
if($sCodeName != '' && !Converter::isGeoJsonValid($sCodeName)) Converter::convertToGeoJson($sCodeName);
|
||||
|
||||
$asProject['geofilepath'] = Spot::addTimestampToFilePath(GeoJson::getDistFilePath($sCodeName));
|
||||
//$asProject['geofilepath'] = Spot::addTimestampToFilePath(GeoJson::getDistFilePath($sCodeName));
|
||||
$asProject['gpxfilepath'] = Spot::addTimestampToFilePath(Gpx::getDistFilePath($sCodeName));
|
||||
$asProject['codename'] = $sCodeName;
|
||||
}
|
||||
return $bSpecificProj?$asProject:$asProjects;
|
||||
}
|
||||
|
||||
public function getGeoJson() {
|
||||
if($this->sCodeName != '' && !Converter::isGeoJsonValid($this->sCodeName)) Converter::convertToGeoJson($this->sCodeName);
|
||||
|
||||
return json_decode(file_get_contents(GeoJson::getDistFilePath($this->sCodeName)), true);
|
||||
}
|
||||
|
||||
public function getProject() {
|
||||
return $this->getProjects($this->getProjectId());
|
||||
}
|
||||
@@ -185,7 +189,7 @@ class Project extends PhpObject {
|
||||
$this->sCodeName = $asProject['codename'];
|
||||
$this->sMode = $asProject['mode'];
|
||||
$this->asActive = array('from'=>$asProject['active_from'], 'to'=>$asProject['active_to']);
|
||||
$this->asGeo = array('geofile'=>$asProject['geofilepath'], 'gpxfile'=>$asProject['gpxfilepath']);
|
||||
$this->asGeo = array(/*'geofile'=>$asProject['geofilepath'], */'gpxfile'=>$asProject['gpxfilepath']);
|
||||
}
|
||||
else $this->addError('Error while setting project: no project ID');
|
||||
}
|
||||
|
||||
185
lib/Spot.php
185
lib/Spot.php
@@ -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
|
||||
@@ -148,7 +150,8 @@ class Spot extends Main
|
||||
Project::PROJ_TABLE => "UNIQUE KEY `uni_proj_name` (`codename`)",
|
||||
Media::MEDIA_TABLE => "UNIQUE KEY `uni_file_name` (`filename`)",
|
||||
User::USER_TABLE => "UNIQUE KEY `uni_email` (`email`)",
|
||||
Map::MAP_TABLE => "UNIQUE KEY `uni_map_name` (`codename`)"
|
||||
Map::MAP_TABLE => "UNIQUE KEY `uni_map_name` (`codename`)",
|
||||
Map::MAPPING_TABLE => "default_on_generic_map_only CHECK (`default_map` = 0 OR `id_project` IS NULL)"
|
||||
),
|
||||
'cascading_delete' => array
|
||||
(
|
||||
@@ -160,7 +163,7 @@ class Spot extends Main
|
||||
);
|
||||
}
|
||||
|
||||
public function getAppParams() {
|
||||
public function getAppMainPage() {
|
||||
|
||||
//Cache Page List
|
||||
$asPages = array_diff($this->asMasks, array('email_update', 'email_conf'));
|
||||
@@ -168,34 +171,27 @@ class Spot extends Main
|
||||
$asPages = array_diff($asPages, array('admin', 'upload'));
|
||||
}
|
||||
|
||||
$asGlobalVars = array(
|
||||
'vars' => array(
|
||||
'chunk_size' => self::FEED_CHUNK_SIZE,
|
||||
'default_project_codename' => $this->oProject->getProjectCodeName(),
|
||||
'projects' => $this->oProject->getProjects(),
|
||||
'user' => $this->oUser->getUserInfo()
|
||||
),
|
||||
'consts' => array(
|
||||
'server' => $this->asContext['serv_name'],
|
||||
'modes' => Project::MODES,
|
||||
'clearances' => User::CLEARANCES,
|
||||
'default_timezone' => Settings::TIMEZONE
|
||||
)
|
||||
);
|
||||
|
||||
return self::getJsonResult(true, '', parent::getParams($asGlobalVars, self::MAIN_PAGE, $asPages));
|
||||
}
|
||||
|
||||
public function getAppMainPage()
|
||||
{
|
||||
return parent::getMainPage(
|
||||
array(
|
||||
'vars' => array(
|
||||
'default_project_codename' => $this->oProject->getProjectCodeName(),
|
||||
'projects' => $this->oProject->getProjects(),
|
||||
'user' => $this->oUser->getUserInfo()
|
||||
),
|
||||
'consts' => array(
|
||||
'modes' => Project::MODES,
|
||||
'clearances' => User::CLEARANCES,
|
||||
'default_timezone' => Settings::TIMEZONE,
|
||||
'chunk_size' => self::FEED_CHUNK_SIZE
|
||||
)
|
||||
),
|
||||
self::MAIN_PAGE,
|
||||
array(
|
||||
'language' => $this->oLang->getLanguage(),
|
||||
'host_url' => $this->asContext['serv_name'],
|
||||
'filepath_css' => self::addTimestampToFilePath('spot.css'),
|
||||
'filepath_js' => self::addTimestampToFilePath('../dist/app.js')
|
||||
)
|
||||
'filepath_js' => self::addTimestampToFilePath('../dist/app.js'),
|
||||
),
|
||||
$asPages
|
||||
);
|
||||
}
|
||||
|
||||
@@ -209,6 +205,10 @@ class Spot extends Main
|
||||
$this->oProject->setProjectId($iProjectId);
|
||||
}
|
||||
|
||||
public function getProjectGeoJson() {
|
||||
return self::getJsonResult(true, '', $this->oProject->getGeoJson());
|
||||
}
|
||||
|
||||
public function updateProject() {
|
||||
$bNewMsg = false;
|
||||
$bSuccess = true;
|
||||
@@ -223,34 +223,7 @@ class Spot extends Main
|
||||
|
||||
//Send Update Email
|
||||
if($bNewMsg) {
|
||||
$oEmail = new Email($this->asContext['serv_name'], 'email_update');
|
||||
$oEmail->setDestInfo($this->oUser->getActiveUsersInfo());
|
||||
|
||||
//Add Position
|
||||
$asLastMessage = array_shift($this->getSpotMessages(array($this->oProject->getLastMessageId($this->getFeedConstraints(Feed::MSG_TABLE)))));
|
||||
$oEmail->oTemplate->setTags($asLastMessage);
|
||||
$oEmail->oTemplate->setTag('date_time', 'time:'.$asLastMessage['unix_time'], 'd/m/Y, H:i');
|
||||
|
||||
//Add latest news feed
|
||||
$asNews = $this->getNextFeed(0, true);
|
||||
$iPostCount = 0;
|
||||
foreach($asNews as $asPost) {
|
||||
if($asPost['type'] != 'message') {
|
||||
$oEmail->oTemplate->newInstance('news');
|
||||
$oEmail->oTemplate->setInstanceTags('news', array(
|
||||
'local_server' => $this->asContext['serv_name'],
|
||||
'project' => $this->oProject->getProjectCodeName(),
|
||||
'type' => $asPost['type'],
|
||||
'id' => $asPost['id_'.$asPost['type']])
|
||||
);
|
||||
$oEmail->oTemplate->addInstance($asPost['type'], $asPost);
|
||||
$oEmail->oTemplate->setInstanceTag($asPost['type'], 'local_server', $this->asContext['serv_name']);
|
||||
$iPostCount++;
|
||||
}
|
||||
if($iPostCount == self::MAIL_CHUNK_SIZE) break;
|
||||
}
|
||||
|
||||
$bSuccess = $oEmail->send();
|
||||
$bSuccess = $this->sendEmail();
|
||||
$sDesc = $bSuccess?'mail_sent':'mail_failure';
|
||||
}
|
||||
else $sDesc = 'no_new_msg';
|
||||
@@ -258,6 +231,37 @@ class Spot extends Main
|
||||
return self::getJsonResult($bSuccess, $sDesc);
|
||||
}
|
||||
|
||||
private function sendEmail() {
|
||||
$oEmail = new Email($this->asContext['serv_name'], 'email_update');
|
||||
$oEmail->setDestInfo($this->oUser->getActiveUsersInfo());
|
||||
|
||||
//Add Position
|
||||
$asLastMessage = array_shift($this->getSpotMessages(array($this->oProject->getLastMessageId($this->getFeedConstraints(Feed::MSG_TABLE)))));
|
||||
$oEmail->oTemplate->setTags($asLastMessage);
|
||||
$oEmail->oTemplate->setTag('date_time', 'time:'.$asLastMessage['unix_time'], 'd/m/Y, H:i');
|
||||
|
||||
//Add latest news feed
|
||||
$asNews = $this->getNextFeed(0, true);
|
||||
$iPostCount = 0;
|
||||
foreach($asNews as $asPost) {
|
||||
if($asPost['type'] != 'message') {
|
||||
$oEmail->oTemplate->newInstance('news');
|
||||
$oEmail->oTemplate->setInstanceTags('news', array(
|
||||
'local_server' => $this->asContext['serv_name'],
|
||||
'project' => $this->oProject->getProjectCodeName(),
|
||||
'type' => $asPost['type'],
|
||||
'id' => $asPost['id_'.$asPost['type']])
|
||||
);
|
||||
$oEmail->oTemplate->addInstance($asPost['type'], $asPost);
|
||||
$oEmail->oTemplate->setInstanceTag($asPost['type'], 'local_server', $this->asContext['serv_name']);
|
||||
$iPostCount++;
|
||||
}
|
||||
if($iPostCount == self::MAIL_CHUNK_SIZE) break;
|
||||
}
|
||||
|
||||
return $oEmail->send();
|
||||
}
|
||||
|
||||
public function genCronFile() {
|
||||
//$bSuccess = (file_put_contents('spot_cron.sh', '#!/bin/bash'."\n".'cd '.dirname($_SERVER['SCRIPT_FILENAME'])."\n".'php -f index.php a=update_feed')!==false);
|
||||
$sFileName = 'spot_cron.sh';
|
||||
@@ -632,6 +636,19 @@ class Spot extends Main
|
||||
return self::getJsonResult($asResult['result'], $asResult['desc'], $asResult['data']);
|
||||
}
|
||||
|
||||
public function addPosition($sLat, $sLng, $iTimestamp) {
|
||||
$oFeed = new Feed($this->oDb, $this->oProject->getFeedIds()[0]);
|
||||
$bSuccess = ($oFeed->addManualPosition($sLat, $sLng, $iTimestamp) > 0);
|
||||
|
||||
if($bSuccess) {
|
||||
$bSuccess = $this->sendEmail();
|
||||
$sDesc = $bSuccess?'mail_sent':'mail_failure';
|
||||
}
|
||||
else $sDesc = 'error_commit_db';
|
||||
|
||||
return self::getJsonResult($bSuccess, $sDesc);
|
||||
}
|
||||
|
||||
public function getAdminSettings($sType='') {
|
||||
$oFeed = new Feed($this->oDb);
|
||||
$asData = array(
|
||||
@@ -654,6 +671,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);
|
||||
@@ -712,38 +731,68 @@ 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 function buildGeoJSON($sCodeName) {
|
||||
return Converter::convertToGeoJson($sCodeName);
|
||||
}
|
||||
|
||||
public static function decToDms($dValue, $sType) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
114
lib/User.php
114
lib/User.php
@@ -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);
|
||||
|
||||
@@ -26,6 +26,9 @@ $oValue = $_REQUEST['value'] ?? '';
|
||||
$iId = $_REQUEST['id'] ?? 0 ;
|
||||
$sType = $_REQUEST['type'] ?? '';
|
||||
$sEmail = $_REQUEST['email'] ?? '';
|
||||
$sLat = $_REQUEST['latitude'] ?? '';
|
||||
$sLng = $_REQUEST['longitude'] ?? '';
|
||||
$iTimestamp = $_REQUEST['timestamp'] ?? 0;
|
||||
|
||||
//Initiate class
|
||||
$oSpot = new Spot(__FILE__, $sTimezone);
|
||||
@@ -36,12 +39,12 @@ if($sAction!='')
|
||||
{
|
||||
switch($sAction)
|
||||
{
|
||||
case 'params':
|
||||
$sResult = $oSpot->getAppParams();
|
||||
break;
|
||||
case 'markers':
|
||||
$sResult = $oSpot->getMarkers();
|
||||
break;
|
||||
case 'geojson':
|
||||
$sResult = $oSpot->getProjectGeoJson();
|
||||
break;
|
||||
case 'next_feed':
|
||||
$sResult = $oSpot->getNextFeed($iId);
|
||||
break;
|
||||
@@ -74,8 +77,8 @@ if($sAction!='')
|
||||
case 'add_comment':
|
||||
$sResult = $oSpot->addComment($iId, $sContent);
|
||||
break;
|
||||
case 'admin_new':
|
||||
$sResult = $oSpot->createProject();
|
||||
case 'add_position':
|
||||
$sResult = $oSpot->addPosition($sLat, $sLng, $iTimestamp);
|
||||
break;
|
||||
case 'admin_get':
|
||||
$sResult = $oSpot->getAdminSettings();
|
||||
@@ -83,8 +86,11 @@ if($sAction!='')
|
||||
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();
|
||||
@@ -92,6 +98,9 @@ if($sAction!='')
|
||||
case 'sql':
|
||||
$sResult = $oSpot->getDbBuildScript();
|
||||
break;
|
||||
case 'build_geojson':
|
||||
$sResult = $oSpot->buildGeoJSON($sName);
|
||||
break;
|
||||
default:
|
||||
$sResult = Main::getJsonResult(false, Main::NOT_FOUND);
|
||||
}
|
||||
|
||||
212
masks/admin.html
212
masks/admin.html
@@ -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>
|
||||
1205
masks/project.html
1205
masks/project.html
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
6177
package-lock.json
generated
Normal file
6177
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
47
package.json
Normal file
47
package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.23.9",
|
||||
"@babel/preset-env": "^7.23.9",
|
||||
"babel-loader": "^10.0.0",
|
||||
"resolve-url-loader": "^5.0.0",
|
||||
"symlink-webpack-plugin": "^1.1.0",
|
||||
"vue-loader": "^17.4.2",
|
||||
"vue-template-compiler": "^2.7.16",
|
||||
"webpack": "^5.99.7",
|
||||
"webpack-cli": "^6.0.1"
|
||||
},
|
||||
"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": "^13.0.0",
|
||||
"css-loader": "^7.1.2",
|
||||
"d3": "^7.8.5",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-loader": "^5.0.0",
|
||||
"jquery": "^3.7.1",
|
||||
"jquery-mousewheel": "^3.1.13",
|
||||
"jquery.waitforimages": "^2.4.0",
|
||||
"lightbox2": "^2.11.4",
|
||||
"maplibre-gl": "^5.4.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"sass": "^1.70.0",
|
||||
"sass-loader": "^16.0.5",
|
||||
"simplebar-vue": "^2.3.3",
|
||||
"style-loader": "^4.0.0",
|
||||
"url-loader": "^4.1.1",
|
||||
"vue": "^3.3.8",
|
||||
"vue-style-loader": "^4.1.3"
|
||||
}
|
||||
}
|
||||
28
readme.md
28
readme.md
@@ -1,6 +1,10 @@
|
||||
# Spot Project
|
||||
[Spot](https://www.findmespot.com) & GPX integration
|
||||
|
||||
## Dependencies
|
||||
|
||||
* npm 18+
|
||||
* composer
|
||||
* php-mbstring
|
||||
* php-imagick
|
||||
* php-gd
|
||||
@@ -9,23 +13,31 @@
|
||||
* ffprobe & ffmpeg
|
||||
* STARTTLS Email Server (use Gmail if none available)
|
||||
* Optional: Geo Caching Server (WMTS Caching Service)
|
||||
|
||||
## PHP Configuration
|
||||
|
||||
* max_execution_time = 300
|
||||
* memory_limit = 500M
|
||||
* post_max_size = 4G
|
||||
* upload_max_filesize = 4G
|
||||
* 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 install webpack
|
||||
4. npm run dev
|
||||
5. Update php.ini parameters
|
||||
6. Copy timezone data: mariadb-tzinfo-to-sql /usr/share/zoneinfo | mariadb -u root mysql
|
||||
7. Copy settings-sample.php to settings.php and populate
|
||||
8. Go to #admin and create a new project, feed & maps
|
||||
9. Add a GPX file named <project_codename>.gpx to /geo/
|
||||
|
||||
## To Do List
|
||||
* ECMA import/export
|
||||
|
||||
* Add mail frequency slider
|
||||
* Use WMTS servers directly when not using Geo Caching Server
|
||||
* Allow HEIF picture format
|
||||
* Vector tiles support (https://www.arcgis.com/home/item.html?id=7dc6cea0b1764a1f9af2e679f642f0f5) + Use of GL library. Use Mapbox GL JS / Maplibre GL JS / ESRI-Leaflet-vector?
|
||||
* Fix .MOV playback on windows firefox
|
||||
* Fix .MOV playback on windows firefox
|
||||
* Garmin InReach Integration
|
||||
|
||||
84
src/Spot.vue
Normal file
84
src/Spot.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<script>
|
||||
import Project from './components/project.vue';
|
||||
import Admin from './components/admin.vue';
|
||||
import Upload from './components/upload.vue';
|
||||
|
||||
const aoRoutes = {
|
||||
'project': Project,
|
||||
'admin': Admin,
|
||||
'upload': Upload
|
||||
};
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
hash: {},
|
||||
consts: this.spot.consts,
|
||||
user: this.spot.vars('user')
|
||||
};
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
projects: this.spot.vars('projects'),
|
||||
consts: this.consts,
|
||||
user: this.user
|
||||
};
|
||||
},
|
||||
inject: ['spot'],
|
||||
computed: {
|
||||
page() {
|
||||
this.spot.vars('page', this.hash.page);
|
||||
return aoRoutes[this.hash.page];
|
||||
}
|
||||
},
|
||||
created() {
|
||||
//User
|
||||
this.user.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || this.consts.default_timezone;
|
||||
},
|
||||
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 != '') {
|
||||
if(asHash.page == this.hash.page) this.spot.onSamePageMove(asHash);
|
||||
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="page" />
|
||||
</div>
|
||||
<div id="mobile"></div>
|
||||
</template>
|
||||
235
src/components/admin.vue
Normal file
235
src/components/admin.vue
Normal file
@@ -0,0 +1,235 @@
|
||||
<script>
|
||||
import SpotIcon from './spotIcon.vue';
|
||||
import SpotButton from './spotButton.vue';
|
||||
import AdminInput from './adminInput.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SpotIcon,
|
||||
SpotButton,
|
||||
AdminInput
|
||||
},
|
||||
inject: ['spot'],
|
||||
data() {
|
||||
return {
|
||||
elems: {},
|
||||
feedbacks: []
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.setEvents();
|
||||
this.setProjects();
|
||||
},
|
||||
methods: {
|
||||
l(id) {
|
||||
return this.spot.lang(id);
|
||||
},
|
||||
setEvents() {
|
||||
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});
|
||||
}
|
||||
});
|
||||
},
|
||||
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) => {
|
||||
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) => {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'});});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div id="admin">
|
||||
<a name="back" class="button" href="#project"><SpotIcon :icon="'back'" :text="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 :icon="'close fa-lg'" @click="deleteElem(project)" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<SpotButton :classes="'new'" :text="l('new_project')" :icon="'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 :icon="'close fa-lg'" @click="deleteElem(feed)" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<SpotButton :classes="'new'" :text="l('new_feed')" :icon="'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 :icon="'close fa-lg'" @click="deleteElem(user)" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<h1>{{ l('toolbox') }}</h1>
|
||||
<div id="toolbox">
|
||||
<SpotButton :classes="'refresh'" :text="l('update_project')" :icon="'refresh'" @click="updateProject" />
|
||||
</div>
|
||||
<div id="feedback" class="feedback">
|
||||
<p v-for="feedback in feedbacks" :class="feedback.type">{{ feedback.msg }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
18
src/components/adminInput.vue
Normal file
18
src/components/adminInput.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<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>
|
||||
590
src/components/project.vue
Normal file
590
src/components/project.vue
Normal file
@@ -0,0 +1,590 @@
|
||||
<script>
|
||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
import { Map, NavigationControl, Marker, LngLatBounds, LngLat, Popup } from 'maplibre-gl';
|
||||
import { createApp, defineComponent, nextTick, ref, defineCustomElement, provide, inject } from 'vue';
|
||||
import simplebar from 'simplebar-vue';
|
||||
|
||||
import autosize from 'autosize';
|
||||
import mousewheel from 'jquery-mousewheel';
|
||||
import waitforimages from 'jquery.waitforimages';
|
||||
import lightbox from '../scripts/lightbox.js';
|
||||
|
||||
//import SimpleBar from 'simplebar';
|
||||
|
||||
import SpotIcon from './spotIcon.vue';
|
||||
import SpotButton from './spotButton.vue';
|
||||
import ProjectPost from './projectPost.vue';
|
||||
import ProjectPopup from './projectPopup.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SpotIcon,
|
||||
SpotButton,
|
||||
ProjectPost,
|
||||
ProjectPopup,
|
||||
simplebar
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
server: this.spot.consts.server,
|
||||
feed: {loading:false, updatable:true, outOfData:false, refIdFirst:0, refIdLast:0, firstChunk:true},
|
||||
feedPanelOpen: false,
|
||||
feedSimpleBar: null,
|
||||
settingsPanelOpen: false,
|
||||
markerSize: {width: 32, height: 32},
|
||||
project: {},
|
||||
projectCodename: null,
|
||||
modeHisto: false,
|
||||
posts: [],
|
||||
nlFeedbacks: [],
|
||||
nlLoading: false,
|
||||
baseMaps: {},
|
||||
baseMap: null,
|
||||
messages: null,
|
||||
map: null,
|
||||
hikes: {
|
||||
colors:{'main':'#00ff78', 'off-track':'#0000ff', 'hitchhiking':'#FF7814'},
|
||||
width: 4
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
projectClasses() {
|
||||
return [
|
||||
this.feedPanelOpen?'with-feed':'',
|
||||
this.settingsPanelOpen?'with-settings':''
|
||||
].filter(n => n).join(' ');
|
||||
},
|
||||
nlClasses() {
|
||||
return [
|
||||
this.nlAction,
|
||||
this.nlLoading?'loading':''
|
||||
].filter(n => n).join(' ');
|
||||
},
|
||||
subscribed() {
|
||||
return this.user.id_user > 0;
|
||||
},
|
||||
nlAction() {
|
||||
return this.subscribed?'unsubscribe':'subscribe';
|
||||
},
|
||||
mobile() {
|
||||
return this.spot.isMobile();
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
baseMap(sNewBaseMap, sOldBaseMap) {
|
||||
if(sOldBaseMap) this.map.setLayoutProperty(sOldBaseMap, 'visibility', 'none');
|
||||
if(sNewBaseMap) this.map.setLayoutProperty(sNewBaseMap, 'visibility', 'visible');
|
||||
},
|
||||
projectCodename(sNewCodeName, sOldCodeName) {
|
||||
console.log('change in projectCodename: '+sNewCodeName);
|
||||
//this.toggleSettingsPanel(false);
|
||||
this.$parent.setHash(this.$parent.hash.page, [sNewCodeName]);
|
||||
this.init();
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
project: this.project
|
||||
};
|
||||
},
|
||||
inject: ['spot', 'projects', 'user'],
|
||||
mounted() {
|
||||
this.spot.addPage('project', {
|
||||
onResize: () => {
|
||||
//this.spot.tmp('map_offset', -1 * (this.feedPanelOpen?getOuterWidth(this.$refs.feed):0) / getOuterWidth(window));
|
||||
|
||||
/* TODO
|
||||
if(typeof this.spot.tmp('elev') != 'undefined' && this.spot.tmp('elev')._showState) {
|
||||
this.spot.tmp('elev').resize({width:this.getElevWidth()});
|
||||
}
|
||||
*/
|
||||
}
|
||||
});
|
||||
|
||||
this.projectCodename = (this.$parent.hash.items.length==0)?this.spot.vars('default_project_codename'):this.$parent.hash.items[0];
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
let bFirstLoad = (typeof this.project.codename == 'undefined');
|
||||
this.initProject();
|
||||
if(bFirstLoad) this.initLightbox();
|
||||
this.initFeed();
|
||||
this.initMap();
|
||||
},
|
||||
initProject() {
|
||||
this.project = this.projects[this.projectCodename];
|
||||
this.modeHisto = (this.project.mode == this.spot.consts.modes.histo);
|
||||
this.feed = {loading:false, updatable:true, outOfData:false, refIdFirst:0, refIdLast:0, firstChunk:true};
|
||||
this.posts = [];
|
||||
//this.baseMap = null;
|
||||
this.baseMaps = {};
|
||||
},
|
||||
initLightbox() {
|
||||
lightbox.option({
|
||||
alwaysShowNavOnTouchDevices: true,
|
||||
albumLabel: '<i class="fa fa-fw fa-lg fa-media push"></i> %1 / %2',
|
||||
fadeDuration: 300,
|
||||
imageFadeDuration: 400,
|
||||
positionFromTop: 0,
|
||||
resizeDuration: 400,
|
||||
hasVideo: true,
|
||||
onMediaChange: (oMedia) => {
|
||||
this.spot.updateHash('media', oMedia.id);
|
||||
if(oMedia.set == 'post-medias') this.goToPost({type: 'media', id: oMedia.id});
|
||||
},
|
||||
onClosing: () => {this.spot.flushHash();}
|
||||
});
|
||||
},
|
||||
async initFeed() {
|
||||
//Simplebar event
|
||||
this.$refs.feedSimpleBar.scrollElement.addEventListener('scroll', (oEvent) => {this.onFeedScroll(oEvent);});
|
||||
|
||||
//Mobile Touchscreen Events
|
||||
//TODO
|
||||
|
||||
//Add post Event handling
|
||||
//TODO
|
||||
|
||||
await this.getNextFeed();
|
||||
|
||||
//Scroll to post
|
||||
if(this.$parent.hash.items.length == 3) this.findPost({type: this.$parent.hash.items[1], id: this.$parent.hash.items[2]});
|
||||
},
|
||||
async initMap() {
|
||||
//Get Map Info
|
||||
const aoMarkers = await this.spot.get2('markers', {id_project: this.project.id});
|
||||
this.baseMap = null;
|
||||
this.baseMaps = aoMarkers.maps;
|
||||
this.messages = aoMarkers.messages;
|
||||
|
||||
//Base maps (raster tiles)
|
||||
let asSources = {};
|
||||
let asLayers = [];
|
||||
for(const asBaseMap of this.baseMaps) {
|
||||
asSources[asBaseMap.codename] = {
|
||||
type: 'raster',
|
||||
tiles: [asBaseMap.pattern],
|
||||
tileSize: asBaseMap.tile_size
|
||||
};
|
||||
asLayers.push({
|
||||
id: asBaseMap.codename,
|
||||
type: 'raster',
|
||||
source: asBaseMap.codename,
|
||||
'layout': {'visibility': 'none'},
|
||||
minZoom: asBaseMap.min_zoom,
|
||||
maxZoom: asBaseMap.max_zoom
|
||||
});
|
||||
}
|
||||
|
||||
//Map
|
||||
if(this.map) this.map.remove();
|
||||
this.map = new Map({
|
||||
container: 'map',
|
||||
style: {
|
||||
version: 8,
|
||||
sources: asSources,
|
||||
layers: asLayers
|
||||
},
|
||||
attributionControl: false
|
||||
});
|
||||
|
||||
this.map.once('load', async () => {
|
||||
//Default Basemap
|
||||
this.baseMap = this.baseMaps.filter((asBM) => asBM.default_map)[0].codename;
|
||||
|
||||
//Get track
|
||||
const oTrack = await this.spot.get2('geojson', {id_project: this.project.id});
|
||||
this.map.addSource('track', {
|
||||
'type': 'geojson',
|
||||
'data': oTrack
|
||||
});
|
||||
|
||||
//Color mapping
|
||||
let asColorMapping = ['match', ['get', 'type']];
|
||||
for(const sHikeType in this.hikes.colors) {
|
||||
asColorMapping.push(sHikeType);
|
||||
asColorMapping.push(this.hikes.colors[sHikeType]);
|
||||
}
|
||||
asColorMapping.push('black'); //fallback value
|
||||
|
||||
//Track layer
|
||||
this.map.addLayer({
|
||||
'id': 'track',
|
||||
'type': 'line',
|
||||
'source': 'track',
|
||||
'layout': {
|
||||
'line-join': 'round',
|
||||
'line-cap': 'round'
|
||||
},
|
||||
'paint': {
|
||||
'line-color': asColorMapping,
|
||||
'line-width': this.hikes.width
|
||||
}
|
||||
});
|
||||
|
||||
//Markers
|
||||
let aoMarkerSource = {type:'geojson', data:{type: 'FeatureCollection', features: []}};
|
||||
for(const oMsg of this.messages) {
|
||||
aoMarkerSource.data.features.push({
|
||||
'type': 'Feature',
|
||||
'properties': {
|
||||
...oMsg,
|
||||
...{'description': ''}
|
||||
},
|
||||
'geometry': {
|
||||
'type': 'Point',
|
||||
'coordinates': [oMsg.longitude, oMsg.latitude]
|
||||
}
|
||||
});
|
||||
//Tooltip
|
||||
/*
|
||||
let $Tooltip = $($('<div>', {'class':'info-window'})
|
||||
.append($('<h1>')
|
||||
.addIcon('fa-message fa-lg', true)
|
||||
.append($('<span>').text(this.spot.lang('post_message')+' '+this.spot.lang('counter', oMsg.displayed_id)))
|
||||
.append($('<span>', {'class':'message-type'}).text('('+oMsg.type+')'))
|
||||
)
|
||||
.append($('<div>', {'class':'separator'}))
|
||||
.append($('<p>', {'class':'coordinates'})
|
||||
.addIcon('fa-coords fa-fw fa-lg', true)
|
||||
.append(this.getGoogleMapsLink(oMsg))
|
||||
)
|
||||
.append($('<p>', {'class':'time'})
|
||||
.addIcon('fa-time fa-fw fa-lg', true)
|
||||
.append(oMsg.formatted_time+(this.project.mode==this.spot.consts.modes.blog?' ('+oMsg.relative_time+')':''))))[0];
|
||||
|
||||
const vTooltip = h(SpotIcon, {icon:'project', 'classes':'fa-fw', text:'hikes'});
|
||||
|
||||
//let vTooltip = h(SpotIcon, {icon:'project', 'classes':'fa-fw', text:'hikes'});
|
||||
|
||||
oPopup.setDOMContent(vTooltip);
|
||||
|
||||
new Marker({
|
||||
element: $('<div style="width:'+this.markerSize.width+'px;height:'+this.markerSize.height+'px;"><span class="fa-stack"><i class="fa fa-message fa-stack-2x"></i><i class="fa fa-message-in fa-rotate-270 fa-stack-1x"></i></span></div>')[0],
|
||||
anchor: 'bottom'
|
||||
})
|
||||
.setLngLat(new LngLat(oMsg.longitude, oMsg.latitude))
|
||||
.setPopup(oPopup)
|
||||
.addTo(this.map)
|
||||
;
|
||||
*/
|
||||
}
|
||||
this.map.addSource('markers', aoMarkerSource);
|
||||
const image = await this.map.loadImage('https://maplibre.org/maplibre-gl-js/docs/assets/custom_marker.png');
|
||||
this.map.addImage('markerIcon', image.data);
|
||||
this.map.addLayer({
|
||||
'id': 'markers',
|
||||
'type': 'symbol',
|
||||
'source': 'markers',
|
||||
'layout': {
|
||||
//'icon-anchor': 'bottom',
|
||||
'icon-image': 'markerIcon'
|
||||
//'icon-overlap': 'always'
|
||||
}
|
||||
});
|
||||
this.map.on("click", "markers", (e) => {
|
||||
var oPopup = new Popup({
|
||||
anchor: 'bottom',
|
||||
offset: [0, this.markerSize.height * -1],
|
||||
closeButton: false
|
||||
})
|
||||
.setHTML('<div id="popup"></div>')
|
||||
.setLngLat(e.lngLat)
|
||||
.addTo(this.map);
|
||||
|
||||
let rProp = ref(e.features[0].properties);
|
||||
const vPopup = defineComponent({
|
||||
extends: ProjectPopup,
|
||||
setup: () => {
|
||||
console.log(rProp.value);
|
||||
provide('options', rProp.value);
|
||||
provide('spot', this.spot);
|
||||
provide('project', this.project);
|
||||
return {'options': rProp.value, 'spot':this.spot, 'project':this.project};
|
||||
}
|
||||
});
|
||||
nextTick(() => {
|
||||
createApp(vPopup).mount("#popup");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
//Centering map
|
||||
let bOpenFeedPanel = !this.mobile;
|
||||
let oBounds = new LngLatBounds();
|
||||
if(
|
||||
this.project.mode == this.spot.consts.modes.blog &&
|
||||
this.messages.length > 0 &&
|
||||
this.$parent.hash.items[2] != 'message'
|
||||
) {
|
||||
//Fit to last message
|
||||
let oLastMsg = this.messages[this.messages.length - 1];
|
||||
oBounds.extend(new LngLat(oLastMsg.longitude, oLastMsg.latitude));
|
||||
}
|
||||
else {
|
||||
//Fit to track
|
||||
for(const iFeatureId in oTrack.features) {
|
||||
oBounds = oTrack.features[iFeatureId].geometry.coordinates.reduce(
|
||||
(bounds, coord) => {
|
||||
return bounds.extend(coord);
|
||||
},
|
||||
oBounds
|
||||
);
|
||||
}
|
||||
}
|
||||
const iFeedPanelPadding = bOpenFeedPanel?(getOuterWidth(this.$refs.feed)/2):0;
|
||||
await this.map.fitBounds(
|
||||
oBounds,
|
||||
{
|
||||
padding: {
|
||||
top: 20,
|
||||
bottom: 20,
|
||||
left: (20 + iFeedPanelPadding),
|
||||
right: (20 + iFeedPanelPadding)
|
||||
},
|
||||
animate: false,
|
||||
maxZoom: 15
|
||||
}
|
||||
);
|
||||
|
||||
//Toggle only when map is ready, for the tilt effet
|
||||
this.toggleFeedPanel(bOpenFeedPanel);
|
||||
});
|
||||
|
||||
this.map.on('idle', () => {
|
||||
|
||||
});
|
||||
|
||||
//Legend
|
||||
|
||||
|
||||
|
||||
},
|
||||
getGoogleMapsLink(asInfo) {
|
||||
return $('<a>', {
|
||||
href:'https://www.google.com/maps/place/'+asInfo.lat_dms+'+'+asInfo.lon_dms+'/@'+asInfo.latitude+','+asInfo.longitude+',10z',
|
||||
title: this.spot.lang('see_on_google'),
|
||||
target: '_blank',
|
||||
rel: 'noreferrer noopener'
|
||||
}).text(asInfo.lat_dms+' '+asInfo.lon_dms);
|
||||
},
|
||||
async getNextFeed() {
|
||||
if(!this.feed.outOfData && !this.feed.loading) {
|
||||
//Get next chunk
|
||||
this.feed.loading = true;
|
||||
let aoData = await this.spot.get2('next_feed', {id_project: this.project.id, id: this.feed.refIdLast});
|
||||
let iPostCount = Object.keys(aoData.feed).length;
|
||||
this.feed.loading = false;
|
||||
this.feed.firstChunk = false;
|
||||
|
||||
//Update pointers
|
||||
this.feed.outOfData = (iPostCount < this.spot.consts.chunk_size);
|
||||
if(iPostCount > 0) {
|
||||
this.feed.refIdLast = aoData.ref_id_last;
|
||||
if(this.feed.firstChunk) this.feed.refIdFirst = aoData.ref_id_first;
|
||||
}
|
||||
|
||||
//Add posts
|
||||
this.posts.push(...aoData.feed);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
onFeedScroll(oEvent) {
|
||||
//FIXME remvove jquery dependency
|
||||
var $Box = $(oEvent.currentTarget);
|
||||
var $BoxContent = $Box.find('.simplebar-content');
|
||||
if(($Box.scrollTop() + $(window).height()) / $BoxContent.height() >= 0.8) this.getNextFeed();
|
||||
},
|
||||
async manageSubs() {
|
||||
var regexEmail = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
if(!regexEmail.test(this.user.email)) this.nlFeedbacks.push({type:'error', 'msg':this.spot.lang('nl_invalid_email')});
|
||||
else {
|
||||
this.spot.get2(this.nlAction, {'email': this.user.email, 'name': this.user.name}, this.nlLoading)
|
||||
.then((asUser, sDesc) => {
|
||||
this.nlFeedbacks.push('success', sDesc);
|
||||
this.user = asUser;
|
||||
})
|
||||
.catch((sDesc) => {this.nlFeedbacks.push('error', sDesc);});
|
||||
}
|
||||
},
|
||||
toggleFeedPanel(bShow, sMapAction) {
|
||||
let bOldValue = this.feedPanelOpen;
|
||||
this.feedPanelOpen = (typeof bShow === 'object' || typeof bShow === 'undefined')?(!this.feedPanelOpen):bShow;
|
||||
|
||||
if(bOldValue != this.feedPanelOpen && !this.mobile) {
|
||||
this.spot.onResize();
|
||||
|
||||
sMapAction = sMapAction || 'panTo';
|
||||
switch(sMapAction) {
|
||||
case 'none':
|
||||
break;
|
||||
case 'panTo':
|
||||
this.map.panBy(
|
||||
[(this.feedPanelOpen?1:-1) * getOuterWidth(this.$refs.feed) / 2, 0],
|
||||
{duration: 500}
|
||||
);
|
||||
break;
|
||||
case 'panToInstant':
|
||||
this.map.panBy([(this.feedPanelOpen?1:-1) * getOuterWidth(this.$refs.feed) / 2, 0]);
|
||||
break;
|
||||
case 'fitBounds':
|
||||
/*
|
||||
this.map.fitBounds(
|
||||
this.spot.tmp('track').getBounds(),
|
||||
{
|
||||
paddingTopLeft: L.point(5, this.spot.tmp('marker_size').height + 5),
|
||||
paddingBottomRight: L.point(this.spot.tmp('$Feed').outerWidth(true) + 5, 5)
|
||||
}
|
||||
);
|
||||
break;
|
||||
*/
|
||||
}
|
||||
}
|
||||
},
|
||||
toggleSettingsPanel(bShow, sMapAction) {
|
||||
let bOldValue = this.settingsPanelOpen;
|
||||
this.settingsPanelOpen = (typeof bShow === 'object' || typeof bShow === 'undefined')?(!this.settingsPanelOpen):bShow;
|
||||
|
||||
if(bOldValue != this.settingsPanelOpen && !this.mobile) {
|
||||
this.spot.onResize();
|
||||
|
||||
sMapAction = sMapAction || 'panTo';
|
||||
switch(sMapAction) {
|
||||
case 'none':
|
||||
break;
|
||||
case 'panTo':
|
||||
this.map.panBy(
|
||||
[(this.settingsPanelOpen?-1:1) * getOuterWidth(this.$refs.settings) / 2, 0],
|
||||
{duration: 500}
|
||||
);
|
||||
break;
|
||||
case 'panToInstant':
|
||||
this.map.panBy([(this.settingsPanelOpen?-1:1) * getOuterWidth(this.$refs.settings) /2, 0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
async findPost(oPost) {
|
||||
if(this.goToPost(oPost)) {
|
||||
//if(oPost.type=='media' || oPost.type=='message') $Post.find('a.drill').click();
|
||||
}
|
||||
else if(!this.feed.outOfData) {
|
||||
await this.getNextFeed();
|
||||
this.findPost(oPost);
|
||||
}
|
||||
else console.log('Missing element ID "'+oPost.id+'" of type "'+oPost.type+'"');
|
||||
},
|
||||
goToPost(oPost) {
|
||||
//TODO remove jquery deps
|
||||
let bFound = false;
|
||||
let aoRefs = this.$refs.posts.filter((post)=>{return post.postId == oPost.type+'-'+oPost.id;});
|
||||
if(aoRefs.length == 1) {
|
||||
this.$refs.feedSimpleBar.scrollElement.scrollTop += Math.round(
|
||||
$(aoRefs[0].$el).offset().top
|
||||
- parseInt($(this.$refs.feedSimpleBar.$el).css('padding-top'))
|
||||
);
|
||||
bFound = true;
|
||||
this.spot.flushHash(['post', 'message']);
|
||||
}
|
||||
|
||||
return bFound;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="projects" :class="projectClasses">
|
||||
<div id="background"></div>
|
||||
<div id="submap">
|
||||
<div class="loader fa fa-fw fa-map flicker"></div>
|
||||
</div>
|
||||
<div id="map"></div>
|
||||
<div id="settings" class="map-container map-container-left" ref="settings">
|
||||
<div id="settings-panel" class="map-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">
|
||||
<simplebar id="settings-sections-scrollbox">
|
||||
<div class="settings-section">
|
||||
<h1><SpotIcon :icon="'project'" :classes="'fa-fw'" :text="spot.lang('hikes')" /></h1>
|
||||
<div class="settings-section-body">
|
||||
<div class="radio" v-for="project in projects">
|
||||
<input type="radio" :id="project.id" :value="project.codename" v-model="projectCodename" />
|
||||
<label :for="project.id">
|
||||
<span>{{ project.name }}</span>
|
||||
<a class="download" :href="project.gpxfilepath" :title="spot.lang('track_download')" @click.stop="()=>{}">
|
||||
<SpotIcon :icon="'download'" :classes="'push-left'" />
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<h1><SpotIcon :icon="'map'" :classes="'fa-fw'" :text="spot.lang('maps')" /></h1>
|
||||
<div class="settings-section-body">
|
||||
<div class="radio" v-for="bm in baseMaps">
|
||||
<input type="radio" :id="bm.id_map" :value="bm.codename" v-model="baseMap" />
|
||||
<label :for="bm.id_map">{{ this.spot.lang('map_'+bm.codename) }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-section newsletter">
|
||||
<h1><SpotIcon :icon="'newsletter'" :classes="'fa-fw'" :text="spot.lang('newsletter')" /></h1>
|
||||
<input type="email" name="email" id="email" :placeholder="spot.lang('nl_email_placeholder')" v-model="user.email" :disabled="nlLoading || subscribed" />
|
||||
<SpotButton id="nl_btn" :classes="nlClasses" :title="spot.lang('nl_'+nlAction)" @click="manageSubs" />
|
||||
<div id="settings-feedback" class="feedback">
|
||||
<p v-for="feedback in nlFeedbacks" :class="feedback.type">
|
||||
<SpotIcon :icon="feedback.type" :text="feedback.msg" />
|
||||
</p>
|
||||
</div>
|
||||
{{ spot.lang(subscribed?'nl_subscribed_desc':'nl_unsubscribed_desc') }}
|
||||
</div>
|
||||
<div class="settings-section admin" v-if="spot.checkClearance(spot.consts.clearances.admin)">
|
||||
<h1><SpotIcon :icon="'admin fa-fw'" :text="spot.lang('admin')" /></h1>
|
||||
<a class="button" href="#admin"><SpotIcon :icon="'config'" :text="spot.lang('admin_config')" /></a>
|
||||
<a class="button" href="#upload"><SpotIcon :icon="'upload'" :text="spot.lang('admin_upload')" /></a>
|
||||
</div>
|
||||
</simplebar>
|
||||
</div>
|
||||
<div class="settings-footer">
|
||||
<a href="https://git.lutran.fr/franzz/spot" :title="spot.lang('credits_git')" target="_blank" rel="noopener">
|
||||
<SpotIcon :icon="'credits'" :text="spot.lang('credits_project')" />
|
||||
</a> {{ spot.lang('credits_license') }}</div>
|
||||
</div>
|
||||
<div :class="'map-control map-control-icon settings-control map-control-'+(mobile?'bottom':'top')" @click="toggleSettingsPanel">
|
||||
<SpotIcon :icon="settingsPanelOpen?'prev':'menu'" />
|
||||
</div>
|
||||
<div v-if="!mobile" id="legend" class="map-control settings-control map-control-bottom">
|
||||
<div v-for="(color, hikeType) in hikes.colors" class="track">
|
||||
<span class="line" :style="'background-color:'+color+'; height:'+hikes.width+'px;'"></span>
|
||||
<span class="desc">{{ spot.lang('track_'+hikeType) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="title" :class="'map-control settings-control map-control-'+(mobile?'bottom':'top')">
|
||||
<span>{{ project.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="feed" class="map-container map-container-right" ref="feed">
|
||||
<simplebar id="feed-panel" class="map-panel" ref="feedSimpleBar">
|
||||
<div id="feed-header">
|
||||
<ProjectPost v-if="modeHisto" :options="{type: 'archived', headerless: true}" />
|
||||
<ProjectPost v-else :options="{type: 'poster', relative_time: spot.lang('post_new_message')}" />
|
||||
</div>
|
||||
<div id="feed-posts">
|
||||
<ProjectPost v-for="post in posts" :options="post" ref="posts" />
|
||||
</div>
|
||||
<div id="feed-footer" v-if="feed.loading">
|
||||
<ProjectPost :options="{type: 'loading', headerless: true}" />
|
||||
</div>
|
||||
</simplebar>
|
||||
<div :class="'map-control map-control-icon feed-control map-control-'+(mobile?'bottom':'top')" @click="toggleFeedPanel">
|
||||
<SpotIcon :icon="feedPanelOpen?'next':'post'" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
17
src/components/projectMapLink.vue
Normal file
17
src/components/projectMapLink.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
options: Object
|
||||
},
|
||||
inject: ['spot']
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a
|
||||
:href="'https://www.google.com/maps/place/'+options.lat_dms+'+'+options.lon_dms+'/@'+options.latitude+','+options.longitude+',10z'"
|
||||
:title="spot.lang('see_on_google')"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>{{ options.lat_dms+' '+options.lon_dms }}</a>
|
||||
</template>
|
||||
60
src/components/projectMediaLink.vue
Normal file
60
src/components/projectMediaLink.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<script>
|
||||
import spotIcon from './spotIcon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
spotIcon
|
||||
},
|
||||
props: {
|
||||
options: Object,
|
||||
type: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title:''
|
||||
}
|
||||
},
|
||||
inject: ['spot'],
|
||||
mounted() {
|
||||
this.title =
|
||||
(this.$refs.comment?this.$refs.comment.outerHTML:'') +
|
||||
this.$refs[this.type=='marker'?'takenon':'postedon'].outerHTML +
|
||||
this.$refs[this.type=='marker'?'postedon':'takenon'].outerHTML
|
||||
;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a
|
||||
class="media-link drill"
|
||||
:href="options.media_path"
|
||||
:data-lightbox="type+'-medias'"
|
||||
:data-type="options.subtype"
|
||||
:data-id="options.id_media"
|
||||
:data-title="title"
|
||||
:data-orientation="options.rotate"
|
||||
>
|
||||
<img
|
||||
:src="options.thumb_path"
|
||||
:width="options.width"
|
||||
:height="options.height"
|
||||
:title="spot.lang((options.subtype == 'video')?'click_watch':'click_zoom')"
|
||||
class="clickable"
|
||||
/>
|
||||
<span class="drill-icon"><spotIcon :icon="'drill-'+options.subtype" /></span>
|
||||
<span v-if="options.comment" class="comment">{{ options.comment }}</span>
|
||||
</a>
|
||||
<div style="display:none">
|
||||
<span ref="comment" class="lb-caption-line comment desktop" :title="options.comment">
|
||||
<spotIcon :icon="'post'" :classes="'fa-lg fa-fw'" />
|
||||
<span class="comment-text">{{ options.comment }}</span>
|
||||
</span>
|
||||
<span ref="postedon" class="lb-caption-line" :title="$parent.timeDiff?spot.lang('local_time', options.posted_on_formatted_local):''">
|
||||
<spotIcon :icon="'upload'" :classes="'fa-lg fa-fw'" :text="options.posted_on_formatted" />
|
||||
</span>
|
||||
<span ref="takenon" class="lb-caption-line" :title="$parent.timeDiff?spot.lang('local_time', options.taken_on_formatted_local):''">
|
||||
<spotIcon :icon="options.subtype+'-shot'" :classes="'fa-lg fa-fw'" :text="options.taken_on_formatted" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
50
src/components/projectPopup.vue
Normal file
50
src/components/projectPopup.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script>
|
||||
import { options } from 'lightbox2';
|
||||
import projectMapLink from './projectMapLink.vue';
|
||||
import spotIcon from './spotIcon.vue';
|
||||
import projectRelTime from './projectRelTime.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
spotIcon,
|
||||
projectMapLink,
|
||||
projectRelTime
|
||||
},
|
||||
//props: {
|
||||
// options: Object,
|
||||
//},
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
//inject: ['options', 'spot', 'project'],
|
||||
mounted() {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="info-window">
|
||||
<h1>
|
||||
<spotIcon :icon="'message'" :classes="'fa-lg'" :text="spot.lang('post_message')+' '+spot.lang('counter', options.displayed_id)" />
|
||||
<span class="message-type">({{ options.type }})</span>
|
||||
</h1>
|
||||
<div class="separator"></div>
|
||||
<p class="coordinates">
|
||||
<spotIcon :icon="'coords'" :classes="'fa-fw fa-lg'" :margin="true" />
|
||||
<projectMapLink :options="options" />
|
||||
</p>
|
||||
<p class="time">
|
||||
<spotIcon :icon="'time'" :classes="'fa-fw fa-lg'" :text="options.formatted_time" />
|
||||
<span v-if="project.mode==spot.consts.modes.blog"> ({{ options.relative_time }})</span>
|
||||
</p>
|
||||
<p class="timezone" v-if="options.day_offset != '0'">
|
||||
<spotIcon :icon="'timezone'" :classes="'fa-fw fa-lg'" :margin="true" />
|
||||
<projectRelTime :localTime="options.formatted_time_local" :offset="options.day_offset" />
|
||||
</p>
|
||||
<p class="weather" v-if="options.weather_icon && options.weather_icon!='unknown'" :title="options.weather_cond==''?'':spot.lang(options.weather_cond)">
|
||||
<spotIcon :icon="options.weather_icon" :classes="'fa-fw fa-lg'" :text="options.weather_temp+'°C'" />
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
170
src/components/projectPost.vue
Normal file
170
src/components/projectPost.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<script>
|
||||
import spotIcon from './spotIcon.vue';
|
||||
import spotButton from './spotButton.vue';
|
||||
import projectMediaLink from './projectMediaLink.vue';
|
||||
import projectMapLink from './projectMapLink.vue';
|
||||
import projectRelTime from './projectRelTime.vue';
|
||||
|
||||
import autosize from 'autosize';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
spotIcon,
|
||||
spotButton,
|
||||
projectMediaLink,
|
||||
projectMapLink,
|
||||
projectRelTime
|
||||
},
|
||||
props: {
|
||||
options: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mouseOverHeader: false,
|
||||
absTime: this.options.formatted_time,
|
||||
absTimeLocal: this.options.formatted_time_local,
|
||||
timeDiff: (this.options.formatted_time && this.options.formatted_time_local != this.options.formatted_time),
|
||||
anchorVisible: ['message', 'media', 'post'].includes(this.options.type),
|
||||
anchorTitle: this.spot.lang('copy_to_clipboard'),
|
||||
anchorIcon: 'link'
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
postClass() {
|
||||
let sHeaderLess = this.options.headerless?' headerless':'';
|
||||
return 'post-item '+this.options.type+sHeaderLess;
|
||||
},
|
||||
postId() {
|
||||
return this.options.id?(this.options.type+'-'+this.options.id):'';
|
||||
},
|
||||
subType() {
|
||||
return this.options.subtype || this.options.type;
|
||||
},
|
||||
displayedId() {
|
||||
return this.options.displayed_id?(this.spot.lang('counter', this.options.displayed_id)):'';
|
||||
},
|
||||
hash() {
|
||||
let asHash = this.spot.getHash();
|
||||
return '#'+[asHash.page, asHash.items[0], this.options.type, this.options.id].join(this.spot.consts.hash_sep);
|
||||
},
|
||||
modeHisto() {
|
||||
return (this.project.mode==this.spot.consts.modes.histo);
|
||||
},
|
||||
relTime() {
|
||||
return this.modeHisto?(this.options.formatted_time || '').substr(0, 10):this.options.relative_time;
|
||||
},
|
||||
|
||||
},
|
||||
inject: ['spot', 'project', 'user'],
|
||||
methods: {
|
||||
copyAnchor() {
|
||||
copyTextToClipboard(this.spot.consts.server+this.spot.hash());
|
||||
this.anchorTitle = this.spot.lang('link_copied');
|
||||
this.anchorIcon = 'copied';
|
||||
setTimeout(()=>{ //TODO animation
|
||||
this.anchorTitle = this.spot.lang('copy_to_clipboard');
|
||||
this.anchorIcon = 'link';
|
||||
}, 5000);
|
||||
},
|
||||
panMapToMessage() {
|
||||
//TODO
|
||||
/*
|
||||
var $Parent = $(oEvent.currentTarget).parent();
|
||||
var oMarker = this.spot.tmp(['markers', $Parent.data('id')]);
|
||||
if(this.isMobile()) {
|
||||
this.toggleFeedPanel(false, 'panToInstant');
|
||||
this.spot.tmp('map').setView(oMarker.getLatLng(), 15);
|
||||
}
|
||||
else {
|
||||
var iOffset = (this.isFeedPanelOpen()?1:-1)*this.spot.tmp('$Feed').outerWidth(true)/2 - (this.isSettingsPanelOpen()?1:-1)*this.spot.tmp('$Settings').outerWidth(true)/2;
|
||||
var iRatio = -1 * iOffset / $('body').outerWidth(true);
|
||||
this.spot.tmp('map').setOffsetView(iRatio, oMarker.getLatLng(), 15);
|
||||
}
|
||||
|
||||
$Parent.data('clicked', true);
|
||||
if(!oMarker.isPopupOpen()) oMarker.openPopup();
|
||||
*/
|
||||
},
|
||||
openMarkerPopup() {
|
||||
//TODO
|
||||
/*
|
||||
let oMarker = this.spot.tmp(['markers', $(oEvent.currentTarget).data('id')]);
|
||||
if(this.spot.tmp('map') && this.spot.tmp('map').getBounds().contains(oMarker.getLatLng()) && !oMarker.isPopupOpen()) oMarker.openPopup();
|
||||
*/
|
||||
},
|
||||
closeMarkerPopup() {
|
||||
//TODO
|
||||
/*
|
||||
let $This = $(oEvent.currentTarget);
|
||||
let oMarker = this.spot.tmp(['markers', $This.data('id')]);
|
||||
if(oMarker && oMarker.isPopupOpen() && !$This.data('clicked')) oMarker.closePopup();
|
||||
$This.data('clicked', false);
|
||||
*/
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
//Auto-adjust text area height
|
||||
if(this.options.type == 'poster') autosize(this.$refs.post);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="postClass" :id="postId">
|
||||
<div class="header">
|
||||
<div class="index">
|
||||
<spotIcon :icon="subType" :text="displayedId" />
|
||||
<a v-if="anchorVisible" class="link desktop" @click="copyAnchor" ref="anchor" :href="hash" :title="anchorTitle">
|
||||
<spotIcon :icon="anchorIcon" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="time" @mouseleave="mouseOverHeader = false" @mouseover="mouseOverHeader = true" :title="timeDiff?spot.lang('local_time', absTimeLocal):''">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<span v-if="mouseOverHeader">{{ timeDiff?spot.lang('your_time', absTime):absTime }}</span>
|
||||
<span v-else>{{ relTime }}</span>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div v-if="options.type == 'message'" class="body-box" @mouseenter="openMarkerPopup" @mouseleave="closeMarkerPopup">
|
||||
<p><spotIcon :icon="'coords'" :classes="'push'" /><projectMapLink :options="options" /></p>
|
||||
<p><spotIcon :icon="'time'" :text="absTime" /></p>
|
||||
<p v-if="timeDiff"><spotIcon :icon="'timezone'" :classes="'push'" /><projectRelTime :localTime="absTimeLocal" :offset="options.day_offset" /></p>
|
||||
<a class="drill" @click.prevent="panMapToMessage">
|
||||
<span v-if="options.weather_icon && options.weather_icon!='unknown'" class="weather clickable" :title="spot.lang(options.weather_cond)">
|
||||
<spotIcon :icon="options.weather_icon" />
|
||||
<span>{{ options.weather_temp+'°C' }}</span>
|
||||
</span>
|
||||
<img class="staticmap clickable" :title="spot.lang('click_zoom')" :src="options.static_img_url" />
|
||||
<span class="drill-icon fa-stack clickable">
|
||||
<spotIcon :icon="'message'" :classes="'fa-stack-2x clickable'" />
|
||||
<spotIcon :icon="'message-in'" :classes="'fa-stack-1x fa-rotate-270'" />
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div v-else-if="options.type == 'media'" class="body-box">
|
||||
<projectMediaLink :options="options" :type="'post'" />
|
||||
</div>
|
||||
<div v-else-if="options.type == 'post'">
|
||||
<p class="message">{{ options.content }}</p>
|
||||
<p class="signature">
|
||||
<img v-if="options.gravatar" :src="'data:image/png;base64, '+options.gravatar" width="24" height="24" alt="--" />
|
||||
<span v-else>-- </span>
|
||||
<span>{{ options.formatted_name }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<p v-else-if="options.type == 'poster'" class="message">
|
||||
<textarea ref="post" name="post" :placeholder="spot.lang('post_message')" class="autoExpand" rows="1" v-model="$parent.post"></textarea>
|
||||
<input type="text" name="name" :placeholder="spot.lang('post_name')" v-model="user.name" />
|
||||
<spotButton name="submit" :aria-label="spot.lang('send')" :title="spot.lang('send')" :icon="'send'" />
|
||||
</p>
|
||||
<div v-else-if="options.type == 'archived'">
|
||||
<p><spotIcon :icon="'success'" /></p>
|
||||
<p>{{ spot.lang('mode_histo') }}</p>
|
||||
</div>
|
||||
<div v-else-if="options.type == 'loading'">
|
||||
<p class="flicker"><spotIcon :icon="'post'" /></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
17
src/components/projectRelTime.vue
Normal file
17
src/components/projectRelTime.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
localTime: String,
|
||||
offset: String
|
||||
},
|
||||
inject: ['spot']
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span>
|
||||
<span>{{ localTime.substring(-5) }}</span>
|
||||
<sup v-if="offset != '0'" :title="offset+' '+spot.lang('unit_day')+' ('+localTime.substring(0, 5)+')'">{{ ' '+offset }}</sup>
|
||||
<span> {{ spot.lang('local_time', ' ').trim() }}</span>
|
||||
</span>
|
||||
</template>
|
||||
17
src/components/spotButton.vue
Normal file
17
src/components/spotButton.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
import SpotIcon from './spotIcon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SpotIcon
|
||||
},
|
||||
props: {
|
||||
classes: String,
|
||||
text: String,
|
||||
icon: String
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<button :class="classes"><SpotIcon :icon="icon" :text="text" /></button>
|
||||
</template>
|
||||
19
src/components/spotIcon.vue
Normal file
19
src/components/spotIcon.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
icon: String,
|
||||
text: String,
|
||||
margin: Boolean,
|
||||
classes: String
|
||||
},
|
||||
computed: {
|
||||
classNames() {
|
||||
return 'fa fa-'+this.icon+((this.margin || this.text && this.text!='')?' push':'')+(this.classes?' '+this.classes:'')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<i :class="classNames"></i>{{ text }}
|
||||
</template>
|
||||
100
src/components/upload.vue
Normal file
100
src/components/upload.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<script>
|
||||
import SpotIcon from './spotIcon.vue';
|
||||
import SpotButton from './spotButton.vue';
|
||||
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";
|
||||
|
||||
export default {
|
||||
name: 'upload',
|
||||
components: { SpotButton, SpotIcon },
|
||||
inject: ['spot', 'projects', 'consts', 'user'],
|
||||
data() {
|
||||
return {
|
||||
project: this.projects[this.spot.vars('default_project_codename')],
|
||||
files: [],
|
||||
logs: [],
|
||||
progress: 0
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.spot.addPage('upload', {});
|
||||
|
||||
if(this.project.editable) {
|
||||
$('#fileupload')
|
||||
.fileupload({
|
||||
dataType: 'json',
|
||||
formData: {t: this.user.timezone},
|
||||
acceptFileTypes: /(\.|\/)(gif|jpe?g|png|mov)$/i,
|
||||
done: (e, asData) => {
|
||||
$.each(asData.result.files, (iKey, oFile) => {
|
||||
let bError = ('error' in oFile);
|
||||
|
||||
//Feedback
|
||||
this.logs.push(bError?oFile.error:(this.spot.lang('upload_success', [oFile.name])));
|
||||
|
||||
//Comments
|
||||
oFile.content = '';
|
||||
if(!bError) this.files.push(oFile);
|
||||
});
|
||||
},
|
||||
progressall: (e, data) => {
|
||||
this.progress = parseInt(data.loaded / data.total * 100, 10);
|
||||
}
|
||||
});
|
||||
}
|
||||
else this.logs = [this.spot.lang('upload_mode_archived', [this.project.name])];
|
||||
},
|
||||
methods: {
|
||||
addComment(oFile) {
|
||||
this.spot.get2('add_comment', {id: oFile.id, content: oFile.content})
|
||||
.then((asData) => {this.logs.push(this.spot.lang('media_comment_update', asData.filename));})
|
||||
.catch((sMsgId) => {this.logs.push(this.spot.lang(sMsgId));});
|
||||
},
|
||||
addPosition() {
|
||||
if(navigator.geolocation) {
|
||||
this.logs.push('Determining position...');
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
this.logs.push('Sending position...');
|
||||
this.spot.get2('add_position', {'latitude':position.coords.latitude, 'longitude':position.coords.longitude, 'timestamp':Math.round(position.timestamp / 1000)})
|
||||
.then((asData) => {this.logs.push('Position sent');})
|
||||
.catch((sMsgId) => {this.logs.push(self.lang(sMsgId));});
|
||||
},
|
||||
(error) => {
|
||||
this.logs.push(error.message);
|
||||
}
|
||||
);
|
||||
}
|
||||
else this.logs.push('This browser does not support geolocation');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div id="upload">
|
||||
<a name="back" class="button" href="#project"><SpotIcon :icon="'back'" :text="spot.lang('nav_back')" /></a>
|
||||
<h1>{{ spot.lang('upload_title') }}</h1>
|
||||
<h2>{{ this.project.name }}</h2>
|
||||
<div class="section" v-if="project.editable">
|
||||
<input id="fileupload" type="file" name="files[]" :data-url="this.spot.getActionLink('upload')" multiple />
|
||||
</div>
|
||||
<div class="section progress" v-if="progress > 0">
|
||||
<div class="bar" :style="{width:progress+'%'}"></div>
|
||||
</div>
|
||||
<div class="section comment" v-for="file in files">
|
||||
<img class="thumb" :src="file.thumbnail" />
|
||||
<div class="form">
|
||||
<input class="content" name="content" type="text" v-model="file.content" />
|
||||
<input class="id" name="id" type="hidden" :value="file.id" />
|
||||
<SpotButton :classes="'save'" :icon="'save'" :text="spot.lang('save')" @click="addComment(file)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="section location">
|
||||
<SpotButton :icon="'message'" :text="spot.lang('new_position')" @click="addPosition()" />
|
||||
</div>
|
||||
<div class="section logs" v-if="logs.length > 0">
|
||||
<p class="log" v-for="log in logs">{{ log }}.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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,11 @@
|
||||
<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>
|
||||
<div id="container"></div>
|
||||
<script type="module" src="[#]filepath_js[#]"></script>
|
||||
</body>
|
||||
</html>
|
||||
57
src/masks/project.html
Normal file
57
src/masks/project.html
Normal 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
10
src/masks/upload.html
Normal 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
41
src/scripts/app.js
Normal file
@@ -0,0 +1,41 @@
|
||||
//jQuery
|
||||
import './jquery.helpers.js';
|
||||
|
||||
//Common
|
||||
import * as common from './common.js';
|
||||
window.copyArray = common.copyArray;
|
||||
window.getElem = common.getElem;
|
||||
window.setElem = common.setElem;
|
||||
window.getDragPosition = common.getDragPosition;
|
||||
window.copyTextToClipboard = common.copyTextToClipboard;
|
||||
window.getOuterWidth = common.getOuterWidth;
|
||||
|
||||
import Css from './../styles/spot.scss';
|
||||
import LogoText from '../images/logo_black.png';
|
||||
import Logo from '../images/spot-logo-only.svg';
|
||||
|
||||
//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('#container');
|
||||
82
src/scripts/common.js
Normal file
82
src/scripts/common.js
Normal file
@@ -0,0 +1,82 @@
|
||||
/* 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);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function getOuterWidth(element) {
|
||||
var style = getComputedStyle(element);
|
||||
var width = element.offsetWidth; // Width without padding and border
|
||||
width += parseInt(style.marginLeft) + parseInt(style.marginRight); // Add margins
|
||||
|
||||
// Check if the box-sizing is border-box (includes padding and border in the width)
|
||||
if (style.boxSizing === 'border-box') {
|
||||
width += parseInt(style.paddingLeft) + parseInt(style.paddingRight); // Add padding
|
||||
width += parseInt(style.borderLeftWidth) + parseInt(style.borderRightWidth); // Add border
|
||||
}
|
||||
|
||||
return width;
|
||||
}
|
||||
129
src/scripts/jquery.helpers.js
Normal file
129
src/scripts/jquery.helpers.js
Normal 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')
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
14
src/scripts/leaflet.helpers.js
Normal file
14
src/scripts/leaflet.helpers.js
Normal 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);
|
||||
}
|
||||
});
|
||||
@@ -62,6 +62,7 @@
|
||||
sanitizeTitle: false
|
||||
, hasVideo: true
|
||||
, onMediaChange: (oMedia) => {}
|
||||
, onClosing: () => {}
|
||||
};
|
||||
|
||||
Lightbox.prototype.option = function(options) {
|
||||
@@ -477,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;
|
||||
@@ -489,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()
|
||||
@@ -718,11 +720,10 @@
|
||||
if(this.options.hasVideo) {
|
||||
var $lbContainer = this.$lightbox.find('.lb-container');
|
||||
var $hasVideoNav = $lbContainer.hasClass('lb-video-nav');
|
||||
this.$video.attr('src', '');
|
||||
this.$video.trigger('pause').removeAttr('src');
|
||||
|
||||
if($hasVideoNav) $lbContainer.removeClass('lb-video-nav');
|
||||
}
|
||||
oSpot.flushHash();
|
||||
|
||||
$(window).off('resize', this.sizeOverlay);
|
||||
this.$nav.off('mousewheel');
|
||||
@@ -732,6 +733,8 @@
|
||||
if (this.options.disableScrolling) {
|
||||
$('body').removeClass('lb-disable-scrolling');
|
||||
}
|
||||
|
||||
this.options.onClosing();
|
||||
};
|
||||
|
||||
return new Lightbox();
|
||||
|
||||
165
src/scripts/page.admin.js
Normal file
165
src/scripts/page.admin.js
Normal 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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
1180
src/scripts/page.project.js
Normal file
1180
src/scripts/page.project.js
Normal file
File diff suppressed because it is too large
Load Diff
77
src/scripts/page.upload.js
Normal file
77
src/scripts/page.upload.js
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -1,109 +1,130 @@
|
||||
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;
|
||||
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 */
|
||||
|
||||
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();
|
||||
init() {
|
||||
this.elem.container = $('#container');
|
||||
this.elem.main = $('#main');
|
||||
|
||||
//On Key down
|
||||
$('html').on('keydown', function(oEvent){self.onKeydown(oEvent);});
|
||||
$('html').on('keydown', (oEvent) => {this.onKeydown(oEvent);});
|
||||
|
||||
//on window resize
|
||||
$(window).on('resize', function(){self.onResize();});
|
||||
|
||||
//Setup menu
|
||||
//self.initMenu();
|
||||
$(window).on('resize', () => {this.onResize();});
|
||||
|
||||
//Hash management
|
||||
$(window)
|
||||
.bind('hashchange', self.onHashChange)
|
||||
.on('hashchange', () => {this.onHashChange();})
|
||||
.trigger('hashchange');
|
||||
};
|
||||
|
||||
this.updateVars = function(asVars)
|
||||
{
|
||||
$.each(asVars, function(sKey, oValue){self.vars(sKey, oValue)});
|
||||
};
|
||||
}
|
||||
|
||||
/* Variable Management */
|
||||
|
||||
this.vars = function(oVarName, oValue)
|
||||
{
|
||||
vars(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);
|
||||
if(typeof oValue !== 'undefined') setElem(this.vars, copyArray(asVarName), oValue);
|
||||
|
||||
//Get, only name parameter
|
||||
return getElem(self.vars, asVarName);
|
||||
};
|
||||
return getElem(this.vars, asVarName);
|
||||
}
|
||||
|
||||
this.tmp = function(sVarName, oValue)
|
||||
{
|
||||
tmp(sVarName, oValue) {
|
||||
var asVarName = (typeof sVarName == 'object')?sVarName:[sVarName];
|
||||
asVarName.unshift('tmp');
|
||||
return self.vars(asVarName, oValue);
|
||||
};
|
||||
return this.vars(asVarName, oValue);
|
||||
}
|
||||
|
||||
/* Interface with server */
|
||||
|
||||
this.get = function(sAction, fOnSuccess, oVars, fOnError, fonProgress)
|
||||
{
|
||||
if(!oVars) oVars = {};
|
||||
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'] = self.consts.timezone;
|
||||
oVars['t'] = this.consts.timezone;
|
||||
return $.ajax(
|
||||
{
|
||||
url: self.consts.process_page,
|
||||
url: this.consts.process_page,
|
||||
data: oVars,
|
||||
dataType: 'json'
|
||||
})
|
||||
.done(function(oData)
|
||||
{
|
||||
.done((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.desc.substr(0, this.consts.lang_prefix.length)==this.consts.lang_prefix) oData.desc = this.lang(oData.desc.substr(5));
|
||||
|
||||
if(oData.result==self.consts.error) fOnError(oData.desc);
|
||||
else fOnSuccess(oData.data, oData.desc);
|
||||
if(oData.result==this.consts.error) fOnError(oData.desc);
|
||||
else if(fOnSuccess) fOnSuccess(oData.data, oData.desc);
|
||||
})
|
||||
.fail(function(jqXHR, textStatus, errorThrown)
|
||||
{
|
||||
.fail((jqXHR, textStatus, errorThrown) => {
|
||||
fonProgress('fail');
|
||||
fOnError(textStatus+' '+errorThrown);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
async get2(sAction, oVars, bLoading) {
|
||||
oVars = oVars || {};
|
||||
oVars['a'] = sAction;
|
||||
oVars['t'] = this.consts.timezone;
|
||||
bLoading = true;
|
||||
|
||||
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) {
|
||||
bLoading = false;
|
||||
throw new Error('Error HTTP '+oRequest.status+': '+oRequest.statusText);
|
||||
}
|
||||
else {
|
||||
let oResponse = await oRequest.json();
|
||||
bLoading = false;
|
||||
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) {
|
||||
bLoading = false;
|
||||
throw oError;
|
||||
}
|
||||
}
|
||||
|
||||
lang(sKey, asParams) {
|
||||
asParams = asParams || [];
|
||||
if(typeof asParams != 'object') asParams = [asParams];
|
||||
|
||||
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]);
|
||||
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);
|
||||
@@ -111,141 +132,158 @@ function Spot(asGlobals)
|
||||
}
|
||||
|
||||
return sLang;
|
||||
};
|
||||
}
|
||||
|
||||
isMobile() {
|
||||
return $('#mobile').is(':visible');
|
||||
}
|
||||
|
||||
/* 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
|
||||
};
|
||||
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
|
||||
}
|
||||
|
||||
this.getHash = function()
|
||||
{
|
||||
var sHash = self.hash();
|
||||
var asHash = sHash.split(self.consts.hash_sep);
|
||||
getHash() {
|
||||
var sHash = this.hash();
|
||||
var asHash = sHash.split(this.consts.hash_sep);
|
||||
var sPage = asHash.shift() || '';
|
||||
return {hash:sHash, page:sPage, items:asHash};
|
||||
};
|
||||
}
|
||||
|
||||
this.setHash = function(sPage, asItems, bReboot)
|
||||
{
|
||||
setHash(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)
|
||||
{
|
||||
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();
|
||||
};
|
||||
}
|
||||
|
||||
this.updateHash = function(sType, iId) {
|
||||
updateHash(sType, iId) {
|
||||
sType = sType || '';
|
||||
iId = iId || 0;
|
||||
|
||||
var asHash = self.getHash();
|
||||
if(iId) self.setHash(asHash.page, [asHash.items[0], sType, iId]);
|
||||
};
|
||||
var asHash = this.getHash();
|
||||
if(iId) this.setHash(asHash.page, [asHash.items[0], sType, iId]);
|
||||
}
|
||||
|
||||
this.flushHash = function(asTypes) {
|
||||
flushHash(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]]);
|
||||
};
|
||||
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 */
|
||||
|
||||
this.getActionLink = function(sAction, oVars)
|
||||
{
|
||||
getActionLink(sAction, oVars) {
|
||||
if(!oVars) oVars = {};
|
||||
sVars = '';
|
||||
for(i in oVars)
|
||||
{
|
||||
sVars += '&'+i+'='+oVars[i];
|
||||
}
|
||||
return self.consts.process_page+'?a='+sAction+sVars;
|
||||
};
|
||||
let sVars = '';
|
||||
|
||||
for(i in oVars) sVars += '&'+i+'='+oVars[i];
|
||||
|
||||
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){};
|
||||
};
|
||||
return this.consts.process_page+'?a='+sAction+sVars;
|
||||
}
|
||||
|
||||
this.switchPage = function(asHash)
|
||||
{
|
||||
addPage(sPage, oPage) {
|
||||
this.pages[sPage] = oPage;
|
||||
}
|
||||
|
||||
switchPage(asHash) {
|
||||
var sPageName = asHash.page;
|
||||
var bSamePage = (self.vars('page') == sPageName);
|
||||
var bFirstPage = (self.vars('page') == '');
|
||||
var bSamePage = (this.vars('page') == sPageName);
|
||||
var bFirstPage = (this.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']));
|
||||
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(self.onQuitPage(bSamePage) && !bSamePage || self.onSamePageMove(asHash))
|
||||
else if(this.onQuitPage(bSamePage) && !bSamePage || this.onSamePageMove(asHash))
|
||||
{
|
||||
//Delete tmp variables
|
||||
self.vars('tmp', {});
|
||||
|
||||
//disable tmp functions
|
||||
self.resetTmpFunctions();
|
||||
this.vars('tmp', {});
|
||||
|
||||
//Officially a new page
|
||||
self.vars('page', sPageName);
|
||||
self.vars('hash', asHash);
|
||||
|
||||
this.vars('page', sPageName);
|
||||
this.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
|
||||
}
|
||||
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) self.vars('hash', asHash);
|
||||
};
|
||||
else if(bSamePage) this.vars('hash', asHash);
|
||||
}
|
||||
|
||||
this.setPageTitle = function(sTitle) {
|
||||
document.title = self.consts.title+' - '+sTitle;
|
||||
};
|
||||
|
||||
this.splash = function($Dom, asHash, bFirstPage)
|
||||
splash($Dom, asHash, bFirstPage)
|
||||
{
|
||||
//Switch main content
|
||||
self.elem.main.empty().html($Dom);
|
||||
this.elem.main.empty().html($Dom);
|
||||
|
||||
//Page Bootstrap
|
||||
self.pageInit(asHash, bFirstPage);
|
||||
this.pageInit(asHash, bFirstPage);
|
||||
|
||||
//Show main
|
||||
var $FadeInElem = bFirstPage?self.elem.container:self.elem.main;
|
||||
var $FadeInElem = bFirstPage?this.elem.container:this.elem.main;
|
||||
$FadeInElem.hide().fadeTo('slow', 1);
|
||||
};
|
||||
}
|
||||
|
||||
this.getNaturalDuration = function(iHours) {
|
||||
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 {
|
||||
@@ -256,213 +294,13 @@ function Spot(asGlobals)
|
||||
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?(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
|
||||
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
checkClearance(sClearance) {
|
||||
return (this.vars(['user', 'clearance']) >= sClearance);
|
||||
}
|
||||
}
|
||||
|
||||
$.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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -139,3 +139,9 @@ h1 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
|
||||
#mobile {
|
||||
display: none;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
$fa-font-path: "fa/fonts";
|
||||
$fa-font-path: 'fonts';
|
||||
$fa-css-prefix: fa;
|
||||
|
||||
@import 'fa/solid';
|
||||
@@ -79,6 +79,9 @@ $fa-css-prefix: fa;
|
||||
.#{$fa-css-prefix}-config:before { content: fa-content($fa-var-cogs); }
|
||||
.#{$fa-css-prefix}-upload:before { content: fa-content($fa-var-cloud-upload); }
|
||||
|
||||
/* Upload */
|
||||
.#{$fa-css-prefix}-save:before { content: fa-content($fa-var-floppy-disk); }
|
||||
|
||||
/* Feed */
|
||||
.#{$fa-css-prefix}-post:before { content: fa-content($fa-var-comment); }
|
||||
.#{$fa-css-prefix}-media:before { content: fa-content($fa-var-photo-video); }
|
||||
@@ -95,7 +98,7 @@ $fa-css-prefix: fa;
|
||||
.#{$fa-css-prefix}-video-shot:before { content: fa-content($fa-var-camcorder); }
|
||||
.#{$fa-css-prefix}-image-shot:before { content: fa-content($fa-var-camera-alt); }
|
||||
.#{$fa-css-prefix}-link:before { content: fa-content($fa-var-link); }
|
||||
.#{$fa-css-prefix}-link.copied:before { content: fa-content($fa-var-check); }
|
||||
.#{$fa-css-prefix}-copied:before { content: fa-content($fa-var-check); }
|
||||
|
||||
/* Feed - Poster */
|
||||
.#{$fa-css-prefix}-poster:before { content: fa-content($fa-var-comment-edit); }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
$theme : "spot-theme";
|
||||
$base-color : #CCC;
|
||||
$highlight-color : #FFF;
|
||||
$background : rgba($base-color, 0.2);
|
||||
$drag-color : rgba($highlight-color, 0.2);
|
||||
$axis-color : darken($base-color,20%);
|
||||
$stroke-color : darken($base-color,40%);
|
||||
$stroke-width-mouse-focus : 1;
|
||||
$stroke-width-height-focus: 2;
|
||||
$stroke-width-axis : 2;
|
||||
|
||||
@import '../../node_modules/leaflet/dist/leaflet';
|
||||
@import 'leaflet/leaflet_heightgraph';
|
||||
|
||||
/* Leaflet fixes */
|
||||
.leaflet-container {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.leaflet-popup {
|
||||
.leaflet-popup-content-wrapper {
|
||||
border-radius: 5px;
|
||||
padding: 0;
|
||||
|
||||
.leaflet-popup-content {
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.leaflet-control.spot-control, .leaflet-control.heightgraph .heightgraph-toggle {
|
||||
@extend .clickable;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
text-align: center;
|
||||
box-shadow: none;
|
||||
|
||||
.fa {
|
||||
@extend .control-icon;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Leaflet Heightgraph fixes */
|
||||
|
||||
.legend-text, .tick, .tick text, .focusbox, .height-focus.circle, .height-focus.label, .lineSelection, .horizontalLineText {
|
||||
fill: #333 !important;
|
||||
}
|
||||
|
||||
.axis path, .focusbox rect, .focusLine line, .height-focus.label rect, .height-focus.line, .horizontalLine {
|
||||
stroke: #333 !important;
|
||||
}
|
||||
|
||||
.focusbox rect, .height-focus.label rect {
|
||||
stroke-width: 0;
|
||||
}
|
||||
|
||||
.focusLine line, .focusbox rect, .height-focus.label rect {
|
||||
-webkit-filter: drop-shadow(1px 0px 2px rgba(0, 0, 0, 0.6));
|
||||
filter: drop-shadow(1px 0px 2px rgba(0, 0, 0, 0.6));
|
||||
}
|
||||
|
||||
.height-focus.label rect, .focusbox rect {
|
||||
fill: rgba(255,255,255,.6);
|
||||
}
|
||||
|
||||
.heightgraph.leaflet-control {
|
||||
svg.heightgraph-container {
|
||||
background: none;
|
||||
border-radius: 0;
|
||||
|
||||
.area {
|
||||
@include drop-shadow(0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.horizontalLine {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.heightgraph-toggle {
|
||||
background: none;
|
||||
|
||||
.heightgraph-toggle-icon {
|
||||
@extend .control-icon;
|
||||
@extend .fa-elev-chart;
|
||||
height: 44px;
|
||||
position: static;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.heightgraph-close-icon {
|
||||
@extend .control-icon;
|
||||
@extend .fa-unsubscribe;
|
||||
background: none;
|
||||
font-size: 20px;
|
||||
line-height: 26px;
|
||||
width: 26px;
|
||||
text-align: center;
|
||||
display: none;
|
||||
|
||||
&:before {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.leaflet-default-icon-path {
|
||||
background-image: none;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '../../node_modules/lightbox2/dist/css/lightbox.css';
|
||||
@import '../../node_modules/lightbox2/src/css/lightbox.css';
|
||||
|
||||
@mixin lightbox-icon($icon) {
|
||||
background: none;
|
||||
|
||||
@@ -1,842 +0,0 @@
|
||||
//Feed width
|
||||
$elem-spacing: 0.5rem;
|
||||
$block-spacing: 1rem;
|
||||
$block-radius: 3px;
|
||||
$block-shadow: 3px;
|
||||
$panel-width: 30vw;
|
||||
$panel-width-max: "400px + 3 * #{$block-spacing}";
|
||||
$button-width: 44px;
|
||||
|
||||
//Feed colors
|
||||
$post-input-bg: #ffffff; //#d9deff;
|
||||
$post-color: #333; //#323268;
|
||||
$post-color-hover: darken($post-color, 10%);
|
||||
$post-bg: rgba(255,255,255,.8); //#B4BDFF;
|
||||
$message-color: #326526;
|
||||
$message-color-hover: darken($message-color, 10%);
|
||||
$message-bg: #6DFF58;
|
||||
$media-color: #333; //#635C28;
|
||||
$media-bg: rgba(255,255,255,.8); //#F3EC9F;
|
||||
|
||||
//Settings colors
|
||||
$title-color: $post-color;
|
||||
$subtitle-color: #999;
|
||||
|
||||
//Legend colors
|
||||
$track-main-color: #00ff78;
|
||||
$track-off-track-color: #0000ff;
|
||||
$track-hitchhiking-color: #FF7814;
|
||||
$legend-color: $post-color;
|
||||
|
||||
#projects {
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
/* Panels movements */
|
||||
&.with-feed {
|
||||
#submap {
|
||||
width: calc(100% - min(#{$panel-width}, #{$panel-width-max}));
|
||||
}
|
||||
|
||||
#feed {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.leaflet-right {
|
||||
right: min(#{$panel-width}, #{$panel-width-max});
|
||||
}
|
||||
|
||||
#feed-button {
|
||||
.fa {
|
||||
@extend .fa-next;
|
||||
}
|
||||
}
|
||||
|
||||
#title {
|
||||
max-width: calc(100vw - max(#{$panel-width}, #{$panel-width-max}) - (#{$button-width} + #{$block-spacing} * 2) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
&.with-settings {
|
||||
#submap {
|
||||
width: calc(100% - min(#{$panel-width}, #{$panel-width-max}));
|
||||
left: min(#{$panel-width}, #{$panel-width-max});
|
||||
}
|
||||
|
||||
#settings {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.leaflet-left {
|
||||
left: min(#{$panel-width}, #{$panel-width-max});
|
||||
}
|
||||
|
||||
#settings-button {
|
||||
.fa {
|
||||
@extend .fa-prev;
|
||||
}
|
||||
}
|
||||
|
||||
#title {
|
||||
max-width: calc(100vw - #{$block-spacing} * 2 - min(#{$panel-width}, #{$panel-width-max}) - (#{$button-width} + #{$block-spacing} * 2) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
&.with-feed.with-settings {
|
||||
#submap {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#title {
|
||||
max-width: calc(100vw - #{$block-spacing} * 2 - min(#{$panel-width}, #{$panel-width-max}) * 2 - (#{$button-width} + #{$block-spacing} * 2) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
#background {
|
||||
background: #666;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#submap {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
transition: width 0.5s, left 0.5s;
|
||||
|
||||
.loader {
|
||||
position: absolute;
|
||||
font-size: 3em;
|
||||
top: calc(50% - 0.5em);
|
||||
left: calc(50% - 1.25em/2);
|
||||
color: #CCC;
|
||||
}
|
||||
}
|
||||
|
||||
#map {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
|
||||
/* Leaflet Popup */
|
||||
.leaflet-popup-content {
|
||||
|
||||
h1 {
|
||||
font-size: 1.4em;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.separator {
|
||||
border-top: 1px solid #CCC;
|
||||
margin: $elem-spacing 0 $block-spacing 0;
|
||||
}
|
||||
|
||||
/* Marker Popup */
|
||||
.info-window {
|
||||
h1 .message-type {
|
||||
color: #CCC;
|
||||
font-weight: normal;
|
||||
font-size: calc(1em / 1.4);
|
||||
margin-left: 0.5em;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.0em;
|
||||
margin: $elem-spacing 0 0 0;
|
||||
|
||||
a {
|
||||
color: $post-color;
|
||||
}
|
||||
}
|
||||
|
||||
.medias {
|
||||
line-height: 0;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
|
||||
margin: $block-spacing $block-spacing 0 0;
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&.drill {
|
||||
font-size: 2em;
|
||||
|
||||
.fa-drill-image {
|
||||
color: transparent;
|
||||
}
|
||||
.fa-drill-video {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.fa-drill-video, .fa-drill-image {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 200px;
|
||||
max-height: 100px;
|
||||
border-radius: $block-radius;
|
||||
image-orientation: from-image;
|
||||
transition: All 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Track Popup */
|
||||
.track_tooltip {
|
||||
p {
|
||||
margin: 0;
|
||||
|
||||
&.description {
|
||||
font-size: 1.15em;
|
||||
}
|
||||
}
|
||||
|
||||
h1, .description {
|
||||
@include no-text-overflow();
|
||||
}
|
||||
.body {
|
||||
padding-left: calc(1.25em*1.4 + #{$elem-spacing} );
|
||||
|
||||
.details {
|
||||
margin-top: -$block-spacing;
|
||||
|
||||
p.detail {
|
||||
margin-top: $block-spacing;
|
||||
width: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Leaflet patches */
|
||||
.leaflet-control {
|
||||
background-color: rgba(255, 255, 255, 0.6);
|
||||
font-family: Roboto, Arial, sans-serif;
|
||||
border-radius: $block-radius;
|
||||
border: none;
|
||||
margin: $block-spacing;
|
||||
box-shadow: 0 1px 7px rgba(0, 0, 0, .4);
|
||||
|
||||
&+ .leaflet-control:not(.leaflet-control-inline) {
|
||||
margin-top: 0;
|
||||
}
|
||||
&+ .leaflet-control.leaflet-control-inline {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&.leaflet-control-scale {
|
||||
padding: 0.5em;
|
||||
|
||||
.leaflet-control-scale-line {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.leaflet-control-inline {
|
||||
clear: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Pull right/left controls by $panel-width */
|
||||
.leaflet-right, .leaflet-left {
|
||||
transition: left 0.5s, right 0.5s;
|
||||
}
|
||||
|
||||
/* Hide default layer control */
|
||||
.leaflet-top.leaflet-left .leaflet-control-layers .leaflet-control-layers-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#legend {
|
||||
.track {
|
||||
white-space: nowrap;
|
||||
.line {
|
||||
width: 2em;
|
||||
height: 4px;
|
||||
display: inline-block;
|
||||
border-radius: 2px;
|
||||
vertical-align: middle;
|
||||
|
||||
&.main {
|
||||
background-color: $track-main-color;
|
||||
}
|
||||
&.off-track {
|
||||
background-color: $track-off-track-color;
|
||||
}
|
||||
&.hitchhiking {
|
||||
background-color: $track-hitchhiking-color;
|
||||
}
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 1em;
|
||||
margin-left: 0.5em;
|
||||
color: $legend-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#title {
|
||||
@include no-text-overflow();
|
||||
line-height: $button-width;
|
||||
height: $button-width;
|
||||
padding: 0 $block-spacing;
|
||||
margin-bottom: 0;
|
||||
|
||||
span#project_name {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
}
|
||||
|
||||
#feed-button .fa {
|
||||
@extend .fa-post;
|
||||
}
|
||||
#settings-button .fa {
|
||||
@extend .fa-menu;
|
||||
}
|
||||
|
||||
/* Drill & Map icons */
|
||||
|
||||
a.drill {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
|
||||
.drill-icon {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%,-50%);
|
||||
|
||||
i {
|
||||
transition: color 0.3s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fa-stack {
|
||||
.fa-message {
|
||||
font-size: 32px;
|
||||
text-shadow: rgba(0, 0, 0, 0.5) 3px 3px 3px;
|
||||
color: $message-bg;
|
||||
}
|
||||
.fa-message-in {
|
||||
font-size: 13px;
|
||||
color: $message-color;
|
||||
top: 1px;
|
||||
}
|
||||
.fa-track-start, .fa-track-end {
|
||||
color: $message-color;
|
||||
font-size: 14px;
|
||||
top: 1px;
|
||||
}
|
||||
.fa-track-end {
|
||||
color: $track-hitchhiking-color;
|
||||
}
|
||||
}
|
||||
|
||||
/* Feed/Settings Panel */
|
||||
|
||||
#feed, #settings {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
z-index: 999;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
|
||||
&.moving {
|
||||
cursor: grabbing;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
background-color: $post-input-bg;
|
||||
color: $post-color;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button, a.button {
|
||||
background-color: $post-color;
|
||||
color: $post-bg;
|
||||
|
||||
&:hover, &:hover a, &:hover a:visited {
|
||||
background-color: $post-input-bg;
|
||||
color: $post-color;
|
||||
}
|
||||
|
||||
a, a:visited {
|
||||
background-color: $post-color;
|
||||
color: $post-bg;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&+ button, &+ a.button {
|
||||
margin-left: $elem-spacing;
|
||||
}
|
||||
}
|
||||
|
||||
#feed-panel, #settings-panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
#feed {
|
||||
right: calc(min(#{$panel-width}, #{$panel-width-max}) * -1);
|
||||
transition: right 0.5s;
|
||||
width: #{$panel-width};
|
||||
max-width: calc(#{$panel-width-max});
|
||||
|
||||
#feed-panel {
|
||||
width: 100%;
|
||||
padding-top: $block-spacing;
|
||||
|
||||
#posts_list {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#poster {
|
||||
&.histo-mode .poster, &:not(.histo-mode) .archived {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.poster {
|
||||
textarea#post {
|
||||
margin-bottom: 1em;
|
||||
width: calc(100% - 2em);
|
||||
}
|
||||
|
||||
input#name {
|
||||
width: calc(100% - 6em);
|
||||
}
|
||||
|
||||
button#submit {
|
||||
margin-left: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.archived {
|
||||
background: #EEE;
|
||||
}
|
||||
}
|
||||
|
||||
.body-box {
|
||||
position:relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.post-item {
|
||||
margin-bottom: $block-spacing;
|
||||
background: $post-bg;
|
||||
color: $post-color;
|
||||
border-radius: $block-radius;
|
||||
width: calc(100% - #{$block-spacing});
|
||||
box-shadow: 2px 2px 3px 0px rgba(0, 0, 0, 0.5);
|
||||
|
||||
a {
|
||||
color: $post-color;
|
||||
&:hover {
|
||||
color: $post-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
margin: 0;
|
||||
}
|
||||
.signature {
|
||||
margin: $elem-spacing 0 0 0;
|
||||
text-align: right;
|
||||
font-style: italic;
|
||||
|
||||
img {
|
||||
vertical-align: baseline;
|
||||
margin: 0 0.2em calc((1em - 24px)/2) 0;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
.header {
|
||||
padding: 0 $block-spacing;
|
||||
position: relative;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
font-size: 0.8em;
|
||||
padding: $elem-spacing 0px;
|
||||
|
||||
&.index {
|
||||
width: 25%;
|
||||
|
||||
.link, .link:visited, .link_copied {
|
||||
margin-left: $elem-spacing;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.time {
|
||||
width: 75%;
|
||||
text-align: right;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
.body {
|
||||
clear: both;
|
||||
padding: 0em $block-spacing $block-spacing;
|
||||
}
|
||||
|
||||
&.headerless {
|
||||
.header {
|
||||
display: none;
|
||||
}
|
||||
.body {
|
||||
padding-top: $block-spacing;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
|
||||
.fa {
|
||||
display: inline-block;
|
||||
font-size: 2em;
|
||||
margin: $elem-spacing 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.message {
|
||||
background: $message-bg;
|
||||
color: $message-color;
|
||||
|
||||
p {
|
||||
font-size: 0.9em;
|
||||
margin: 0 0 $elem-spacing 0;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $message-color;
|
||||
&:hover {
|
||||
color: $message-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
a.drill {
|
||||
line-height: 0;
|
||||
|
||||
.drill-icon {
|
||||
transform: translate(-16px, -32px);
|
||||
|
||||
.fa-message-in {
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.fa-message {
|
||||
@extend .#{$fa-css-prefix}-drill-message;
|
||||
top: 13px;
|
||||
left: 3px;
|
||||
}
|
||||
.fa-message-in {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.weather {
|
||||
position: absolute;
|
||||
top: $block-spacing;
|
||||
right: $block-spacing;
|
||||
|
||||
.fa {
|
||||
font-size: 1.3em;
|
||||
vertical-align: middle;
|
||||
line-height: 1rem;
|
||||
background: $message-color;
|
||||
color: $message-bg;
|
||||
border-radius: $block-radius 0 0 $block-radius;
|
||||
padding: $elem-spacing;
|
||||
}
|
||||
|
||||
span {
|
||||
vertical-align: middle;
|
||||
padding: $elem-spacing;
|
||||
background: $message-bg;
|
||||
color: $message-color;
|
||||
border-radius: 0 $block-radius $block-radius 0;
|
||||
}
|
||||
}
|
||||
|
||||
.staticmap {
|
||||
width: 100%;
|
||||
border-radius: $block-radius;
|
||||
}
|
||||
}
|
||||
|
||||
&.post {
|
||||
.body {
|
||||
padding: 0em 1em 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
&.media {
|
||||
background: $media-bg;
|
||||
color: $media-color;
|
||||
|
||||
.body {
|
||||
a {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
color: $media-color;
|
||||
position: relative;
|
||||
line-height: 0;
|
||||
|
||||
&.drill {
|
||||
&:hover {
|
||||
.drill-icon .fa-drill-image, .drill-icon .fa-drill-video {
|
||||
color: rgba($media-bg, 0.75);
|
||||
}
|
||||
.comment {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.drill-icon {
|
||||
font-size: 3em;
|
||||
|
||||
.fa-drill-image {
|
||||
color: transparent;
|
||||
}
|
||||
.fa-drill-video {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
image-orientation: from-image;
|
||||
outline: none;
|
||||
border-radius: $block-radius;
|
||||
}
|
||||
|
||||
.comment {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
line-height: normal;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0.5em;
|
||||
text-align: justify;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border-radius: 0 0 $block-radius $block-radius;
|
||||
transition: opacity 0.3s;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#settings {
|
||||
left: calc(min(#{$panel-width} + #{$block-shadow}, #{$panel-width-max} + #{$block-shadow}) * -1);
|
||||
transition: left 0.5s;
|
||||
width: calc(#{$panel-width} + #{$block-shadow}); //Add box-shadow
|
||||
max-width: calc(#{$panel-width-max} + #{$block-shadow}); //Add box-shadow
|
||||
|
||||
#settings-panel {
|
||||
width: calc(100% - #{$block-spacing} - #{$block-shadow}); //Remove box-shadow
|
||||
margin: $block-spacing;
|
||||
border-radius: $block-radius;
|
||||
box-shadow: 2px 2px $block-shadow 0px rgba(0, 0, 0, .5);
|
||||
color: $post-color;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
.settings-header {
|
||||
text-align: center;
|
||||
flex: 0 1 auto;
|
||||
|
||||
.logo {
|
||||
background: rgba(255, 255, 255, .4);
|
||||
padding: 2rem 1rem;
|
||||
border-radius: $block-radius $block-radius 0 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 180px;
|
||||
transform: translateX(-10%); //Center Text, not logo. logo width (40px) / image width (200px) = 20%. And centering: 20% / 2 = 10%
|
||||
}
|
||||
}
|
||||
|
||||
#last_update {
|
||||
position: absolute;
|
||||
margin-top: -2em;
|
||||
padding: 0 1rem;
|
||||
width: calc(100% - 2rem);
|
||||
|
||||
p {
|
||||
text-align: center;
|
||||
font-size: 0.8em;
|
||||
margin: 0;
|
||||
color: $subtitle-color;
|
||||
transform: translateX(calc(-0.5 * (12px + 0.5em))); //icon width + margin right
|
||||
|
||||
span {
|
||||
margin-right: 0.5em;
|
||||
img {
|
||||
width: 12px;
|
||||
vertical-align: middle;
|
||||
animation: spotlogo 20s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
abbr {
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-footer {
|
||||
flex: 0 1 auto;
|
||||
background: rgba(255, 255, 255, .4);
|
||||
border-radius: 0 0 3px 3px;
|
||||
font-size: 0.7em;
|
||||
padding: 0.3rem;
|
||||
text-align: center;
|
||||
color: #888;
|
||||
|
||||
a {
|
||||
color: #777;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-sections {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
|
||||
#settings-sections-scrollbox {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
display: inline-block;
|
||||
margin: 1.5rem 1rem 0 1rem;
|
||||
width: calc(100% - 2 * #{$block-spacing});
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 $block-spacing;
|
||||
color: $title-color;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: .3em;
|
||||
display: block;
|
||||
@extend .clickable;
|
||||
|
||||
& > div {
|
||||
@include no-text-overflow();
|
||||
}
|
||||
}
|
||||
|
||||
&.newsletter {
|
||||
input#email {
|
||||
width: calc(100% - 6em);
|
||||
|
||||
&:disabled {
|
||||
color: #999;
|
||||
background: rgba(255,255,255,0.2);
|
||||
}
|
||||
}
|
||||
button#nl_btn {
|
||||
margin-left: 1em;
|
||||
margin-bottom: 1em;
|
||||
|
||||
&.subscribe .fa {
|
||||
@extend .fa-send;
|
||||
}
|
||||
&.unsubscribe .fa {
|
||||
@extend .fa-unsubscribe;
|
||||
}
|
||||
&.loading {
|
||||
background-color: $message-color;
|
||||
color: white;
|
||||
span {
|
||||
@extend .flicker;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#settings-projects {
|
||||
a.fa-download {
|
||||
color: $legend-color;
|
||||
|
||||
&:hover {
|
||||
color: #0078A8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#elems {
|
||||
display: none;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
#upload {
|
||||
padding: 1em;
|
||||
|
||||
.bar {
|
||||
height: 18px;
|
||||
background: green;
|
||||
}
|
||||
|
||||
.comment {
|
||||
margin-top: 1em;
|
||||
|
||||
.thumb {
|
||||
width: 30%;
|
||||
max-width: 100px;
|
||||
}
|
||||
form {
|
||||
display: inline-block;
|
||||
width: calc(70% - 1em);
|
||||
min-width: calc(100% - 100px - 1em);
|
||||
margin-left: 1em;
|
||||
vertical-align: top;
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0.5em;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.save {
|
||||
margin-top: 1em;
|
||||
padding: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +1,51 @@
|
||||
@media only screen and (max-width: 800px) {
|
||||
|
||||
$panel-width: "100vw - #{$button-width} - 2 * #{$block-spacing}";
|
||||
$panel-width-max: $panel-width;
|
||||
$panel-actual-width: $panel-width;
|
||||
|
||||
.desktop {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#projects {
|
||||
#feed, #settings {
|
||||
.map-container {
|
||||
width: calc(#{$panel-width});
|
||||
max-width: calc(#{$panel-width});
|
||||
}
|
||||
|
||||
#feed {
|
||||
right: calc((#{$panel-width}) * -1);
|
||||
}
|
||||
|
||||
#settings {
|
||||
left: calc((#{$panel-width}) * -1);
|
||||
}
|
||||
|
||||
#title {
|
||||
width: calc(#{$panel-width} - #{$button-width} - 4 * #{$block-spacing});
|
||||
max-width: calc(#{$panel-width} - #{$button-width} - 4 * #{$block-spacing});
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.leaflet-right, .leaflet-left {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.with-feed, &.with-settings {
|
||||
#submap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.leaflet-control-container .leaflet-top.leaflet-right {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#title {
|
||||
width: calc(#{$panel-width} - #{$button-width} - 4 * #{$block-spacing});
|
||||
max-width: calc(#{$panel-width} - #{$button-width} - 4 * #{$block-spacing});
|
||||
max-width: calc(100vw - #{$block-spacing} - #{$panel-actual-width} - (#{$button-width} + #{$block-spacing} * 2) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
&.with-feed {
|
||||
.leaflet-right {
|
||||
right: calc(#{$panel-width});
|
||||
}
|
||||
.leaflet-left {
|
||||
left: calc((#{$panel-width}) * -1);
|
||||
}
|
||||
}
|
||||
|
||||
&.with-settings {
|
||||
.leaflet-right {
|
||||
right: calc((#{$panel-width}) * -1);
|
||||
}
|
||||
.leaflet-left {
|
||||
left: calc(#{$panel-width});
|
||||
#submap {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.leaflet-control-container .leaflet-top.leaflet-left {
|
||||
display: none;
|
||||
&.with-feed {
|
||||
.map-container-left {
|
||||
transform: translateX(-200vw);
|
||||
}
|
||||
|
||||
.map-container-right {
|
||||
transform: translateX(calc(#{$button-width} + #{$block-spacing} * 2));
|
||||
}
|
||||
}
|
||||
|
||||
&.with-settings {
|
||||
.map-container-left {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.map-container-right {
|
||||
transform: translateX(200vw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,11 +62,9 @@
|
||||
a.lb-next::before {
|
||||
right: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 801px) {
|
||||
.mobile {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
#mobile {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
256
src/styles/_page.project.feed.scss
Normal file
256
src/styles/_page.project.feed.scss
Normal file
@@ -0,0 +1,256 @@
|
||||
#feed {
|
||||
#feed-panel {
|
||||
#feed-header {
|
||||
.poster {
|
||||
textarea[name=post] {
|
||||
margin-bottom: 1em;
|
||||
width: calc(100% - 2em);
|
||||
}
|
||||
|
||||
input[name=name] {
|
||||
width: calc(100% - 6em);
|
||||
}
|
||||
|
||||
button[name=submit] {
|
||||
margin-left: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.archived {
|
||||
background: #EEE;
|
||||
}
|
||||
}
|
||||
|
||||
#feed-posts {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.body-box {
|
||||
position:relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.post-item {
|
||||
margin-bottom: $block-spacing;
|
||||
background: $post-bg;
|
||||
color: $post-color;
|
||||
border-radius: $block-radius;
|
||||
width: calc(100% - #{$block-spacing});
|
||||
box-shadow: 2px 2px 3px 0px rgba(0, 0, 0, 0.5);
|
||||
|
||||
a {
|
||||
color: $post-color;
|
||||
&:hover {
|
||||
color: $post-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
margin: 0;
|
||||
}
|
||||
.signature {
|
||||
margin: $elem-spacing 0 0 0;
|
||||
text-align: right;
|
||||
font-style: italic;
|
||||
|
||||
img {
|
||||
vertical-align: baseline;
|
||||
margin: 0 0.2em calc((1em - 24px)/2) 0;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
.header {
|
||||
padding: 0 $block-spacing;
|
||||
position: relative;
|
||||
|
||||
div {
|
||||
display: inline-block;
|
||||
font-size: 0.8em;
|
||||
padding: $elem-spacing 0px;
|
||||
|
||||
&.index {
|
||||
width: 25%;
|
||||
|
||||
.link {
|
||||
margin-left: $elem-spacing;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.time {
|
||||
width: 75%;
|
||||
text-align: right;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
.body {
|
||||
clear: both;
|
||||
padding: 0em $block-spacing $block-spacing;
|
||||
}
|
||||
|
||||
&.headerless {
|
||||
.header {
|
||||
display: none;
|
||||
}
|
||||
.body {
|
||||
padding-top: $block-spacing;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
|
||||
.fa {
|
||||
display: inline-block;
|
||||
font-size: 2em;
|
||||
margin: $elem-spacing 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.message {
|
||||
background: $message-bg;
|
||||
color: $message-color;
|
||||
|
||||
p {
|
||||
font-size: 0.9em;
|
||||
height: 1em;
|
||||
margin: 0 0 $elem-spacing 0;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $message-color;
|
||||
&:hover {
|
||||
color: $message-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
a.drill {
|
||||
line-height: 0;
|
||||
|
||||
.drill-icon {
|
||||
transform: translate(-16px, -32px);
|
||||
|
||||
.fa-message-in {
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.fa-message {
|
||||
@extend .#{$fa-css-prefix}-drill-message;
|
||||
top: 13px;
|
||||
left: 3px;
|
||||
}
|
||||
.fa-message-in {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.weather {
|
||||
position: absolute;
|
||||
top: $block-spacing;
|
||||
right: $block-spacing;
|
||||
|
||||
.fa {
|
||||
font-size: 1.3em;
|
||||
vertical-align: middle;
|
||||
line-height: 1rem;
|
||||
background: $message-color;
|
||||
color: $message-bg;
|
||||
border-radius: $block-radius 0 0 $block-radius;
|
||||
padding: $elem-spacing;
|
||||
}
|
||||
|
||||
span {
|
||||
vertical-align: middle;
|
||||
padding: $elem-spacing;
|
||||
background: $message-bg;
|
||||
color: $message-color;
|
||||
border-radius: 0 $block-radius $block-radius 0;
|
||||
}
|
||||
}
|
||||
|
||||
.staticmap {
|
||||
width: 100%;
|
||||
border-radius: $block-radius;
|
||||
}
|
||||
}
|
||||
|
||||
&.post {
|
||||
.body {
|
||||
padding: 0em 1em 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
&.media {
|
||||
background: $media-bg;
|
||||
color: $media-color;
|
||||
|
||||
.body {
|
||||
a {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
color: $media-color;
|
||||
position: relative;
|
||||
line-height: 0;
|
||||
|
||||
&.drill {
|
||||
&:hover {
|
||||
.drill-icon .fa-drill-image, .drill-icon .fa-drill-video {
|
||||
color: rgba($media-bg, 0.75);
|
||||
}
|
||||
.comment {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.drill-icon {
|
||||
font-size: 3em;
|
||||
|
||||
.fa-drill-image {
|
||||
color: transparent;
|
||||
}
|
||||
.fa-drill-video {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
image-orientation: from-image;
|
||||
outline: none;
|
||||
border-radius: $block-radius;
|
||||
}
|
||||
|
||||
.comment {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
line-height: normal;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0.5em;
|
||||
text-align: justify;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border-radius: 0 0 $block-radius $block-radius;
|
||||
transition: opacity 0.3s;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
110
src/styles/_page.project.map.scss
Normal file
110
src/styles/_page.project.map.scss
Normal file
@@ -0,0 +1,110 @@
|
||||
#map {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
|
||||
/* Leaflet Popup */
|
||||
.maplibregl-popup-content {
|
||||
|
||||
h1 {
|
||||
font-size: 1.4em;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.separator {
|
||||
border-top: 1px solid #CCC;
|
||||
margin: $elem-spacing 0 $block-spacing 0;
|
||||
}
|
||||
|
||||
/* Marker Popup */
|
||||
.info-window {
|
||||
h1 .message-type {
|
||||
color: #CCC;
|
||||
font-weight: normal;
|
||||
font-size: calc(1em / 1.4);
|
||||
margin-left: 0.5em;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.0em;
|
||||
margin: $elem-spacing 0 0 0;
|
||||
|
||||
a {
|
||||
color: $post-color;
|
||||
}
|
||||
}
|
||||
|
||||
.medias {
|
||||
line-height: 0;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
|
||||
margin: $block-spacing $block-spacing 0 0;
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&.drill {
|
||||
font-size: 2em;
|
||||
|
||||
.fa-drill-image {
|
||||
color: transparent;
|
||||
}
|
||||
.fa-drill-video {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.fa-drill-video, .fa-drill-image {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 200px;
|
||||
max-height: 100px;
|
||||
border-radius: $block-radius;
|
||||
image-orientation: from-image;
|
||||
transition: All 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Track Popup */
|
||||
.track_tooltip {
|
||||
p {
|
||||
margin: 0;
|
||||
|
||||
&.description {
|
||||
font-size: 1.15em;
|
||||
}
|
||||
}
|
||||
|
||||
h1, .description {
|
||||
@include no-text-overflow();
|
||||
}
|
||||
.body {
|
||||
padding-left: calc(1.25em*1.4 + #{$elem-spacing} );
|
||||
|
||||
.details {
|
||||
margin-top: -$block-spacing;
|
||||
|
||||
p.detail {
|
||||
margin-top: $block-spacing;
|
||||
width: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
213
src/styles/_page.project.panel.scss
Normal file
213
src/styles/_page.project.panel.scss
Normal file
@@ -0,0 +1,213 @@
|
||||
$panel-width: 30vw;
|
||||
$panel-width-max: "400px + 3 * #{$block-spacing}";
|
||||
$panel-actual-width: min(#{$panel-width}, #{$panel-width-max});
|
||||
|
||||
#projects {
|
||||
&.with-feed, &.with-settings {
|
||||
#title {
|
||||
max-width: calc(100vw - #{$block-spacing} - #{$panel-actual-width} - (#{$button-width} + #{$block-spacing} * 2) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
&.with-feed {
|
||||
#submap {
|
||||
transform: translateX(calc(#{$panel-actual-width} / -2));
|
||||
}
|
||||
|
||||
.map-container-right {
|
||||
transform: translateX(calc(100vw - #{$panel-actual-width}));
|
||||
}
|
||||
}
|
||||
|
||||
&.with-settings {
|
||||
#submap {
|
||||
transform: translateX(calc(#{$panel-actual-width} / 2));
|
||||
}
|
||||
|
||||
.map-container-left {
|
||||
transform: translateX(0);
|
||||
|
||||
.map-panel {
|
||||
box-shadow: 2px 2px $block-shadow 0px rgba(0, 0, 0, .5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.with-feed.with-settings {
|
||||
#submap {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
#title {
|
||||
max-width: calc(100vw - #{$block-spacing} - #{$panel-actual-width} * 2 - (#{$button-width} + #{$block-spacing} * 2) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
.map-container { //#feed, #settings
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
user-select: none;
|
||||
width: #{$panel-width};
|
||||
max-width: calc(#{$panel-width-max});
|
||||
transition: transform 0.5s;
|
||||
|
||||
&.moving {
|
||||
cursor: grabbing;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.map-panel { //#feed-panel, #settings-panel
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
background-color: $post-input-bg;
|
||||
color: $post-color;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button, a.button {
|
||||
background-color: $post-color;
|
||||
color: $post-bg;
|
||||
|
||||
&:hover, &:hover a, &:hover a:visited {
|
||||
background-color: $post-input-bg;
|
||||
color: $post-color;
|
||||
}
|
||||
|
||||
a, a:visited {
|
||||
background-color: $post-color;
|
||||
color: $post-bg;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&+ button, &+ a.button {
|
||||
margin-left: $elem-spacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.map-container-left { //#settings
|
||||
transform: translateX(-100%);
|
||||
|
||||
.map-panel { //#settings-panel
|
||||
width: calc(100% - #{$block-spacing});
|
||||
margin: $block-spacing;
|
||||
border-radius: $block-radius;
|
||||
color: $post-color;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.map-container-right { //#feed
|
||||
transform: translateX(100vw);
|
||||
|
||||
.map-panel { //#feed-panel
|
||||
width: 100%;
|
||||
padding-top: $block-spacing;
|
||||
}
|
||||
}
|
||||
|
||||
.map-control {
|
||||
position: absolute;
|
||||
background-color: $post-bg;
|
||||
padding: $elem-spacing;
|
||||
border-radius: 3px;
|
||||
box-shadow: 2px 2px 3px 0px rgba(0, 0, 0, 0.5);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
|
||||
&.map-control-top {
|
||||
top: $block-spacing;
|
||||
}
|
||||
|
||||
&.map-control-bottom {
|
||||
bottom: $block-spacing;
|
||||
}
|
||||
|
||||
&.map-control-icon {
|
||||
cursor: pointer;
|
||||
|
||||
.fa {
|
||||
@extend .fa-fw;
|
||||
color: $post-color;
|
||||
}
|
||||
|
||||
&:hover .fa {
|
||||
color: #000000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.feed-control {
|
||||
right: calc(100% + $block-spacing);
|
||||
}
|
||||
|
||||
.settings-control {
|
||||
left: calc(100% + $block-spacing);
|
||||
}
|
||||
|
||||
#legend {
|
||||
.track {
|
||||
white-space: nowrap;
|
||||
.line {
|
||||
width: 2em;
|
||||
display: inline-block;
|
||||
border-radius: 2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 1em;
|
||||
margin-left: 0.5em;
|
||||
color: $legend-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#title {
|
||||
left: calc(100% + #{$button-width} + 2 * #{$block-spacing});
|
||||
max-width: calc(100vw - #{$block-spacing} - (#{$button-width} + 2 * #{$block-spacing}) * 2);
|
||||
transition: max-width 0.5s;
|
||||
@include no-text-overflow();
|
||||
|
||||
span {
|
||||
font-size: 1.3em;
|
||||
line-height: $block-spacing;
|
||||
}
|
||||
}
|
||||
|
||||
#background {
|
||||
background: #666;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#submap {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
transition: transform 0.5s;
|
||||
|
||||
.loader {
|
||||
position: absolute;
|
||||
font-size: 3em;
|
||||
top: calc(50% - 0.5em);
|
||||
left: calc(50% - 1.25em/2);
|
||||
color: #CCC;
|
||||
}
|
||||
}
|
||||
}
|
||||
80
src/styles/_page.project.scss
Normal file
80
src/styles/_page.project.scss
Normal file
@@ -0,0 +1,80 @@
|
||||
//Feed width
|
||||
$elem-spacing: 0.5rem;
|
||||
$block-spacing: 1rem;
|
||||
$block-radius: 3px;
|
||||
$block-shadow: 3px;
|
||||
$button-width: 31px;
|
||||
|
||||
//Feed colors
|
||||
$post-input-bg: #ffffff;
|
||||
$post-color: #333;
|
||||
$post-color-hover: darken($post-color, 10%);
|
||||
$post-bg: rgba(255, 255, 255, .8);
|
||||
$message-color: #326526;
|
||||
$message-color-hover: darken($message-color, 10%);
|
||||
$message-bg: #6DFF58;
|
||||
$media-color: #333;
|
||||
$media-bg: rgba(255, 255, 255, .8);
|
||||
|
||||
//Settings colors
|
||||
$title-color: $post-color;
|
||||
$subtitle-color: #999;
|
||||
|
||||
//Legend colors
|
||||
$legend-color: $post-color;
|
||||
|
||||
@import 'page.project.map';
|
||||
@import 'page.project.panel';
|
||||
@import 'page.project.feed';
|
||||
@import 'page.project.settings';
|
||||
|
||||
#projects {
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
/* Drill & Map icons */
|
||||
|
||||
a.drill {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
|
||||
.drill-icon {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%,-50%);
|
||||
|
||||
i {
|
||||
transition: color 0.3s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fa-stack {
|
||||
.fa-message {
|
||||
font-size: 32px;
|
||||
text-shadow: rgba(0, 0, 0, 0.5) 3px 3px 3px;
|
||||
color: $message-bg;
|
||||
}
|
||||
.fa-message-in {
|
||||
font-size: 13px;
|
||||
color: $message-color;
|
||||
top: 1px;
|
||||
}
|
||||
.fa-track-start, .fa-track-end {
|
||||
color: $message-color;
|
||||
font-size: 14px;
|
||||
top: 1px;
|
||||
}
|
||||
.fa-track-end {
|
||||
color: #FF7814;
|
||||
}
|
||||
}
|
||||
}
|
||||
142
src/styles/_page.project.settings.scss
Normal file
142
src/styles/_page.project.settings.scss
Normal file
@@ -0,0 +1,142 @@
|
||||
#settings {
|
||||
#settings-panel {
|
||||
.settings-header {
|
||||
text-align: center;
|
||||
flex: 0 1 auto;
|
||||
|
||||
.logo {
|
||||
background: rgba(255, 255, 255, .4);
|
||||
padding: 2rem 1rem;
|
||||
border-radius: $block-radius $block-radius 0 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 180px;
|
||||
transform: translateX(-10%); //Center Text, not logo. logo width (40px) / image width (200px) = 20%. And centering: 20% / 2 = 10%
|
||||
}
|
||||
}
|
||||
|
||||
#last_update {
|
||||
position: absolute;
|
||||
margin-top: -2em;
|
||||
padding: 0 1rem;
|
||||
width: calc(100% - 2rem);
|
||||
|
||||
p {
|
||||
text-align: center;
|
||||
font-size: 0.8em;
|
||||
margin: 0;
|
||||
color: $subtitle-color;
|
||||
transform: translateX(calc(-0.5 * (12px + 0.5em))); //icon width + margin right
|
||||
|
||||
span {
|
||||
margin-right: 0.5em;
|
||||
img {
|
||||
width: 12px;
|
||||
vertical-align: middle;
|
||||
animation: spotlogo 20s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
abbr {
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-footer {
|
||||
flex: 0 1 auto;
|
||||
background: rgba(255, 255, 255, .4);
|
||||
border-radius: 0 0 3px 3px;
|
||||
font-size: 0.7em;
|
||||
padding: 0.3rem;
|
||||
text-align: center;
|
||||
color: #888;
|
||||
|
||||
a {
|
||||
color: #777;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-sections {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
|
||||
#settings-sections-scrollbox {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
display: inline-block;
|
||||
margin: 1.5rem 1rem 0 1rem;
|
||||
width: calc(100% - 2 * #{$block-spacing});
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 $block-spacing;
|
||||
color: $title-color;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.settings-section-body {
|
||||
.radio {
|
||||
&:not(:first-child) {
|
||||
margin-top: $elem-spacing;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-left: .3rem;
|
||||
@extend .clickable;
|
||||
@include no-text-overflow();
|
||||
}
|
||||
|
||||
.download {
|
||||
color: $legend-color;
|
||||
|
||||
&:hover {
|
||||
color: #0078A8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.newsletter {
|
||||
input#email {
|
||||
width: calc(100% - 6em);
|
||||
|
||||
&:disabled {
|
||||
color: #999;
|
||||
background: rgba(255,255,255,0.2);
|
||||
}
|
||||
}
|
||||
button#nl_btn {
|
||||
margin-left: 1em;
|
||||
margin-bottom: 1em;
|
||||
|
||||
&.subscribe .fa {
|
||||
@extend .fa-send;
|
||||
}
|
||||
&.unsubscribe .fa {
|
||||
@extend .fa-unsubscribe;
|
||||
}
|
||||
&.loading {
|
||||
background-color: $message-color;
|
||||
color: white;
|
||||
span {
|
||||
@extend .flicker;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
53
src/styles/_page.upload.scss
Normal file
53
src/styles/_page.upload.scss
Normal file
@@ -0,0 +1,53 @@
|
||||
#upload {
|
||||
padding: 1em;
|
||||
|
||||
.section {
|
||||
border-radius: 3px;
|
||||
margin-top: 1rem;
|
||||
padding: 0 1rem 1rem 1rem;
|
||||
border-bottom: 1px solid #EEE;
|
||||
}
|
||||
|
||||
.progress {
|
||||
.bar {
|
||||
height: 18px;
|
||||
background: green;
|
||||
}
|
||||
}
|
||||
|
||||
.comment {
|
||||
.thumb {
|
||||
width: 30%;
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: inline-block;
|
||||
width: calc(70% - 2rem);
|
||||
min-width: calc(100% - 100px - 2rem);
|
||||
padding: 1rem;
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
|
||||
.content {
|
||||
width: calc(100% - 2rem);
|
||||
box-sizing: border-box;
|
||||
padding: 0.5em;
|
||||
background: #EEE;
|
||||
}
|
||||
|
||||
.save {
|
||||
margin-top: 1rem;
|
||||
padding: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logs {
|
||||
padding: 1rem;
|
||||
|
||||
p.log {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
[data-simplebar] {
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
align-content: flex-start;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.simplebar-wrapper {
|
||||
overflow: hidden;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
max-width: inherit;
|
||||
max-height: inherit;
|
||||
}
|
||||
|
||||
.simplebar-mask {
|
||||
direction: inherit;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.simplebar-offset {
|
||||
direction: inherit !important;
|
||||
box-sizing: inherit !important;
|
||||
resize: none !important;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.simplebar-content-wrapper {
|
||||
direction: inherit;
|
||||
box-sizing: border-box !important;
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 100%; /* Required for horizontal native scrollbar to not appear if parent is taller than natural height */
|
||||
width: auto;
|
||||
max-width: 100%; /* Not required for horizontal scroll to trigger */
|
||||
max-height: 100%; /* Needed for vertical scroll to trigger */
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.simplebar-content-wrapper::-webkit-scrollbar,
|
||||
.simplebar-hide-scrollbar::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.simplebar-content:before,
|
||||
.simplebar-content:after {
|
||||
content: ' ';
|
||||
display: table;
|
||||
}
|
||||
|
||||
.simplebar-placeholder {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.simplebar-height-auto-observer-wrapper {
|
||||
box-sizing: inherit !important;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: 1px;
|
||||
position: relative;
|
||||
float: left;
|
||||
max-height: 1px;
|
||||
overflow: hidden;
|
||||
z-index: -1;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
pointer-events: none;
|
||||
flex-grow: inherit;
|
||||
flex-shrink: 0;
|
||||
flex-basis: 0;
|
||||
}
|
||||
|
||||
.simplebar-height-auto-observer {
|
||||
box-sizing: inherit;
|
||||
display: block;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 1000%;
|
||||
width: 1000%;
|
||||
min-height: 1px;
|
||||
min-width: 1px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.simplebar-track {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-simplebar].simplebar-dragging .simplebar-content {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
[data-simplebar].simplebar-dragging .simplebar-track {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.simplebar-scrollbar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
min-height: 10px;
|
||||
}
|
||||
|
||||
.simplebar-scrollbar:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
background: black;
|
||||
border-radius: 7px;
|
||||
left: 2px;
|
||||
right: 2px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
|
||||
.simplebar-scrollbar.simplebar-visible:before {
|
||||
/* When hovered, remove all transitions from drag handle */
|
||||
opacity: 0.5;
|
||||
transition: opacity 0s linear;
|
||||
}
|
||||
|
||||
.simplebar-track.simplebar-vertical {
|
||||
top: 0;
|
||||
width: 11px;
|
||||
}
|
||||
|
||||
.simplebar-track.simplebar-vertical .simplebar-scrollbar:before {
|
||||
top: 2px;
|
||||
bottom: 2px;
|
||||
}
|
||||
|
||||
.simplebar-track.simplebar-horizontal {
|
||||
left: 0;
|
||||
height: 11px;
|
||||
}
|
||||
|
||||
.simplebar-track.simplebar-horizontal .simplebar-scrollbar:before {
|
||||
height: 100%;
|
||||
left: 2px;
|
||||
right: 2px;
|
||||
}
|
||||
|
||||
.simplebar-track.simplebar-horizontal .simplebar-scrollbar {
|
||||
right: auto;
|
||||
left: 0;
|
||||
top: 2px;
|
||||
height: 7px;
|
||||
min-height: 0;
|
||||
min-width: 10px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Rtl support */
|
||||
[data-simplebar-direction='rtl'] .simplebar-track.simplebar-vertical {
|
||||
right: auto;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.hs-dummy-scrollbar-size {
|
||||
direction: rtl;
|
||||
position: fixed;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
height: 500px;
|
||||
width: 500px;
|
||||
overflow-y: hidden;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
.simplebar-hide-scrollbar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
visibility: hidden;
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
9
src/styles/_vue.scss
Normal file
9
src/styles/_vue.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -13,10 +13,13 @@
|
||||
.#{$fa-css-prefix}-solid,
|
||||
.far,
|
||||
.#{$fa-css-prefix}-regular,
|
||||
.fasr,
|
||||
.fal,
|
||||
.#{$fa-css-prefix}-light,
|
||||
.fasl,
|
||||
.fat,
|
||||
.#{$fa-css-prefix}-thin,
|
||||
.fast,
|
||||
.fad,
|
||||
.#{$fa-css-prefix}-duotone,
|
||||
.fass,
|
||||
@@ -56,8 +59,14 @@
|
||||
}
|
||||
|
||||
.fass,
|
||||
.fasr,
|
||||
.fasl,
|
||||
.fast,
|
||||
.#{$fa-css-prefix}-sharp {
|
||||
font-family: 'Font Awesome 6 Sharp';
|
||||
}
|
||||
.fass,
|
||||
.#{$fa-css-prefix}-sharp {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
|
||||
/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen readers do not read off random characters that represent icons */
|
||||
|
||||
|
||||
@each $name, $icon in $fa-icons {
|
||||
.fad.#{$fa-css-prefix}-#{$name}::after, .#{$fa-css-prefix}-duotone.#{$fa-css-prefix}-#{$name}::after {
|
||||
content: unquote("\"#{ $icon }#{ $icon }\"");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,3 +7,4 @@ readers do not read off random characters that represent icons */
|
||||
@each $name, $icon in $fa-icons {
|
||||
.#{$fa-css-prefix}-#{$name}::before { content: unquote("\"#{ $icon }\""); }
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +110,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
@mixin fa-icon-sharp-regular($fa-var) {
|
||||
@extend %fa-icon;
|
||||
@extend .fa-sharp-regular;
|
||||
|
||||
&::before {
|
||||
content: unquote("\"#{ $fa-var }\"");
|
||||
}
|
||||
}
|
||||
|
||||
@mixin fa-icon-sharp-light($fa-var) {
|
||||
@extend %fa-icon;
|
||||
@extend .fa-sharp-light;
|
||||
|
||||
&::before {
|
||||
content: unquote("\"#{ $fa-var }\"");
|
||||
}
|
||||
}
|
||||
|
||||
@mixin fa-icon-sharp-thin($fa-var) {
|
||||
@extend %fa-icon;
|
||||
@extend .fa-sharp-thin;
|
||||
|
||||
&::before {
|
||||
content: unquote("\"#{ $fa-var }\"");
|
||||
}
|
||||
}
|
||||
|
||||
@mixin fa-icon-brands($fa-var) {
|
||||
@extend %fa-icon;
|
||||
@extend .fa-brands;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
/*!
|
||||
* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com
|
||||
* Font Awesome Pro 6.5.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license (Commercial License)
|
||||
* Copyright 2022 Fonticons, Inc.
|
||||
* Copyright 2023 Fonticons, Inc.
|
||||
*/
|
||||
@import 'functions';
|
||||
@import 'variables';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*!
|
||||
* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com
|
||||
* Font Awesome Pro 6.5.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license (Commercial License)
|
||||
* Copyright 2022 Fonticons, Inc.
|
||||
* Copyright 2023 Fonticons, Inc.
|
||||
*/
|
||||
@import 'functions';
|
||||
@import 'variables';
|
||||
|
||||
4
src/styles/fa/fontawesome.scss
vendored
4
src/styles/fa/fontawesome.scss
vendored
@@ -1,7 +1,7 @@
|
||||
/*!
|
||||
* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com
|
||||
* Font Awesome Pro 6.5.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license (Commercial License)
|
||||
* Copyright 2022 Fonticons, Inc.
|
||||
* Copyright 2023 Fonticons, Inc.
|
||||
*/
|
||||
// Font Awesome core compile (Web Fonts-based)
|
||||
// -------------------------
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
src/styles/fa/fonts/fa-sharp-light-300.ttf
Normal file
BIN
src/styles/fa/fonts/fa-sharp-light-300.ttf
Normal file
Binary file not shown.
BIN
src/styles/fa/fonts/fa-sharp-light-300.woff2
Normal file
BIN
src/styles/fa/fonts/fa-sharp-light-300.woff2
Normal file
Binary file not shown.
BIN
src/styles/fa/fonts/fa-sharp-regular-400.ttf
Normal file
BIN
src/styles/fa/fonts/fa-sharp-regular-400.ttf
Normal file
Binary file not shown.
BIN
src/styles/fa/fonts/fa-sharp-regular-400.woff2
Normal file
BIN
src/styles/fa/fonts/fa-sharp-regular-400.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
src/styles/fa/fonts/fa-sharp-thin-100.ttf
Normal file
BIN
src/styles/fa/fonts/fa-sharp-thin-100.ttf
Normal file
Binary file not shown.
BIN
src/styles/fa/fonts/fa-sharp-thin-100.woff2
Normal file
BIN
src/styles/fa/fonts/fa-sharp-thin-100.woff2
Normal file
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user