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
|
/vendor/
|
||||||
/style/.sass-cache/
|
/config/settings.php
|
||||||
/files/**/*.jpg
|
/files/
|
||||||
/files/**/*.JPG
|
/geo/
|
||||||
/files/**/*.jpeg
|
/node_modules/
|
||||||
/files/**/*.JPEG
|
/log.html
|
||||||
/files/**/*.png
|
/dist/
|
||||||
/files/**/*.PNG
|
|
||||||
/files/**/*.mov
|
|
||||||
/files/**/*.MOV
|
|
||||||
/geo/*.geojson
|
|
||||||
/spot_cron.sh
|
|
||||||
/vendor/*
|
|
||||||
/log.html
|
|
||||||
|
|||||||
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": {
|
"require": {
|
||||||
"franzz/objects": "dev-composer",
|
"franzz/objects": "dev-vue",
|
||||||
"phpmailer/phpmailer": "^6.5"
|
"phpmailer/phpmailer": "^6.5"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Franzz\\Spot\\": "api/"
|
"Franzz\\Spot\\": "lib/"
|
||||||
},
|
},
|
||||||
"files": [
|
"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",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "164c903fea5bdcfb36cf6ea31ec0c307",
|
"content-hash": "12bb836a394b645df50c14652a2ae5bf",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "franzz/objects",
|
"name": "franzz/objects",
|
||||||
"version": "dev-composer",
|
"version": "dev-vue",
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "path",
|
"type": "path",
|
||||||
"url": "../objects",
|
"url": "../objects",
|
||||||
"reference": "e1cf78b992a6f52742d6834f7508c0ef373ac860"
|
"reference": "bcae723140735b1432caaf3070ef4e29ecb73a76"
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"autoload": {
|
"autoload": {
|
||||||
@@ -27,16 +27,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpmailer/phpmailer",
|
"name": "phpmailer/phpmailer",
|
||||||
"version": "v6.8.0",
|
"version": "v6.10.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
||||||
"reference": "df16b615e371d81fb79e506277faea67a1be18f1"
|
"reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/df16b615e371d81fb79e506277faea67a1be18f1",
|
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
|
||||||
"reference": "df16b615e371d81fb79e506277faea67a1be18f1",
|
"reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -46,16 +46,17 @@
|
|||||||
"php": ">=5.5.0"
|
"php": ">=5.5.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.2",
|
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
|
||||||
"doctrine/annotations": "^1.2.6 || ^1.13.3",
|
"doctrine/annotations": "^1.2.6 || ^1.13.3",
|
||||||
"php-parallel-lint/php-console-highlighter": "^1.0.0",
|
"php-parallel-lint/php-console-highlighter": "^1.0.0",
|
||||||
"php-parallel-lint/php-parallel-lint": "^1.3.2",
|
"php-parallel-lint/php-parallel-lint": "^1.3.2",
|
||||||
"phpcompatibility/php-compatibility": "^9.3.5",
|
"phpcompatibility/php-compatibility": "^9.3.5",
|
||||||
"roave/security-advisories": "dev-latest",
|
"roave/security-advisories": "dev-latest",
|
||||||
"squizlabs/php_codesniffer": "^3.7.1",
|
"squizlabs/php_codesniffer": "^3.7.2",
|
||||||
"yoast/phpunit-polyfills": "^1.0.4"
|
"yoast/phpunit-polyfills": "^1.0.4"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
|
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
|
||||||
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
|
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
|
||||||
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
|
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
|
||||||
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
|
"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",
|
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
|
"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": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -103,7 +104,7 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2023-03-06T14:43:22+00:00"
|
"time": "2025-04-24T15:19:31+00:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packages-dev": [],
|
"packages-dev": [],
|
||||||
@@ -114,7 +115,7 @@
|
|||||||
},
|
},
|
||||||
"prefer-stable": false,
|
"prefer-stable": false,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": [],
|
"platform": {},
|
||||||
"platform-dev": [],
|
"platform-dev": {},
|
||||||
"plugin-api-version": "2.3.0"
|
"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_NAME = 'spot';
|
||||||
const DB_ENC = 'utf8mb4';
|
const DB_ENC = 'utf8mb4';
|
||||||
const TEXT_ENC = 'UTF-8';
|
const TEXT_ENC = 'UTF-8';
|
||||||
const TIMEZONE = 'Europe/Paris';
|
const TIMEZONE = 'Europe/Zurich';
|
||||||
const MAIL_SERVER = '';
|
const MAIL_SERVER = '';
|
||||||
const MAIL_FROM = '';
|
const MAIL_FROM = '';
|
||||||
const MAIL_USER = '';
|
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
|
page_og_desc = Keep contact with François when he is off hiking
|
||||||
error_commit_db = Issue committing to DB
|
error_commit_db = Issue committing to DB
|
||||||
unknown_field = Field "$0" is unknown
|
unknown_field = Field "$0" is unknown
|
||||||
|
impossible_value = Value "$0" is not possible for field "$1"
|
||||||
|
|
||||||
nav_back = Back
|
nav_back = Back
|
||||||
|
|
||||||
@@ -10,6 +11,9 @@ admin_config = Config
|
|||||||
admin_upload = Upload
|
admin_upload = Upload
|
||||||
save = Save
|
save = Save
|
||||||
admin_save_success = Saved
|
admin_save_success = Saved
|
||||||
|
admin_create_success= Created
|
||||||
|
admin_delete_success= Deleted
|
||||||
|
no_auth = No authorization
|
||||||
|
|
||||||
track_main = Main track
|
track_main = Main track
|
||||||
track_off-track = Off-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_mode_archived= Project "$0" is archived. No upload allowed
|
||||||
upload_success = $0 uploaded successfully
|
upload_success = $0 uploaded successfully
|
||||||
upload_media_exist = Picture $0 already exists
|
upload_media_exist = Picture $0 already exists
|
||||||
|
new_position = New Position
|
||||||
|
|
||||||
post_message = Message
|
post_message = Message
|
||||||
post_name = Name
|
post_name = Name
|
||||||
@@ -63,7 +68,7 @@ id_project = Project ID
|
|||||||
project = Project
|
project = Project
|
||||||
projects = Projects
|
projects = Projects
|
||||||
new_project = New Project
|
new_project = New Project
|
||||||
update_project = Update Project
|
update_project = Update project messages
|
||||||
hikes = Hikes
|
hikes = Hikes
|
||||||
mode = Mode
|
mode = Mode
|
||||||
mode_previz = Project in preparation
|
mode_previz = Project in preparation
|
||||||
@@ -75,6 +80,7 @@ end = End
|
|||||||
feeds = Feeds
|
feeds = Feeds
|
||||||
id_feed = Feed ID
|
id_feed = Feed ID
|
||||||
ref_feed_id = Ref. Feed ID
|
ref_feed_id = Ref. Feed ID
|
||||||
|
new_feed = New feed
|
||||||
id_spot = Spot ID
|
id_spot = Spot ID
|
||||||
name = Name
|
name = Name
|
||||||
status = Status
|
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
|
page_og_desc = Mantente en contacto con François durante sus aventuras a la montaña
|
||||||
error_commit_db = Error SQL
|
error_commit_db = Error SQL
|
||||||
unknown_field = Campo "$0" desconocido
|
unknown_field = Campo "$0" desconocido
|
||||||
|
impossible_value = Valor "$0" no es posible para campo "$1"
|
||||||
|
|
||||||
nav_back = Atrás
|
nav_back = Atrás
|
||||||
|
|
||||||
@@ -10,6 +11,9 @@ admin_config = Configuración
|
|||||||
admin_upload = Cargar
|
admin_upload = Cargar
|
||||||
save = Guardar
|
save = Guardar
|
||||||
admin_save_success = Guardado
|
admin_save_success = Guardado
|
||||||
|
admin_create_success= Creado
|
||||||
|
admin_delete_success= Eliminado
|
||||||
|
no_auth = No autorización
|
||||||
|
|
||||||
track_main = Camino principal
|
track_main = Camino principal
|
||||||
track_off-track = Variante
|
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_mode_archived= El proyecto "$0" esta archivado. No se puede cargar
|
||||||
upload_success = $0 ha sido subido
|
upload_success = $0 ha sido subido
|
||||||
upload_media_exist = La imagen $0 ya existe
|
upload_media_exist = La imagen $0 ya existe
|
||||||
|
new_position = Nueva posición
|
||||||
|
|
||||||
post_message = Mensaje
|
post_message = Mensaje
|
||||||
post_name = Nombre
|
post_name = Nombre
|
||||||
@@ -63,7 +68,7 @@ id_project = Proyecto ID
|
|||||||
project = Proyecto
|
project = Proyecto
|
||||||
projects = Proyectos
|
projects = Proyectos
|
||||||
new_project = Nuevo proyecto
|
new_project = Nuevo proyecto
|
||||||
update_project = Actualizar el proyecto
|
update_project = Actualizar los mensajes del proyecto
|
||||||
hikes = Senderos
|
hikes = Senderos
|
||||||
mode = Modo
|
mode = Modo
|
||||||
mode_previz = Proyecto en preparación
|
mode_previz = Proyecto en preparación
|
||||||
@@ -75,6 +80,7 @@ end = Fin
|
|||||||
feeds = Feeds
|
feeds = Feeds
|
||||||
id_feed = ID Feed
|
id_feed = ID Feed
|
||||||
ref_feed_id = ID Feed ref.
|
ref_feed_id = ID Feed ref.
|
||||||
|
new_feed = Nuevo feed
|
||||||
id_spot = ID Spot
|
id_spot = ID Spot
|
||||||
name = Descripción
|
name = Descripción
|
||||||
status = Estado
|
status = Estado
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ locale = fr_CH
|
|||||||
page_og_desc = Gardez le contact avec François lorsqu'il part sur les chemins
|
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
|
error_commit_db = Error lors de la requête SQL
|
||||||
unknown_field = Champ "$0" inconnu
|
unknown_field = Champ "$0" inconnu
|
||||||
|
impossible_value = La valeur "$0" n'est pas possible pour le champ "$1"
|
||||||
|
|
||||||
nav_back = Retour
|
nav_back = Retour
|
||||||
|
|
||||||
@@ -10,6 +11,9 @@ admin_config = Paramètres
|
|||||||
admin_upload = Uploader
|
admin_upload = Uploader
|
||||||
save = Sauvegarder
|
save = Sauvegarder
|
||||||
admin_save_success = Sauvegardé
|
admin_save_success = Sauvegardé
|
||||||
|
admin_create_success= Créé
|
||||||
|
admin_delete_success= Supprimé
|
||||||
|
no_auth = Pas d'authorisation
|
||||||
|
|
||||||
track_main = Trajet principal
|
track_main = Trajet principal
|
||||||
track_off-track = Variante
|
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_mode_archived= Le projet "$0" a été archivé. Aucun upload possible
|
||||||
upload_success = $0 a été uploadé
|
upload_success = $0 a été uploadé
|
||||||
upload_media_exist = l'image $0 existe déjà
|
upload_media_exist = l'image $0 existe déjà
|
||||||
|
new_position = Nouvelle position
|
||||||
|
|
||||||
post_message = Message
|
post_message = Message
|
||||||
post_name = Nom
|
post_name = Nom
|
||||||
@@ -63,7 +68,7 @@ id_project = ID projet
|
|||||||
project = Projet
|
project = Projet
|
||||||
projects = Projets
|
projects = Projets
|
||||||
new_project = Nouveau projet
|
new_project = Nouveau projet
|
||||||
update_project = Mettre à jour le projet
|
update_project = Mettre à jour les messages du projet
|
||||||
hikes = Randonnées
|
hikes = Randonnées
|
||||||
mode = Mode
|
mode = Mode
|
||||||
mode_previz = Projet en cours de préparation
|
mode_previz = Projet en cours de préparation
|
||||||
@@ -75,6 +80,7 @@ end = Arrivée
|
|||||||
feeds = Feeds
|
feeds = Feeds
|
||||||
id_feed = ID Feed
|
id_feed = ID Feed
|
||||||
ref_feed_id = ID Feed ref.
|
ref_feed_id = ID Feed ref.
|
||||||
|
new_feed = Nouveau feed
|
||||||
id_spot = ID Spot
|
id_spot = ID Spot
|
||||||
name = Description
|
name = Description
|
||||||
status = Statut
|
status = Statut
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
namespace Franzz\Spot;
|
namespace Franzz\Spot;
|
||||||
use Franzz\Objects\PhpObject;
|
use Franzz\Objects\PhpObject;
|
||||||
use Franzz\Objects\ToolBox;
|
|
||||||
use \Settings;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GPX to GeoJSON Converter
|
* GPX to GeoJSON Converter
|
||||||
@@ -35,298 +33,10 @@ class Converter extends PhpObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static function isGeoJsonValid($sCodeName) {
|
public static function isGeoJsonValid($sCodeName) {
|
||||||
$bResult = false;
|
|
||||||
$sGpxFilePath = Gpx::getFilePath($sCodeName);
|
$sGpxFilePath = Gpx::getFilePath($sCodeName);
|
||||||
$sGeoJsonFilePath = GeoJson::getFilePath($sCodeName);
|
$sGeoJsonFilePath = GeoJson::getFilePath($sCodeName);
|
||||||
|
|
||||||
//No need to generate if gpx is missing
|
//No need to generate if gpx is missing
|
||||||
if(!file_exists($sGpxFilePath) || file_exists($sGeoJsonFilePath) && filemtime($sGeoJsonFilePath) > filemtime(Gpx::getFilePath($sCodeName))) $bResult = true;
|
return !file_exists($sGpxFilePath) || file_exists($sGeoJsonFilePath) && filemtime($sGeoJsonFilePath) >= filemtime($sGpxFilePath);
|
||||||
return $bResult;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
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() {
|
private function updateFeed() {
|
||||||
$bNewMsg = false;
|
$bNewMsg = false;
|
||||||
$asData = $this->retrieveFeed();
|
$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';
|
const MAPPING_TABLE = 'mappings';
|
||||||
|
|
||||||
private Db $oDb;
|
private Db $oDb;
|
||||||
|
|
||||||
private $asMaps;
|
private $asMaps;
|
||||||
|
|
||||||
public function __construct(Db &$oDb) {
|
public function __construct(Db &$oDb) {
|
||||||
parent::__construct(__CLASS__);
|
parent::__construct(__CLASS__);
|
||||||
$this->oDb = &$oDb;
|
$this->oDb = &$oDb;
|
||||||
$this->setMaps();
|
$this->asMaps = array();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function setMaps() {
|
private function setMaps() {
|
||||||
@@ -25,14 +24,36 @@ class Map extends PhpObject {
|
|||||||
foreach($asMaps as $asMap) $this->asMaps[$asMap['codename']] = $asMap;
|
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) {
|
public function getProjectMaps($iProjectId) {
|
||||||
$asMappings = $this->oDb->getArrayQuery("SELECT id_map FROM mappings WHERE id_project = ".$iProjectId." OR id_project IS NULL", true);
|
$asMappings = $this->oDb->selectRows(
|
||||||
return array_filter($this->asMaps, function($asMap) use($asMappings) {return in_array($asMap['id_map'], $asMappings);});
|
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) {
|
public function getMapUrl($sCodeName, $asParams) {
|
||||||
$asParams['token'] = $this->asMaps[$sCodeName]['token'];
|
$asMap = $this->getMaps($sCodeName);
|
||||||
return self::populateParams($this->asMaps[$sCodeName]['pattern'], $asParams);
|
$asParams['token'] = $asMap['token'];
|
||||||
|
return self::populateParams($asMap['pattern'], $asParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function populateParams($sUrl, $asParams) {
|
private static function populateParams($sUrl, $asParams) {
|
||||||
|
|||||||
@@ -12,35 +12,26 @@ class Media extends PhpObject {
|
|||||||
const MEDIA_TABLE = 'medias';
|
const MEDIA_TABLE = 'medias';
|
||||||
|
|
||||||
//Media folders
|
//Media folders
|
||||||
const MEDIA_FOLDER = '../files/';
|
const MEDIA_FOLDER = 'files/';
|
||||||
const THUMB_FOLDER = self::MEDIA_FOLDER.'thumbs/';
|
const THUMB_FOLDER = self::MEDIA_FOLDER.'thumbs/';
|
||||||
|
|
||||||
const THUMB_MAX_WIDTH = 400;
|
const THUMB_MAX_WIDTH = 400;
|
||||||
|
|
||||||
/**
|
private Db $oDb;
|
||||||
* Database Handle
|
private Project $oProject;
|
||||||
* @var Db
|
|
||||||
*/
|
|
||||||
private $oDb;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Media Project
|
|
||||||
* @var Project
|
|
||||||
*/
|
|
||||||
private $oProject;
|
|
||||||
private $asMedia;
|
private $asMedia;
|
||||||
private $asMedias;
|
private $asMedias;
|
||||||
private $sSystemType;
|
//private $sSystemType;
|
||||||
|
|
||||||
private $iMediaId;
|
private $iMediaId;
|
||||||
|
|
||||||
public function __construct(Db &$oDb, &$oProject, $iMediaId=0) {
|
public function __construct(Db &$oDb, Project &$oProject, $iMediaId=0) {
|
||||||
parent::__construct(__CLASS__);
|
parent::__construct(__CLASS__);
|
||||||
$this->oDb = &$oDb;
|
$this->oDb = &$oDb;
|
||||||
$this->oProject = &$oProject;
|
$this->oProject = &$oProject;
|
||||||
$this->asMedia = array();
|
$this->asMedia = array();
|
||||||
$this->asMedias = 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);
|
$this->setMediaId($iMediaId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -142,15 +142,19 @@ class Project extends PhpObject {
|
|||||||
}
|
}
|
||||||
$asProject['editable'] = $this->isModeEditable($asProject['mode']);
|
$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['gpxfilepath'] = Spot::addTimestampToFilePath(Gpx::getDistFilePath($sCodeName));
|
||||||
$asProject['codename'] = $sCodeName;
|
$asProject['codename'] = $sCodeName;
|
||||||
}
|
}
|
||||||
return $bSpecificProj?$asProject:$asProjects;
|
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() {
|
public function getProject() {
|
||||||
return $this->getProjects($this->getProjectId());
|
return $this->getProjects($this->getProjectId());
|
||||||
}
|
}
|
||||||
@@ -185,7 +189,7 @@ class Project extends PhpObject {
|
|||||||
$this->sCodeName = $asProject['codename'];
|
$this->sCodeName = $asProject['codename'];
|
||||||
$this->sMode = $asProject['mode'];
|
$this->sMode = $asProject['mode'];
|
||||||
$this->asActive = array('from'=>$asProject['active_from'], 'to'=>$asProject['active_to']);
|
$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');
|
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`):
|
* - Posts (table `posts`):
|
||||||
* - site_time: timestamp in Site Time
|
* - site_time: timestamp in Site Time
|
||||||
* - timezone: Local Timezone
|
* - timezone: Local Timezone
|
||||||
|
* - Users (table `users`):
|
||||||
|
* - timezone: Site Timezone (stored user's timezone for emails)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class Spot extends Main
|
class Spot extends Main
|
||||||
@@ -148,7 +150,8 @@ class Spot extends Main
|
|||||||
Project::PROJ_TABLE => "UNIQUE KEY `uni_proj_name` (`codename`)",
|
Project::PROJ_TABLE => "UNIQUE KEY `uni_proj_name` (`codename`)",
|
||||||
Media::MEDIA_TABLE => "UNIQUE KEY `uni_file_name` (`filename`)",
|
Media::MEDIA_TABLE => "UNIQUE KEY `uni_file_name` (`filename`)",
|
||||||
User::USER_TABLE => "UNIQUE KEY `uni_email` (`email`)",
|
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
|
'cascading_delete' => array
|
||||||
(
|
(
|
||||||
@@ -160,7 +163,7 @@ class Spot extends Main
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAppParams() {
|
public function getAppMainPage() {
|
||||||
|
|
||||||
//Cache Page List
|
//Cache Page List
|
||||||
$asPages = array_diff($this->asMasks, array('email_update', 'email_conf'));
|
$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'));
|
$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(
|
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,
|
self::MAIN_PAGE,
|
||||||
array(
|
array(
|
||||||
'language' => $this->oLang->getLanguage(),
|
'language' => $this->oLang->getLanguage(),
|
||||||
'host_url' => $this->asContext['serv_name'],
|
|
||||||
'filepath_css' => self::addTimestampToFilePath('spot.css'),
|
'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);
|
$this->oProject->setProjectId($iProjectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getProjectGeoJson() {
|
||||||
|
return self::getJsonResult(true, '', $this->oProject->getGeoJson());
|
||||||
|
}
|
||||||
|
|
||||||
public function updateProject() {
|
public function updateProject() {
|
||||||
$bNewMsg = false;
|
$bNewMsg = false;
|
||||||
$bSuccess = true;
|
$bSuccess = true;
|
||||||
@@ -223,34 +223,7 @@ class Spot extends Main
|
|||||||
|
|
||||||
//Send Update Email
|
//Send Update Email
|
||||||
if($bNewMsg) {
|
if($bNewMsg) {
|
||||||
$oEmail = new Email($this->asContext['serv_name'], 'email_update');
|
$bSuccess = $this->sendEmail();
|
||||||
$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();
|
|
||||||
$sDesc = $bSuccess?'mail_sent':'mail_failure';
|
$sDesc = $bSuccess?'mail_sent':'mail_failure';
|
||||||
}
|
}
|
||||||
else $sDesc = 'no_new_msg';
|
else $sDesc = 'no_new_msg';
|
||||||
@@ -258,6 +231,37 @@ class Spot extends Main
|
|||||||
return self::getJsonResult($bSuccess, $sDesc);
|
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() {
|
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);
|
//$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';
|
$sFileName = 'spot_cron.sh';
|
||||||
@@ -632,6 +636,19 @@ class Spot extends Main
|
|||||||
return self::getJsonResult($asResult['result'], $asResult['desc'], $asResult['data']);
|
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='') {
|
public function getAdminSettings($sType='') {
|
||||||
$oFeed = new Feed($this->oDb);
|
$oFeed = new Feed($this->oDb);
|
||||||
$asData = array(
|
$asData = array(
|
||||||
@@ -654,6 +671,8 @@ class Spot extends Main
|
|||||||
$sDesc = '';
|
$sDesc = '';
|
||||||
$asResult = array();
|
$asResult = array();
|
||||||
|
|
||||||
|
if($this->oDb->isId($sField) && $sValue <= 0) return self::getJsonResult(false, $this->oLang->getTranslation('impossible_value', [$sValue, $sField]));
|
||||||
|
|
||||||
switch($sType) {
|
switch($sType) {
|
||||||
case 'project':
|
case 'project':
|
||||||
$oProject = new Project($this->oDb, $iId);
|
$oProject = new Project($this->oDb, $iId);
|
||||||
@@ -712,38 +731,68 @@ class Spot extends Main
|
|||||||
return self::getJsonResult($bSuccess, $sDesc, array($sType=>array($asResult)));
|
return self::getJsonResult($bSuccess, $sDesc, array($sType=>array($asResult)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delAdminSettings($sType, $iId) {
|
public function createAdminSettings($sType) {
|
||||||
$bSuccess = false;
|
$bSuccess = false;
|
||||||
$sDesc = '';
|
$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) {
|
switch($sType) {
|
||||||
case 'project':
|
case 'project':
|
||||||
$oProject = new Project($this->oDb, $iId);
|
$oProject = new Project($this->oDb, $iId);
|
||||||
$asResult = $oProject->delete();
|
$asResult = $oProject->delete();
|
||||||
$sDesc = $asResult['project'][0]['desc'];
|
$sDesc = $asResult['project'][0]['desc'];
|
||||||
|
$bSuccess = $asResult['project'][0]['del'];
|
||||||
break;
|
break;
|
||||||
case 'feed':
|
case 'feed':
|
||||||
$oFeed = new Feed($this->oDb, $iId);
|
$oFeed = new Feed($this->oDb, $iId);
|
||||||
$asResult = array('feed'=>array($oFeed->delete()));
|
$asResult = array('feed' => array($oFeed->delete()));
|
||||||
$sDesc = $asResult['feed'][0]['desc'];
|
$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;
|
break;
|
||||||
}
|
}
|
||||||
$bSuccess = ($sDesc=='');
|
|
||||||
|
|
||||||
return self::getJsonResult($bSuccess, $sDesc, $asResult);
|
return self::getJsonResult($bSuccess, $sDesc, $asResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createProject() {
|
public function buildGeoJSON($sCodeName) {
|
||||||
$oProject = new Project($this->oDb);
|
return Converter::convertToGeoJson($sCodeName);
|
||||||
$iNewProjectId = $oProject->createProjectId();
|
|
||||||
|
|
||||||
$oFeed = new Feed($this->oDb);
|
|
||||||
$oFeed->createFeedId($iNewProjectId);
|
|
||||||
|
|
||||||
return self::getJsonResult($iNewProjectId>0, '', array(
|
|
||||||
'project' => array($oProject->getProject()),
|
|
||||||
'feed' => array($oFeed->getFeed())
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function decToDms($dValue, $sType) {
|
public static function decToDms($dValue, $sType) {
|
||||||
|
|||||||
@@ -25,7 +25,12 @@ class Uploader extends UploadHandler
|
|||||||
$this->oMedia = &$oMedia;
|
$this->oMedia = &$oMedia;
|
||||||
$this->oLang = &$oLang;
|
$this->oLang = &$oLang;
|
||||||
$this->sBody = '';
|
$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) {
|
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
|
//Cookie
|
||||||
const COOKIE_ID_USER = 'subscriber';
|
const COOKIE_ID_USER = 'subscriber';
|
||||||
const COOKIE_DURATION = 60 * 60 * 24 * 365; //1 year
|
const COOKIE_DURATION = 60 * 60 * 24 * 365; //1 year
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database Handle
|
* Database Handle
|
||||||
* @var Db
|
* @var Db
|
||||||
@@ -33,7 +34,7 @@ class User extends PhpObject {
|
|||||||
public function __construct(Db &$oDb) {
|
public function __construct(Db &$oDb) {
|
||||||
parent::__construct(__CLASS__);
|
parent::__construct(__CLASS__);
|
||||||
$this->oDb = &$oDb;
|
$this->oDb = &$oDb;
|
||||||
$this->iUserId = 0;
|
$this->setUserId(0);
|
||||||
$this->asUserInfo = array(
|
$this->asUserInfo = array(
|
||||||
'id' => 0,
|
'id' => 0,
|
||||||
Db::getId(self::USER_TABLE) => 0,
|
Db::getId(self::USER_TABLE) => 0,
|
||||||
@@ -47,6 +48,51 @@ class User extends PhpObject {
|
|||||||
$this->checkUserCookie();
|
$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() {
|
public function getLang() {
|
||||||
return $this->asUserInfo['language'];
|
return $this->asUserInfo['language'];
|
||||||
}
|
}
|
||||||
@@ -95,20 +141,25 @@ class User extends PhpObject {
|
|||||||
return Spot::getResult($bSuccess, $sDesc);
|
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;
|
$bSuccess = false;
|
||||||
$sDesc = '';
|
$sDesc = '';
|
||||||
|
|
||||||
if($this->iUserId > 0) {
|
if($bSelf || $this->checkUserClearance(self::CLEARANCE_ADMIN)) {
|
||||||
$iUserId = $this->oDb->updateRow(self::USER_TABLE, $this->getUserId(), array('active'=>self::USER_INACTIVE));
|
if($this->getUserId() > 0) {
|
||||||
if($iUserId==0) $sDesc = 'lang:error_commit_db';
|
$iUserId = $this->oDb->updateRow(self::USER_TABLE, $iUserId, array('active' => self::USER_INACTIVE));
|
||||||
else {
|
if($iUserId==0) $sDesc = 'lang:error_commit_db';
|
||||||
$sDesc = 'lang:nl_unsubscribed';
|
else {
|
||||||
$this->updateCookie(-60 * 60); //Set Cookie in the past, deleting it
|
$sDesc = 'lang:nl_unsubscribed';
|
||||||
$bSuccess = true;
|
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);
|
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)
|
public function checkUserClearance($iClearance)
|
||||||
{
|
{
|
||||||
return ($this->asUserInfo['clearance'] >= $iClearance);
|
return ($this->asUserInfo['clearance'] >= $iClearance);
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ $oValue = $_REQUEST['value'] ?? '';
|
|||||||
$iId = $_REQUEST['id'] ?? 0 ;
|
$iId = $_REQUEST['id'] ?? 0 ;
|
||||||
$sType = $_REQUEST['type'] ?? '';
|
$sType = $_REQUEST['type'] ?? '';
|
||||||
$sEmail = $_REQUEST['email'] ?? '';
|
$sEmail = $_REQUEST['email'] ?? '';
|
||||||
|
$sLat = $_REQUEST['latitude'] ?? '';
|
||||||
|
$sLng = $_REQUEST['longitude'] ?? '';
|
||||||
|
$iTimestamp = $_REQUEST['timestamp'] ?? 0;
|
||||||
|
|
||||||
//Initiate class
|
//Initiate class
|
||||||
$oSpot = new Spot(__FILE__, $sTimezone);
|
$oSpot = new Spot(__FILE__, $sTimezone);
|
||||||
@@ -36,12 +39,12 @@ if($sAction!='')
|
|||||||
{
|
{
|
||||||
switch($sAction)
|
switch($sAction)
|
||||||
{
|
{
|
||||||
case 'params':
|
|
||||||
$sResult = $oSpot->getAppParams();
|
|
||||||
break;
|
|
||||||
case 'markers':
|
case 'markers':
|
||||||
$sResult = $oSpot->getMarkers();
|
$sResult = $oSpot->getMarkers();
|
||||||
break;
|
break;
|
||||||
|
case 'geojson':
|
||||||
|
$sResult = $oSpot->getProjectGeoJson();
|
||||||
|
break;
|
||||||
case 'next_feed':
|
case 'next_feed':
|
||||||
$sResult = $oSpot->getNextFeed($iId);
|
$sResult = $oSpot->getNextFeed($iId);
|
||||||
break;
|
break;
|
||||||
@@ -74,8 +77,8 @@ if($sAction!='')
|
|||||||
case 'add_comment':
|
case 'add_comment':
|
||||||
$sResult = $oSpot->addComment($iId, $sContent);
|
$sResult = $oSpot->addComment($iId, $sContent);
|
||||||
break;
|
break;
|
||||||
case 'admin_new':
|
case 'add_position':
|
||||||
$sResult = $oSpot->createProject();
|
$sResult = $oSpot->addPosition($sLat, $sLng, $iTimestamp);
|
||||||
break;
|
break;
|
||||||
case 'admin_get':
|
case 'admin_get':
|
||||||
$sResult = $oSpot->getAdminSettings();
|
$sResult = $oSpot->getAdminSettings();
|
||||||
@@ -83,8 +86,11 @@ if($sAction!='')
|
|||||||
case 'admin_set':
|
case 'admin_set':
|
||||||
$sResult = $oSpot->setAdminSettings($sType, $iId, $sField, $oValue);
|
$sResult = $oSpot->setAdminSettings($sType, $iId, $sField, $oValue);
|
||||||
break;
|
break;
|
||||||
case 'admin_del':
|
case 'admin_create':
|
||||||
$sResult = $oSpot->delAdminSettings($sType, $iId);
|
$sResult = $oSpot->createAdminSettings($sType);
|
||||||
|
break;
|
||||||
|
case 'admin_delete':
|
||||||
|
$sResult = $oSpot->deleteAdminSettings($sType, $iId);
|
||||||
break;
|
break;
|
||||||
case 'generate_cron':
|
case 'generate_cron':
|
||||||
$sResult = $oSpot->genCronFile();
|
$sResult = $oSpot->genCronFile();
|
||||||
@@ -92,6 +98,9 @@ if($sAction!='')
|
|||||||
case 'sql':
|
case 'sql':
|
||||||
$sResult = $oSpot->getDbBuildScript();
|
$sResult = $oSpot->getDbBuildScript();
|
||||||
break;
|
break;
|
||||||
|
case 'build_geojson':
|
||||||
|
$sResult = $oSpot->buildGeoJSON($sName);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
$sResult = Main::getJsonResult(false, Main::NOT_FOUND);
|
$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 Project
|
||||||
[Spot](https://www.findmespot.com) & GPX integration
|
[Spot](https://www.findmespot.com) & GPX integration
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
|
* npm 18+
|
||||||
|
* composer
|
||||||
* php-mbstring
|
* php-mbstring
|
||||||
* php-imagick
|
* php-imagick
|
||||||
* php-gd
|
* php-gd
|
||||||
@@ -9,23 +13,31 @@
|
|||||||
* ffprobe & ffmpeg
|
* ffprobe & ffmpeg
|
||||||
* STARTTLS Email Server (use Gmail if none available)
|
* STARTTLS Email Server (use Gmail if none available)
|
||||||
* Optional: Geo Caching Server (WMTS Caching Service)
|
* Optional: Geo Caching Server (WMTS Caching Service)
|
||||||
|
|
||||||
## PHP Configuration
|
## PHP Configuration
|
||||||
|
|
||||||
* max_execution_time = 300
|
* max_execution_time = 300
|
||||||
* memory_limit = 500M
|
* memory_limit = 500M
|
||||||
* post_max_size = 4G
|
* post_max_size = 4G
|
||||||
* upload_max_filesize = 4G
|
* upload_max_filesize = 4G
|
||||||
* max_file_uploads = 50
|
* max_file_uploads = 50
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
1. Clone Git onto web server
|
1. Clone Git onto web server
|
||||||
2. Install dependencies & update php.ini parameters
|
2. composer install
|
||||||
3. Copy timezone data: mariadb_tzinfo_to_sql /usr/share/zoneinfo | mariadb -u root mysql
|
3. npm install webpack
|
||||||
4. Copy settings-sample.php to settings.php and populate
|
4. npm run dev
|
||||||
5. Go to #admin and create a new project, feed & maps
|
5. Update php.ini parameters
|
||||||
6. Add a GPX file named <project_codename>.gpx to /geo/
|
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
|
## To Do List
|
||||||
* ECMA import/export
|
|
||||||
* Add mail frequency slider
|
* Add mail frequency slider
|
||||||
* Use WMTS servers directly when not using Geo Caching Server
|
* Use WMTS servers directly when not using Geo Caching Server
|
||||||
* Allow HEIF picture format
|
* 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:title" content="Spotty" />
|
||||||
<meta property="og:description" content="[#]lang:page_og_desc[#]" />
|
<meta property="og:description" content="[#]lang:page_og_desc[#]" />
|
||||||
<meta property="og:type" content="website" />
|
<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:image" content="images/ogp.png" />
|
||||||
<meta property="og:locale" content="[#]lang:locale[#]" />
|
<meta property="og:locale" content="[#]lang:locale[#]" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="images/icons/apple-touch-icon.png?v=GvmqYyKwbb">
|
<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-TileColor" content="#00a300">
|
||||||
<meta name="msapplication-config" content="images/icons/browserconfig.xml?v=GvmqYyKwbb">
|
<meta name="msapplication-config" content="images/icons/browserconfig.xml?v=GvmqYyKwbb">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#ffffff">
|
||||||
<link type="text/css" href="[#]filepath_css[#]" rel="stylesheet" media="all" />
|
<script type="text/javascript">window.params = [#]GLOBAL_VARS[#];</script>
|
||||||
<script type="text/javascript" src="[#]filepath_js_d3[#]"></script>
|
<title>Spotty</title>
|
||||||
<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>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="container">
|
<div id="container"></div>
|
||||||
<div id="main"></div>
|
<script type="module" src="[#]filepath_js[#]"></script>
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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
|
sanitizeTitle: false
|
||||||
, hasVideo: true
|
, hasVideo: true
|
||||||
, onMediaChange: (oMedia) => {}
|
, onMediaChange: (oMedia) => {}
|
||||||
|
, onClosing: () => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
Lightbox.prototype.option = function(options) {
|
Lightbox.prototype.option = function(options) {
|
||||||
@@ -477,6 +478,7 @@
|
|||||||
var $hasVideoNav = this.$container.hasClass('lb-video-nav');
|
var $hasVideoNav = this.$container.hasClass('lb-video-nav');
|
||||||
switch(self.album[imageNumber].type) {
|
switch(self.album[imageNumber].type) {
|
||||||
case 'video':
|
case 'video':
|
||||||
|
self.$image.removeAttr('src');
|
||||||
this.$video.on('loadedmetadata', function(){
|
this.$video.on('loadedmetadata', function(){
|
||||||
self.album[imageNumber].width = this.videoWidth;
|
self.album[imageNumber].width = this.videoWidth;
|
||||||
self.album[imageNumber].height = this.videoHeight;
|
self.album[imageNumber].height = this.videoHeight;
|
||||||
@@ -489,7 +491,7 @@
|
|||||||
if(!$hasVideoNav) this.$container.addClass('lb-video-nav');
|
if(!$hasVideoNav) this.$container.addClass('lb-video-nav');
|
||||||
break;
|
break;
|
||||||
case 'image':
|
case 'image':
|
||||||
this.$video.attr('src', '');
|
this.$video.trigger('pause').removeAttr('src');
|
||||||
if($hasVideoNav) this.$container.removeClass('lb-video-nav');
|
if($hasVideoNav) this.$container.removeClass('lb-video-nav');
|
||||||
|
|
||||||
// When image to show is preloaded, we send the width and height to sizeContainer()
|
// When image to show is preloaded, we send the width and height to sizeContainer()
|
||||||
@@ -718,11 +720,10 @@
|
|||||||
if(this.options.hasVideo) {
|
if(this.options.hasVideo) {
|
||||||
var $lbContainer = this.$lightbox.find('.lb-container');
|
var $lbContainer = this.$lightbox.find('.lb-container');
|
||||||
var $hasVideoNav = $lbContainer.hasClass('lb-video-nav');
|
var $hasVideoNav = $lbContainer.hasClass('lb-video-nav');
|
||||||
this.$video.attr('src', '');
|
this.$video.trigger('pause').removeAttr('src');
|
||||||
|
|
||||||
if($hasVideoNav) $lbContainer.removeClass('lb-video-nav');
|
if($hasVideoNav) $lbContainer.removeClass('lb-video-nav');
|
||||||
}
|
}
|
||||||
oSpot.flushHash();
|
|
||||||
|
|
||||||
$(window).off('resize', this.sizeOverlay);
|
$(window).off('resize', this.sizeOverlay);
|
||||||
this.$nav.off('mousewheel');
|
this.$nav.off('mousewheel');
|
||||||
@@ -732,6 +733,8 @@
|
|||||||
if (this.options.disableScrolling) {
|
if (this.options.disableScrolling) {
|
||||||
$('body').removeClass('lb-disable-scrolling');
|
$('body').removeClass('lb-disable-scrolling');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.options.onClosing();
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Lightbox();
|
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)
|
export default class Spot {
|
||||||
{
|
|
||||||
self = this;
|
constructor(asGlobals) {
|
||||||
this.consts = asGlobals.consts;
|
this.consts = asGlobals.consts;
|
||||||
this.consts.hash_sep = '-';
|
this.consts.hash_sep = '-';
|
||||||
this.consts.title = 'Spotty';
|
this.consts.title = 'Spotty';
|
||||||
this.consts.default_page = 'project';
|
this.consts.default_page = 'project';
|
||||||
this.consts.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || this.consts.default_timezone;
|
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 */
|
/* Initialization */
|
||||||
|
|
||||||
this.init = function()
|
init() {
|
||||||
{
|
this.elem.container = $('#container');
|
||||||
//Variables & constants from php
|
this.elem.main = $('#main');
|
||||||
self.vars('tmp', 'object');
|
|
||||||
self.vars('page', 'string');
|
|
||||||
self.updateVars(asGlobals.vars);
|
|
||||||
|
|
||||||
//page elem
|
|
||||||
self.elem = {};
|
|
||||||
self.elem.container = $('#container');
|
|
||||||
self.elem.main = $('#main');
|
|
||||||
|
|
||||||
self.resetTmpFunctions();
|
|
||||||
|
|
||||||
//On Key down
|
//On Key down
|
||||||
$('html').on('keydown', function(oEvent){self.onKeydown(oEvent);});
|
$('html').on('keydown', (oEvent) => {this.onKeydown(oEvent);});
|
||||||
|
|
||||||
//on window resize
|
//on window resize
|
||||||
$(window).on('resize', function(){self.onResize();});
|
$(window).on('resize', () => {this.onResize();});
|
||||||
|
|
||||||
//Setup menu
|
|
||||||
//self.initMenu();
|
|
||||||
|
|
||||||
//Hash management
|
//Hash management
|
||||||
$(window)
|
$(window)
|
||||||
.bind('hashchange', self.onHashChange)
|
.on('hashchange', () => {this.onHashChange();})
|
||||||
.trigger('hashchange');
|
.trigger('hashchange');
|
||||||
};
|
}
|
||||||
|
|
||||||
this.updateVars = function(asVars)
|
|
||||||
{
|
|
||||||
$.each(asVars, function(sKey, oValue){self.vars(sKey, oValue)});
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Variable Management */
|
/* Variable Management */
|
||||||
|
|
||||||
this.vars = function(oVarName, oValue)
|
vars(oVarName, oValue) {
|
||||||
{
|
|
||||||
var asVarName = (typeof oVarName == 'object')?oVarName:[oVarName];
|
var asVarName = (typeof oVarName == 'object')?oVarName:[oVarName];
|
||||||
|
|
||||||
//Set, name & type / default value (init)
|
//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
|
//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];
|
var asVarName = (typeof sVarName == 'object')?sVarName:[sVarName];
|
||||||
asVarName.unshift('tmp');
|
asVarName.unshift('tmp');
|
||||||
return self.vars(asVarName, oValue);
|
return this.vars(asVarName, oValue);
|
||||||
};
|
}
|
||||||
|
|
||||||
/* Interface with server */
|
/* Interface with server */
|
||||||
|
|
||||||
this.get = function(sAction, fOnSuccess, oVars, fOnError, fonProgress)
|
get(sAction, fOnSuccess, oVars, fOnError, fonProgress) {
|
||||||
{
|
oVars = oVars || {};
|
||||||
if(!oVars) oVars = {};
|
|
||||||
fOnError = fOnError || function(sError) {console.log(sError);};
|
fOnError = fOnError || function(sError) {console.log(sError);};
|
||||||
fonProgress = fonProgress || function(sState){};
|
fonProgress = fonProgress || function(sState){};
|
||||||
fonProgress('start');
|
fonProgress('start');
|
||||||
|
|
||||||
oVars['a'] = sAction;
|
oVars['a'] = sAction;
|
||||||
oVars['t'] = self.consts.timezone;
|
oVars['t'] = this.consts.timezone;
|
||||||
return $.ajax(
|
return $.ajax(
|
||||||
{
|
{
|
||||||
url: self.consts.process_page,
|
url: this.consts.process_page,
|
||||||
data: oVars,
|
data: oVars,
|
||||||
dataType: 'json'
|
dataType: 'json'
|
||||||
})
|
})
|
||||||
.done(function(oData)
|
.done((oData) => {
|
||||||
{
|
|
||||||
fonProgress('done');
|
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);
|
if(oData.result==this.consts.error) fOnError(oData.desc);
|
||||||
else fOnSuccess(oData.data, oData.desc);
|
else if(fOnSuccess) fOnSuccess(oData.data, oData.desc);
|
||||||
})
|
})
|
||||||
.fail(function(jqXHR, textStatus, errorThrown)
|
.fail((jqXHR, textStatus, errorThrown) => {
|
||||||
{
|
|
||||||
fonProgress('fail');
|
fonProgress('fail');
|
||||||
fOnError(textStatus+' '+errorThrown);
|
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 = '';
|
var sLang = '';
|
||||||
|
if(sKey in this.consts.lang) {
|
||||||
if(sKey in self.consts.lang) {
|
sLang = this.consts.lang[sKey];
|
||||||
sLang = self.consts.lang[sKey];
|
for(let i in asParams) {
|
||||||
for(i in asParams) sLang = sLang.replace('$'+i, asParams[i]);
|
sLang = sLang.replace('$'+i, asParams[i]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.log('missing translation: '+sKey);
|
console.log('missing translation: '+sKey);
|
||||||
@@ -111,141 +132,158 @@ function Spot(asGlobals)
|
|||||||
}
|
}
|
||||||
|
|
||||||
return sLang;
|
return sLang;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
isMobile() {
|
||||||
|
return $('#mobile').is(':visible');
|
||||||
|
}
|
||||||
|
|
||||||
/* Page Switch - Trigger & Event catching */
|
/* Page Switch - Trigger & Event catching */
|
||||||
|
|
||||||
this.onHashChange = function()
|
onHashChange() {
|
||||||
{
|
var asHash = this.getHash();
|
||||||
var asHash = self.getHash();
|
if(asHash.hash !='' && asHash.page != '') this.switchPage(asHash); //page switching
|
||||||
if(asHash.hash !='' && asHash.page != '') self.switchPage(asHash); //page switching
|
else if(this.vars('page')=='') this.setHash(this.consts.default_page); //first page
|
||||||
else if(self.vars('page')=='') self.setHash(self.consts.default_page); //first page
|
}
|
||||||
};
|
|
||||||
|
|
||||||
this.getHash = function()
|
getHash() {
|
||||||
{
|
var sHash = this.hash();
|
||||||
var sHash = self.hash();
|
var asHash = sHash.split(this.consts.hash_sep);
|
||||||
var asHash = sHash.split(self.consts.hash_sep);
|
|
||||||
var sPage = asHash.shift() || '';
|
var sPage = asHash.shift() || '';
|
||||||
return {hash:sHash, page:sPage, items:asHash};
|
return {hash:sHash, page:sPage, items:asHash};
|
||||||
};
|
}
|
||||||
|
|
||||||
this.setHash = function(sPage, asItems, bReboot)
|
setHash(sPage, asItems, bReboot) {
|
||||||
{
|
|
||||||
bReboot = bReboot || false;
|
bReboot = bReboot || false;
|
||||||
sPage = sPage || '';
|
sPage = sPage || '';
|
||||||
asItems = asItems || [];
|
asItems = asItems || [];
|
||||||
if(typeof asItems == 'string') 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;
|
bReboot = bReboot || false;
|
||||||
if(!hash) return window.location.hash.slice(1);
|
if(!hash) return window.location.hash.slice(1);
|
||||||
else window.location.hash = '#'+hash;
|
else window.location.hash = '#'+hash;
|
||||||
|
|
||||||
if(bReboot) location.reload();
|
if(bReboot) location.reload();
|
||||||
};
|
}
|
||||||
|
|
||||||
this.updateHash = function(sType, iId) {
|
updateHash(sType, iId) {
|
||||||
sType = sType || '';
|
sType = sType || '';
|
||||||
iId = iId || 0;
|
iId = iId || 0;
|
||||||
|
|
||||||
var asHash = self.getHash();
|
var asHash = this.getHash();
|
||||||
if(iId) self.setHash(asHash.page, [asHash.items[0], sType, iId]);
|
if(iId) this.setHash(asHash.page, [asHash.items[0], sType, iId]);
|
||||||
};
|
}
|
||||||
|
|
||||||
this.flushHash = function(asTypes) {
|
flushHash(asTypes) {
|
||||||
asTypes = asTypes || [];
|
asTypes = asTypes || [];
|
||||||
var asHash = self.getHash();
|
var asHash = this.getHash();
|
||||||
if(asHash.items.length > 1 && (asTypes.length == 0 || asTypes.indexOf(asHash.items[1]) != -1)) self.setHash(asHash.page, [asHash.items[0]]);
|
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 */
|
/* Page Switch - DOM Replacement */
|
||||||
|
|
||||||
this.getActionLink = function(sAction, oVars)
|
getActionLink(sAction, oVars) {
|
||||||
{
|
|
||||||
if(!oVars) oVars = {};
|
if(!oVars) oVars = {};
|
||||||
sVars = '';
|
let sVars = '';
|
||||||
for(i in oVars)
|
|
||||||
{
|
for(i in oVars) sVars += '&'+i+'='+oVars[i];
|
||||||
sVars += '&'+i+'='+oVars[i];
|
|
||||||
}
|
|
||||||
return self.consts.process_page+'?a='+sAction+sVars;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.resetTmpFunctions = function()
|
return this.consts.process_page+'?a='+sAction+sVars;
|
||||||
{
|
}
|
||||||
self.pageInit = function(asHash){console.log('no init for the page: '+asHash.page)};
|
|
||||||
self.onSamePageMove = function(asHash){return false};
|
|
||||||
self.onQuitPage = function(){return true};
|
|
||||||
self.onResize = function(){};
|
|
||||||
self.onFeedback = function(sType, sMsg){};
|
|
||||||
self.onKeydown = function(oEvent){};
|
|
||||||
};
|
|
||||||
|
|
||||||
this.switchPage = function(asHash)
|
addPage(sPage, oPage) {
|
||||||
{
|
this.pages[sPage] = oPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
switchPage(asHash) {
|
||||||
var sPageName = asHash.page;
|
var sPageName = asHash.page;
|
||||||
var bSamePage = (self.vars('page') == sPageName);
|
var bSamePage = (this.vars('page') == sPageName);
|
||||||
var bFirstPage = (self.vars('page') == '');
|
var bFirstPage = (this.vars('page') == '');
|
||||||
|
|
||||||
if(!self.consts.pages[sPageName]) { //Page does not exist
|
if(!this.consts.pages[sPageName]) { //Page does not exist
|
||||||
if(bFirstPage) self.setHash(self.consts.default_page);
|
if(bFirstPage) this.setHash(this.consts.default_page);
|
||||||
else self.setHash(self.vars('page'), self.vars(['hash', 'items']));
|
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
|
//Delete tmp variables
|
||||||
self.vars('tmp', {});
|
this.vars('tmp', {});
|
||||||
|
|
||||||
//disable tmp functions
|
|
||||||
self.resetTmpFunctions();
|
|
||||||
|
|
||||||
//Officially a new page
|
//Officially a new page
|
||||||
self.vars('page', sPageName);
|
this.vars('page', sPageName);
|
||||||
self.vars('hash', asHash);
|
this.vars('hash', asHash);
|
||||||
|
|
||||||
//Update Page Title
|
//Update Page Title
|
||||||
this.setPageTitle(sPageName+' '+(asHash.items[0] || ''));
|
this.setPageTitle(sPageName+' '+(asHash.items[0] || ''));
|
||||||
|
|
||||||
//Replacing DOM
|
//Replacing DOM
|
||||||
var $Dom = $(self.consts.pages[sPageName]);
|
var $Dom = $(this.consts.pages[sPageName]);
|
||||||
if(bFirstPage)
|
if(bFirstPage) this.splash($Dom, asHash, bFirstPage); //first page
|
||||||
{
|
else this.elem.main.stop().fadeTo('fast', 0, () => {this.splash($Dom, asHash, bFirstPage);}); //Switching page
|
||||||
self.splash($Dom, asHash, bFirstPage); //first page
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
self.elem.main.stop().fadeTo('fast', 0, function(){self.splash($Dom, asHash, bFirstPage);}); //Switching page
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if(bSamePage) self.vars('hash', asHash);
|
else if(bSamePage) this.vars('hash', asHash);
|
||||||
};
|
}
|
||||||
|
|
||||||
this.setPageTitle = function(sTitle) {
|
splash($Dom, asHash, bFirstPage)
|
||||||
document.title = self.consts.title+' - '+sTitle;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.splash = function($Dom, asHash, bFirstPage)
|
|
||||||
{
|
{
|
||||||
//Switch main content
|
//Switch main content
|
||||||
self.elem.main.empty().html($Dom);
|
this.elem.main.empty().html($Dom);
|
||||||
|
|
||||||
//Page Bootstrap
|
//Page Bootstrap
|
||||||
self.pageInit(asHash, bFirstPage);
|
this.pageInit(asHash, bFirstPage);
|
||||||
|
|
||||||
//Show main
|
//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);
|
$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
|
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
|
if(iTimeDays > 1) iTimeDays = Math.round(iTimeDays * 2) / 2; //Round down to the closest half day
|
||||||
else {
|
else {
|
||||||
@@ -256,213 +294,13 @@ function Spot(asGlobals)
|
|||||||
iTimeMinutes = Math.floor(iHours * 4) * 15; //Round down to the closest 15 minutes
|
iTimeMinutes = Math.floor(iHours * 4) * 15; //Round down to the closest 15 minutes
|
||||||
}
|
}
|
||||||
return '~ '
|
return '~ '
|
||||||
+(iTimeDays>0?(iTimeDays+(iTimeDays%2==0?'':'½')+' '+self.lang(iTimeDays>1?'unit_days':'unit_day')):'') //Days
|
+(iTimeDays>0?(iTimeDays+(iTimeDays%2==0?'':'½')+' '+this.lang(iTimeDays>1?'unit_days':'unit_day')):'') //Days
|
||||||
+((iTimeHours>0 || iTimeDays==0)?iTimeHours+self.lang('unit_hour'):'') //Hours
|
+((iTimeHours>0 || iTimeDays==0)?iTimeHours+this.lang('unit_hour'):'') //Hours
|
||||||
+((iTimeDays>0 || iTimeMinutes==0)?'':iTimeMinutes) //Minutes
|
+((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
|
|
||||||
{
|
checkClearance(sClearance) {
|
||||||
var sKey = (typeof asPath == 'object')?asPath.shift():asPath;
|
return (this.vars(['user', 'clearance']) >= sClearance);
|
||||||
return aoAnchor[sKey] = (!(sKey in aoAnchor) && (oValue in asTypes))?asTypes[oValue]:oValue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$.prototype.addInput = function(sType, sName, sValue, aoEvents)
|
|
||||||
{
|
|
||||||
aoEvents = aoEvents || [];
|
|
||||||
var $Input = $('<input>', {type: sType, name: sName, value: sValue}).data('old_value', sValue);
|
|
||||||
$.each(aoEvents, function(iIndex, aoEvent) {
|
|
||||||
$Input.on(aoEvent.on, aoEvent.callback);
|
|
||||||
});
|
|
||||||
return $(this).append($Input);
|
|
||||||
};
|
|
||||||
|
|
||||||
$.prototype.addButton = function(sIcon, sText, sName, fOnClick, sClass)
|
|
||||||
{
|
|
||||||
sText = sText || '';
|
|
||||||
sClass = sClass || '';
|
|
||||||
var $Btn = $('<button>', {name: sName, 'class':sClass})
|
|
||||||
.addIcon('fa-'+sIcon, (sText != ''))
|
|
||||||
.append(sText)
|
|
||||||
.click(fOnClick);
|
|
||||||
|
|
||||||
return $(this).append($Btn);
|
|
||||||
};
|
|
||||||
|
|
||||||
$.prototype.addIcon = function(sIcon, bMargin, sStyle)
|
|
||||||
{
|
|
||||||
bMargin = bMargin || false;
|
|
||||||
sStyle = sStyle || '';
|
|
||||||
return $(this).append($('<i>', {'class':'fa'+sStyle+' '+sIcon+(bMargin?' push':'')}));
|
|
||||||
};
|
|
||||||
|
|
||||||
$.prototype.defaultVal = function(sDefaultValue)
|
|
||||||
{
|
|
||||||
$(this)
|
|
||||||
.data('default_value', sDefaultValue)
|
|
||||||
.val(sDefaultValue)
|
|
||||||
.addClass('defaultText')
|
|
||||||
.focus(function()
|
|
||||||
{
|
|
||||||
var $This = $(this);
|
|
||||||
if($This.val() == $This.data('default_value')) $This.val('').removeClass('defaultText');
|
|
||||||
})
|
|
||||||
.blur(function()
|
|
||||||
{
|
|
||||||
var $This = $(this);
|
|
||||||
if($This.val() == '') $This.val($This.data('default_value')).addClass('defaultText');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$.prototype.checkForm = function(sSelector)
|
|
||||||
{
|
|
||||||
sSelector = sSelector || 'input[type="text"], textarea';
|
|
||||||
var $This = $(this);
|
|
||||||
var bOk = true;
|
|
||||||
$This.find(sSelector).each(function()
|
|
||||||
{
|
|
||||||
$This = $(this);
|
|
||||||
bOk = bOk && $This.val()!='' && $This.val()!=$This.data('default_value');
|
|
||||||
});
|
|
||||||
return bOk;
|
|
||||||
};
|
|
||||||
|
|
||||||
$.prototype.cascadingDown = function(sDuration)
|
|
||||||
{
|
|
||||||
return $(this).slideDown(sDuration, function(){$(this).next().cascadingDown(sDuration);});
|
|
||||||
};
|
|
||||||
|
|
||||||
$.prototype.hoverSwap = function(sDefault, sHover)
|
|
||||||
{
|
|
||||||
return $(this)
|
|
||||||
.data('default', sDefault)
|
|
||||||
.data('hover', sHover)
|
|
||||||
.hover(function(){
|
|
||||||
var $This = $(this),
|
|
||||||
sHover = $This.data('hover');
|
|
||||||
sDefault = $This.data('default');
|
|
||||||
|
|
||||||
if(sDefault!='' && sHover != '') {
|
|
||||||
$This.fadeOut('fast', function() {
|
|
||||||
var $This = $(this);
|
|
||||||
$This.text((sDefault==$This.text())?sHover:sDefault).fadeIn('fast');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.text(sDefault);
|
|
||||||
};
|
|
||||||
|
|
||||||
$.prototype.onSwipe = function(fOnStart, fOnMove, fOnEnd){
|
|
||||||
return $(this)
|
|
||||||
.on('dragstart', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
})
|
|
||||||
.on('mousedown touchstart', (e) => {
|
|
||||||
var $This = $(this);
|
|
||||||
var oPos = getDragPosition(e);
|
|
||||||
$This.data('x-start', oPos.x);
|
|
||||||
$This.data('y-start', oPos.y);
|
|
||||||
$This.data('x-move', oPos.x);
|
|
||||||
$This.data('y-move', oPos.y);
|
|
||||||
$This.data('moving', true).addClass('moving');
|
|
||||||
fOnStart({
|
|
||||||
xStart: $This.data('x-start'),
|
|
||||||
yStart: $This.data('y-start')
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.on('touchmove mousemove', (e) => {
|
|
||||||
var $This = $(this);
|
|
||||||
if($This.data('moving')) {
|
|
||||||
var oPos = getDragPosition(e);
|
|
||||||
$This.data('x-move', oPos.x);
|
|
||||||
$This.data('y-move', oPos.y);
|
|
||||||
fOnMove({
|
|
||||||
xStart: $This.data('x-start'),
|
|
||||||
yStart: $This.data('y-start'),
|
|
||||||
xMove: $This.data('x-move'),
|
|
||||||
yMove: $This.data('y-move')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on('mouseup mouseleave touchend', (e) => {
|
|
||||||
var $This = $(this);
|
|
||||||
if($This.data('moving')) {
|
|
||||||
$This.data('moving', false).removeClass('moving');
|
|
||||||
fOnEnd({
|
|
||||||
xStart: $This.data('x-start'),
|
|
||||||
yStart: $This.data('y-start'),
|
|
||||||
xEnd: $This.data('x-move'),
|
|
||||||
yEnd: $This.data('y-move')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function getDragPosition(oEvent) {
|
|
||||||
let bMouse = oEvent.type.includes('mouse');
|
|
||||||
return {
|
|
||||||
x: bMouse?oEvent.pageX:oEvent.touches[0].clientX,
|
|
||||||
y: bMouse?oEvent.pageY:oEvent.touches[0].clientY
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyTextToClipboard(text) {
|
|
||||||
if(!navigator.clipboard) {
|
|
||||||
var textArea = document.createElement('textarea');
|
|
||||||
textArea.value = text;
|
|
||||||
|
|
||||||
// Avoid scrolling to bottom
|
|
||||||
textArea.style.top = '0';
|
|
||||||
textArea.style.left = '0';
|
|
||||||
textArea.style.position = 'fixed';
|
|
||||||
|
|
||||||
document.body.appendChild(textArea);
|
|
||||||
textArea.focus();
|
|
||||||
textArea.select();
|
|
||||||
|
|
||||||
try {
|
|
||||||
var successful = document.execCommand('copy');
|
|
||||||
if(!successful) console.error('Fallback: Oops, unable to copy', text);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Fallback: Oops, unable to copy', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.removeChild(textArea);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
navigator.clipboard.writeText(text).then(
|
|
||||||
function() {},
|
|
||||||
function(err) {
|
|
||||||
console.error('Async: Could not copy text: ', err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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;
|
$fa-css-prefix: fa;
|
||||||
|
|
||||||
@import 'fa/solid';
|
@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}-config:before { content: fa-content($fa-var-cogs); }
|
||||||
.#{$fa-css-prefix}-upload:before { content: fa-content($fa-var-cloud-upload); }
|
.#{$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 */
|
/* Feed */
|
||||||
.#{$fa-css-prefix}-post:before { content: fa-content($fa-var-comment); }
|
.#{$fa-css-prefix}-post:before { content: fa-content($fa-var-comment); }
|
||||||
.#{$fa-css-prefix}-media:before { content: fa-content($fa-var-photo-video); }
|
.#{$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}-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}-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: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 */
|
/* Feed - Poster */
|
||||||
.#{$fa-css-prefix}-poster:before { content: fa-content($fa-var-comment-edit); }
|
.#{$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 */
|
/* cyrillic-ext */
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
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;
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
}
|
}
|
||||||
/* cyrillic */
|
/* cyrillic */
|
||||||
@@ -15,8 +15,8 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCs6KVjbNBYlgoKew72j00.woff2) format('woff2');
|
src: url(https://fonts.gstatic.com/s/ubuntu/v20/4iCs6KVjbNBYlgoKew72j00.woff2) format('woff2');
|
||||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
}
|
}
|
||||||
/* greek-ext */
|
/* greek-ext */
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
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;
|
unicode-range: U+1F00-1FFF;
|
||||||
}
|
}
|
||||||
/* greek */
|
/* greek */
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
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;
|
unicode-range: U+0370-03FF;
|
||||||
}
|
}
|
||||||
/* latin-ext */
|
/* latin-ext */
|
||||||
@@ -42,8 +42,8 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCs6KVjbNBYlgoKcQ72j00.woff2) format('woff2');
|
src: url(https://fonts.gstatic.com/s/ubuntu/v20/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;
|
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 */
|
/* latin */
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(fonts/4iCs6KVjbNBYlgoKfw72.woff2) format('woff2');
|
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 */
|
/* cyrillic-ext */
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-display: swap;
|
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;
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
}
|
}
|
||||||
/* cyrillic */
|
/* cyrillic */
|
||||||
@@ -69,8 +69,8 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCv6KVjbNBYlgoCxCvjtGyNL4U.woff2) format('woff2');
|
src: url(https://fonts.gstatic.com/s/ubuntu/v20/4iCv6KVjbNBYlgoCxCvjtGyNL4U.woff2) format('woff2');
|
||||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
}
|
}
|
||||||
/* greek-ext */
|
/* greek-ext */
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-display: swap;
|
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;
|
unicode-range: U+1F00-1FFF;
|
||||||
}
|
}
|
||||||
/* greek */
|
/* greek */
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-display: swap;
|
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;
|
unicode-range: U+0370-03FF;
|
||||||
}
|
}
|
||||||
/* latin-ext */
|
/* latin-ext */
|
||||||
@@ -96,8 +96,8 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCv6KVjbNBYlgoCxCvjvmyNL4U.woff2) format('woff2');
|
src: url(https://fonts.gstatic.com/s/ubuntu/v20/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;
|
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 */
|
/* latin */
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -106,5 +106,5 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(fonts/4iCv6KVjbNBYlgoCxCvjsGyN.woff2) format('woff2');
|
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) {
|
@mixin lightbox-icon($icon) {
|
||||||
background: none;
|
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) {
|
@media only screen and (max-width: 800px) {
|
||||||
|
|
||||||
$panel-width: "100vw - #{$button-width} - 2 * #{$block-spacing}";
|
$panel-width: "100vw - #{$button-width} - 2 * #{$block-spacing}";
|
||||||
|
$panel-width-max: $panel-width;
|
||||||
|
$panel-actual-width: $panel-width;
|
||||||
|
|
||||||
.desktop {
|
.desktop {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#projects {
|
#projects {
|
||||||
#feed, #settings {
|
.map-container {
|
||||||
width: calc(#{$panel-width});
|
width: calc(#{$panel-width});
|
||||||
max-width: calc(#{$panel-width});
|
max-width: calc(#{$panel-width});
|
||||||
}
|
}
|
||||||
|
|
||||||
#feed {
|
|
||||||
right: calc((#{$panel-width}) * -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#settings {
|
|
||||||
left: calc((#{$panel-width}) * -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#title {
|
#title {
|
||||||
width: calc(#{$panel-width} - #{$button-width} - 4 * #{$block-spacing});
|
|
||||||
max-width: calc(#{$panel-width} - #{$button-width} - 4 * #{$block-spacing});
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaflet-right, .leaflet-left {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.with-feed, &.with-settings {
|
&.with-feed, &.with-settings {
|
||||||
#submap {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-control-container .leaflet-top.leaflet-right {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#title {
|
#title {
|
||||||
width: calc(#{$panel-width} - #{$button-width} - 4 * #{$block-spacing});
|
max-width: calc(100vw - #{$block-spacing} - #{$panel-actual-width} - (#{$button-width} + #{$block-spacing} * 2) * 2);
|
||||||
max-width: calc(#{$panel-width} - #{$button-width} - 4 * #{$block-spacing});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&.with-feed {
|
#submap {
|
||||||
.leaflet-right {
|
transform: translateX(0);
|
||||||
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});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaflet-control-container .leaflet-top.leaflet-left {
|
&.with-feed {
|
||||||
display: none;
|
.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 {
|
a.lb-next::before {
|
||||||
right: 1em;
|
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,
|
.#{$fa-css-prefix}-solid,
|
||||||
.far,
|
.far,
|
||||||
.#{$fa-css-prefix}-regular,
|
.#{$fa-css-prefix}-regular,
|
||||||
|
.fasr,
|
||||||
.fal,
|
.fal,
|
||||||
.#{$fa-css-prefix}-light,
|
.#{$fa-css-prefix}-light,
|
||||||
|
.fasl,
|
||||||
.fat,
|
.fat,
|
||||||
.#{$fa-css-prefix}-thin,
|
.#{$fa-css-prefix}-thin,
|
||||||
|
.fast,
|
||||||
.fad,
|
.fad,
|
||||||
.#{$fa-css-prefix}-duotone,
|
.#{$fa-css-prefix}-duotone,
|
||||||
.fass,
|
.fass,
|
||||||
@@ -56,8 +59,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fass,
|
.fass,
|
||||||
|
.fasr,
|
||||||
|
.fasl,
|
||||||
|
.fast,
|
||||||
.#{$fa-css-prefix}-sharp {
|
.#{$fa-css-prefix}-sharp {
|
||||||
font-family: 'Font Awesome 6 Sharp';
|
font-family: 'Font Awesome 6 Sharp';
|
||||||
|
}
|
||||||
|
.fass,
|
||||||
|
.#{$fa-css-prefix}-sharp {
|
||||||
font-weight: 900;
|
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 */
|
/* 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 {
|
@each $name, $icon in $fa-icons {
|
||||||
.fad.#{$fa-css-prefix}-#{$name}::after, .#{$fa-css-prefix}-duotone.#{$fa-css-prefix}-#{$name}::after {
|
.fad.#{$fa-css-prefix}-#{$name}::after, .#{$fa-css-prefix}-duotone.#{$fa-css-prefix}-#{$name}::after {
|
||||||
content: unquote("\"#{ $icon }#{ $icon }\"");
|
content: unquote("\"#{ $icon }#{ $icon }\"");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ readers do not read off random characters that represent icons */
|
|||||||
@each $name, $icon in $fa-icons {
|
@each $name, $icon in $fa-icons {
|
||||||
.#{$fa-css-prefix}-#{$name}::before { content: unquote("\"#{ $icon }\""); }
|
.#{$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) {
|
@mixin fa-icon-brands($fa-var) {
|
||||||
@extend %fa-icon;
|
@extend %fa-icon;
|
||||||
@extend .fa-brands;
|
@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)
|
* License - https://fontawesome.com/license (Commercial License)
|
||||||
* Copyright 2022 Fonticons, Inc.
|
* Copyright 2023 Fonticons, Inc.
|
||||||
*/
|
*/
|
||||||
@import 'functions';
|
@import 'functions';
|
||||||
@import 'variables';
|
@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)
|
* License - https://fontawesome.com/license (Commercial License)
|
||||||
* Copyright 2022 Fonticons, Inc.
|
* Copyright 2023 Fonticons, Inc.
|
||||||
*/
|
*/
|
||||||
@import 'functions';
|
@import 'functions';
|
||||||
@import 'variables';
|
@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)
|
* License - https://fontawesome.com/license (Commercial License)
|
||||||
* Copyright 2022 Fonticons, Inc.
|
* Copyright 2023 Fonticons, Inc.
|
||||||
*/
|
*/
|
||||||
// Font Awesome core compile (Web Fonts-based)
|
// 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