Compare commits
10
Commits
46d4702471
...
506c8bd946
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
506c8bd946 | ||
|
|
4ae0e46a7d | ||
|
|
d29403d9c5 | ||
|
|
da5a75a9de | ||
|
|
17009b641f | ||
|
|
171db88400 | ||
|
|
2b32b8a683 | ||
|
|
bf8bfa1de2 | ||
|
|
f1b90a4d83 | ||
|
|
0d4b159ab3 |
@@ -14,3 +14,6 @@
|
|||||||
/vendor/
|
/vendor/
|
||||||
/node_modules/
|
/node_modules/
|
||||||
/composer.dev.lock
|
/composer.dev.lock
|
||||||
|
|
||||||
|
# Lint caches
|
||||||
|
/.php-cs-fixer.cache
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PHP-CS-Fixer config codifying the style already in use under lib/ and
|
||||||
|
* public/. Deliberately NOT based on @PSR2/@PSR12 - this codebase diverges
|
||||||
|
* from those on purpose (tabs, no space before control-structure parens),
|
||||||
|
* so pulling in a preset would rewrite most of the codebase to match a
|
||||||
|
* style nobody uses here. Rules below were derived by sampling lib/*.php,
|
||||||
|
* not from a style guide.
|
||||||
|
*/
|
||||||
|
|
||||||
|
$finder = (new PhpCsFixer\Finder())
|
||||||
|
->in([__DIR__.'/lib', __DIR__.'/public'])
|
||||||
|
->append([__DIR__.'/config/settings-sample.php'])
|
||||||
|
->name('*.php');
|
||||||
|
|
||||||
|
return (new PhpCsFixer\Config())
|
||||||
|
->setIndent("\t")
|
||||||
|
->setLineEnding("\n")
|
||||||
|
->setRules([
|
||||||
|
//Strings: single quotes except where interpolation needs double
|
||||||
|
'single_quote' => true,
|
||||||
|
|
||||||
|
//Arrays: short [] syntax, not long-form array()
|
||||||
|
'array_syntax' => ['syntax' => 'short'],
|
||||||
|
|
||||||
|
//Braces: same line as class/function signature (dominant in lib/,
|
||||||
|
//not the PSR-2 next-line-for-class convention)
|
||||||
|
'braces_position' => [
|
||||||
|
'classes_opening_brace' => 'same_line',
|
||||||
|
'functions_opening_brace' => 'same_line',
|
||||||
|
'anonymous_functions_opening_brace' => 'same_line'
|
||||||
|
],
|
||||||
|
|
||||||
|
//Concatenation: no padding around `.`
|
||||||
|
'concat_space' => ['spacing' => 'none'],
|
||||||
|
|
||||||
|
//true/false/null: lowercase (100% consistent already)
|
||||||
|
'constant_case' => ['case' => 'lower'],
|
||||||
|
'lowercase_keywords' => true,
|
||||||
|
|
||||||
|
'visibility_required' => ['elements' => ['method', 'property', 'const']],
|
||||||
|
|
||||||
|
//Housekeeping - safe regardless of style
|
||||||
|
'no_unused_imports' => true,
|
||||||
|
'no_trailing_whitespace' => true,
|
||||||
|
'no_trailing_whitespace_in_comment' => true,
|
||||||
|
'single_blank_line_at_eof' => true,
|
||||||
|
'no_empty_statement' => true,
|
||||||
|
'trim_array_spaces' => true,
|
||||||
|
'new_with_parentheses' => true
|
||||||
|
])
|
||||||
|
->setFinder($finder);
|
||||||
@@ -13,9 +13,13 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
|
"php": ">=8.5",
|
||||||
"franzz/objects": "dev-vue",
|
"franzz/objects": "dev-vue",
|
||||||
"phpmailer/phpmailer": "^7.1"
|
"phpmailer/phpmailer": "^7.1"
|
||||||
},
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.75"
|
||||||
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Franzz\\Livetrail\\": "lib/",
|
"Franzz\\Livetrail\\": "lib/",
|
||||||
@@ -24,5 +28,9 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"config/settings.php"
|
"config/settings.php"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"lint": "php-cs-fixer fix --dry-run --diff",
|
||||||
|
"lint-fix": "php-cs-fixer fix"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,13 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
|
"php": ">=8.5",
|
||||||
"franzz/objects": "dev-vue",
|
"franzz/objects": "dev-vue",
|
||||||
"phpmailer/phpmailer": "^7.1"
|
"phpmailer/phpmailer": "^7.1"
|
||||||
},
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.75"
|
||||||
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Franzz\\Livetrail\\": "lib/"
|
"Franzz\\Livetrail\\": "lib/"
|
||||||
@@ -20,5 +24,9 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"config/settings.php"
|
"config/settings.php"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"lint": "php-cs-fixer fix --dry-run --diff",
|
||||||
|
"lint-fix": "php-cs-fixer fix"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-17
@@ -1,20 +1,19 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
class Settings
|
class Settings {
|
||||||
{
|
public const DB_SERVER = 'localhost';
|
||||||
const DB_SERVER = 'localhost';
|
public const DB_LOGIN = '';
|
||||||
const DB_LOGIN = '';
|
public const DB_PASS = '';
|
||||||
const DB_PASS = '';
|
public const DB_NAME = 'livetrail';
|
||||||
const DB_NAME = 'livetrail';
|
public const DB_ENC = 'utf8mb4';
|
||||||
const DB_ENC = 'utf8mb4';
|
public const TEXT_ENC = 'UTF-8';
|
||||||
const TEXT_ENC = 'UTF-8';
|
public const TIMEZONE = 'Europe/Zurich';
|
||||||
const TIMEZONE = 'Europe/Zurich';
|
public const MAIL_SERVER = '';
|
||||||
const MAIL_SERVER = '';
|
public const MAIL_FROM = '';
|
||||||
const MAIL_FROM = '';
|
public const MAIL_USER = '';
|
||||||
const MAIL_USER = '';
|
public const MAIL_PASS = '';
|
||||||
const MAIL_PASS = '';
|
public const WEATHER_TOKEN = ''; //visualcrossing.com
|
||||||
const WEATHER_TOKEN = ''; //visualcrossing.com
|
public const TIMEZONE_USER = ''; //geonames.org
|
||||||
const TIMEZONE_USER = ''; //geonames.org
|
public const DEBUG = true;
|
||||||
const DEBUG = true;
|
public const LOG_FOLDER = __DIR__;
|
||||||
const LOG_FOLDER = __DIR__;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// ESLint flat config codifying the style already in use across src/.
|
||||||
|
// Goal: catch real mistakes and enforce the existing conventions, not impose
|
||||||
|
// an external style guide. Rules were derived by sampling src/scripts/*.js
|
||||||
|
// and src/components/*.vue, not from a template.
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import vue from 'eslint-plugin-vue';
|
||||||
|
import globals from 'globals';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
js.configs.recommended,
|
||||||
|
...vue.configs['flat/essential'],
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
globals: {
|
||||||
|
...globals.browser
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
//Indentation: tabs everywhere, one indent per level
|
||||||
|
'indent': 'off', //too many false positives on Vue template attr wrapping; keep manual
|
||||||
|
'no-tabs': 'off',
|
||||||
|
|
||||||
|
//Strings: single quotes (project uses single-quoted strings exclusively)
|
||||||
|
'quotes': ['warn', 'single', {avoidEscape: true, allowTemplateLiterals: true}],
|
||||||
|
|
||||||
|
//Semicolons: required almost everywhere. The one consistent
|
||||||
|
//exception is the top-level `export default {...}` in Vue SFCs,
|
||||||
|
//which never gets a trailing semicolon - ESLint's `semi` rule
|
||||||
|
//can't carve that one spot out, so this is 'warn' rather than
|
||||||
|
//'error' to avoid flagging every component's SFC boilerplate
|
||||||
|
//as a hard failure.
|
||||||
|
'semi': ['warn', 'always'],
|
||||||
|
|
||||||
|
//Equality: codebase mixes == and === deliberately (e.g. loose checks
|
||||||
|
//against numeric strings from the API) - don't force either
|
||||||
|
'eqeqeq': 'off',
|
||||||
|
|
||||||
|
//Function parens: no space before the parameter list - `function(x)`, not `function (x)`
|
||||||
|
'space-before-function-paren': ['warn', 'never'],
|
||||||
|
|
||||||
|
//Control structures: no space before the parenthesis -
|
||||||
|
//`if(x)`, `for(...)`, `switch(x)`, not `if (x)` etc.
|
||||||
|
//(100% consistent across src/ - checked via grep before writing this)
|
||||||
|
'keyword-spacing': ['warn', {
|
||||||
|
after: true,
|
||||||
|
overrides: {
|
||||||
|
if: {after: false},
|
||||||
|
for: {after: false},
|
||||||
|
while: {after: false},
|
||||||
|
switch: {after: false},
|
||||||
|
catch: {after: false}
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
'space-before-blocks': ['warn', 'always'],
|
||||||
|
|
||||||
|
//Object-curly-spacing is deliberately NOT configured: the codebase
|
||||||
|
//consistently uses `{a, b}` (no padding) for object literals and
|
||||||
|
//destructuring, but `import { x } from 'y'` (padded) for imports -
|
||||||
|
//and this rule can't apply different spacing to those two node
|
||||||
|
//kinds, so enforcing either would misfire on the other.
|
||||||
|
'object-curly-spacing': 'off',
|
||||||
|
'array-bracket-spacing': ['warn', 'never'],
|
||||||
|
|
||||||
|
//Prefix convention (Hungarian-ish: sString, iInt, aArray, oObject, bBool)
|
||||||
|
//is a project-wide naming discipline, not something ESLint can check;
|
||||||
|
//left undocumented here on purpose.
|
||||||
|
|
||||||
|
//Real-bug catchers - keep these strict regardless of style
|
||||||
|
//ignoreRestSiblings covers the `const {prev, ...rest} = obj` idiom
|
||||||
|
//(destructuring a key out just to exclude it from the rest), used
|
||||||
|
//in App.vue - that `prev` binding is intentionally unused.
|
||||||
|
'no-unused-vars': ['warn', {argsIgnorePattern: '^_', ignoreRestSiblings: true}],
|
||||||
|
'no-undef': 'error',
|
||||||
|
'no-var': 'warn', //codebase is ES module/class based; var only appears in one legacy fallback block
|
||||||
|
'prefer-const': 'off', //not consistently followed, don't force churn
|
||||||
|
|
||||||
|
//Vue-specific: components are registered and used with camelCase
|
||||||
|
//tags in templates (<appIcon>, <projectMapLink>). ESLint's
|
||||||
|
//component-name-in-template-casing rule only supports PascalCase
|
||||||
|
//or kebab-case, neither of which matches, so it's left off here
|
||||||
|
//rather than forced into a casing the codebase doesn't use.
|
||||||
|
'vue/component-name-in-template-casing': 'off',
|
||||||
|
'vue/multi-word-component-names': 'off', //AppIcon, Admin, Project etc. mix single/multi-word by design
|
||||||
|
'vue/attribute-hyphenation': 'off', //existing templates mix camelCase and kebab-case attrs; not consistent enough to enforce yet
|
||||||
|
'vue/require-default-prop': 'off',
|
||||||
|
'vue/no-v-html': 'error' //codebase currently has zero v-html usage - keep it that way
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignores: ['public/**', 'vendor/**', 'node_modules/**']
|
||||||
|
}
|
||||||
|
];
|
||||||
+20
-33
@@ -6,9 +6,8 @@ use Franzz\Objects\PhpObject;
|
|||||||
use Franzz\Objects\ToolBox;
|
use Franzz\Objects\ToolBox;
|
||||||
|
|
||||||
//TODO Keep only local specificities and move bulk to Franzz\Objects\Controller
|
//TODO Keep only local specificities and move bulk to Franzz\Objects\Controller
|
||||||
class Controller extends PhpObject
|
class Controller extends PhpObject {
|
||||||
{
|
private const MUTATING_ACTIONS = [
|
||||||
const MUTATING_ACTIONS = array(
|
|
||||||
'add_post',
|
'add_post',
|
||||||
'subscribe',
|
'subscribe',
|
||||||
'unsubscribe',
|
'unsubscribe',
|
||||||
@@ -21,34 +20,31 @@ class Controller extends PhpObject
|
|||||||
'admin_set',
|
'admin_set',
|
||||||
'admin_create',
|
'admin_create',
|
||||||
'admin_delete'
|
'admin_delete'
|
||||||
);
|
];
|
||||||
const SESSION_WRITING_ACTIONS = array(
|
private const SESSION_WRITING_ACTIONS = [
|
||||||
'login',
|
'login',
|
||||||
'logout'
|
'logout'
|
||||||
);
|
];
|
||||||
|
|
||||||
private Livetrail $oLivetrail;
|
private Livetrail $oLivetrail;
|
||||||
private array $asReq;
|
private array $asReq;
|
||||||
private string $sCsrfToken = '';
|
private string $sCsrfToken = '';
|
||||||
|
|
||||||
public function __construct()
|
public function __construct() {
|
||||||
{
|
|
||||||
parent::__construct(__CLASS__);
|
parent::__construct(__CLASS__);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function setReqVal(string $sKey, $oValue, string $sValidation=''): void
|
private function setReqVal(string $sKey, $oValue, string $sValidation=''): void {
|
||||||
{
|
|
||||||
$this->asReq[$sKey] = $this->validateValue($sValidation, $oValue);
|
$this->asReq[$sKey] = $this->validateValue($sValidation, $oValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle($sProcessPage, array $argv = array()): string
|
public function handle($sProcessPage, array $argv = []): string {
|
||||||
{
|
|
||||||
//Start buffering so warnings/notices can be collected
|
//Start buffering so warnings/notices can be collected
|
||||||
ob_start();
|
ob_start();
|
||||||
|
|
||||||
//Parse variables
|
//Parse variables
|
||||||
$asReq = ToolBox::getRequest($argv);
|
$asReq = ToolBox::getRequest($argv);
|
||||||
$this->asReq = array();
|
$this->asReq = [];
|
||||||
$sAction = $asReq['a'] ?? '';
|
$sAction = $asReq['a'] ?? '';
|
||||||
$this->setReqVal('t', $asReq['t'] ?? '');
|
$this->setReqVal('t', $asReq['t'] ?? '');
|
||||||
$this->setReqVal('name', $asReq['name'] ?? '');
|
$this->setReqVal('name', $asReq['name'] ?? '');
|
||||||
@@ -90,8 +86,7 @@ class Controller extends PhpObject
|
|||||||
return $sResult;
|
return $sResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateMutationRequest(string $sAction): bool
|
private function validateMutationRequest(string $sAction): bool {
|
||||||
{
|
|
||||||
return
|
return
|
||||||
PHP_SAPI === 'cli'
|
PHP_SAPI === 'cli'
|
||||||
||
|
||
|
||||||
@@ -101,44 +96,38 @@ class Controller extends PhpObject
|
|||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getCsrfToken(): string
|
private function getCsrfToken(): string {
|
||||||
{
|
|
||||||
if($this->sCsrfToken === '') $this->initCsrfToken();
|
if($this->sCsrfToken === '') $this->initCsrfToken();
|
||||||
return $this->sCsrfToken;
|
return $this->sCsrfToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function setCsrfToken(): void
|
private function setCsrfToken(): void {
|
||||||
{
|
|
||||||
if(empty($_SESSION['csrf_token'])) $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
if(empty($_SESSION['csrf_token'])) $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
$this->sCsrfToken = $_SESSION['csrf_token'];
|
$this->sCsrfToken = $_SESSION['csrf_token'];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function initCsrfToken(): void
|
private function initCsrfToken(): void {
|
||||||
{
|
|
||||||
if(PHP_SAPI === 'cli') return;
|
if(PHP_SAPI === 'cli') return;
|
||||||
|
|
||||||
if(session_status() !== PHP_SESSION_ACTIVE) {
|
if(session_status() !== PHP_SESSION_ACTIVE) {
|
||||||
$bSecure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https');
|
$bSecure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https');
|
||||||
session_set_cookie_params(array('httponly' => true, 'secure' => $bSecure, 'samesite' => 'Lax'));
|
session_set_cookie_params(['httponly' => true, 'secure' => $bSecure, 'samesite' => 'Lax']);
|
||||||
session_start();
|
session_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->setCsrfToken();
|
$this->setCsrfToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function checkCsrfToken(string $sClientToken): bool
|
private function checkCsrfToken(string $sClientToken): bool {
|
||||||
{
|
|
||||||
$sServerToken = $this->getCsrfToken();
|
$sServerToken = $this->getCsrfToken();
|
||||||
return PHP_SAPI === 'cli' || ($sServerToken !== '' && is_string($sClientToken) && hash_equals($sServerToken, $sClientToken));
|
return PHP_SAPI === 'cli' || ($sServerToken !== '' && is_string($sClientToken) && hash_equals($sServerToken, $sClientToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function closeSession(): void
|
private function closeSession(): void {
|
||||||
{
|
|
||||||
if(session_status() === PHP_SESSION_ACTIVE) session_write_close();
|
if(session_status() === PHP_SESSION_ACTIVE) session_write_close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function dispatch(string $sAction): string
|
private function dispatch(string $sAction): string {
|
||||||
{
|
|
||||||
return match($sAction) {
|
return match($sAction) {
|
||||||
'markers' => $this->oLivetrail->getMarkers(),
|
'markers' => $this->oLivetrail->getMarkers(),
|
||||||
'last_update' => $this->oLivetrail->getLastUpdate(),
|
'last_update' => $this->oLivetrail->getLastUpdate(),
|
||||||
@@ -154,8 +143,7 @@ class Controller extends PhpObject
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private function dispatchAdmin(string $sAction): string
|
private function dispatchAdmin(string $sAction): string {
|
||||||
{
|
|
||||||
if(!$this->oLivetrail->checkUserClearance(User::CLEARANCE_ADMIN)) {
|
if(!$this->oLivetrail->checkUserClearance(User::CLEARANCE_ADMIN)) {
|
||||||
return Livetrail::getJsonResult(false, Livetrail::NOT_FOUND);
|
return Livetrail::getJsonResult(false, Livetrail::NOT_FOUND);
|
||||||
}
|
}
|
||||||
@@ -173,11 +161,10 @@ class Controller extends PhpObject
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function validateValue(string $sValidation, $oValue=0)
|
private static function validateValue(string $sValidation, $oValue=0) {
|
||||||
{
|
|
||||||
return match($sValidation) {
|
return match($sValidation) {
|
||||||
'' => $oValue,
|
'' => $oValue,
|
||||||
'positiveInt' => filter_var($oValue, FILTER_VALIDATE_INT, array('options' => array('default' => 0, 'min_range' => 0)))
|
'positiveInt' => filter_var($oValue, FILTER_VALIDATE_INT, ['options' => ['default' => 0, 'min_range' => 0]])
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-5
@@ -17,7 +17,7 @@ class Converter extends PhpObject {
|
|||||||
parent::__construct(__CLASS__);
|
parent::__construct(__CLASS__);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function convertToGeoJson($sCodeName) {
|
public static function convertToGeoJson(string $sCodeName) {
|
||||||
$oGpx = new Gpx($sCodeName);
|
$oGpx = new Gpx($sCodeName);
|
||||||
$oGeoJson = new GeoJson($sCodeName);
|
$oGeoJson = new GeoJson($sCodeName);
|
||||||
|
|
||||||
@@ -32,11 +32,17 @@ class Converter extends PhpObject {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function isGeoJsonValid($sCodeName) {
|
public static function hasGpxFile(string $sCodeName) {
|
||||||
|
return ($sCodeName != '' && file_exists(Gpx::getBackendFilePath($sCodeName)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isGeoJsonValid(string $sCodeName) {
|
||||||
$sGpxFilePath = Gpx::getBackendFilePath($sCodeName);
|
$sGpxFilePath = Gpx::getBackendFilePath($sCodeName);
|
||||||
$sGeoJsonFilePath = GeoJson::getBackendFilePath($sCodeName);
|
$sGeoJsonFilePath = GeoJson::getBackendFilePath($sCodeName);
|
||||||
|
|
||||||
//No need to generate if gpx is missing
|
return
|
||||||
return !file_exists($sGpxFilePath) || file_exists($sGeoJsonFilePath) && filemtime($sGeoJsonFilePath) >= filemtime($sGpxFilePath);
|
!self::hasGpxFile($sCodeName) //No GPX, no need for geoJSON
|
||||||
|
||
|
||||||
|
file_exists($sGeoJsonFilePath) && filemtime($sGeoJsonFilePath) >= filemtime($sGpxFilePath); //geoJSON needs to be (re)generated
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -25,7 +25,7 @@ class Email extends PhpObject {
|
|||||||
parent::__construct(__CLASS__);
|
parent::__construct(__CLASS__);
|
||||||
$this->sServName = $sServName;
|
$this->sServName = $sServName;
|
||||||
$this->setTemplate($sTemplateName);
|
$this->setTemplate($sTemplateName);
|
||||||
$this->asDests = array();
|
$this->asDests = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setTemplate($sTemplateName) {
|
public function setTemplate($sTemplateName) {
|
||||||
@@ -39,7 +39,7 @@ class Email extends PhpObject {
|
|||||||
* @param array $asDests Contains: id_user, name, email, language, timezone, active
|
* @param array $asDests Contains: id_user, name, email, language, timezone, active
|
||||||
*/
|
*/
|
||||||
public function setDestInfo($asDests) {
|
public function setDestInfo($asDests) {
|
||||||
if(array_key_exists('email', $asDests)) $asDests = array($asDests);
|
if(array_key_exists('email', $asDests)) $asDests = [$asDests];
|
||||||
$this->asDests = $asDests;
|
$this->asDests = $asDests;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+79
-72
@@ -13,35 +13,32 @@ use \Settings;
|
|||||||
class Feed extends PhpObject {
|
class Feed extends PhpObject {
|
||||||
|
|
||||||
//Spot feed
|
//Spot feed
|
||||||
const FEED_HOOK = 'https://api.findmespot.com/spot-main-web/consumer/rest-api/2.0/public/feed/';
|
private const FEED_HOOK = 'https://api.findmespot.com/spot-main-web/consumer/rest-api/2.0/public/feed/';
|
||||||
const FEED_TYPE_XML = '/message.xml';
|
private const FEED_TYPE_XML = '/message.xml';
|
||||||
const FEED_TYPE_JSON = '/message.json';
|
private const FEED_TYPE_JSON = '/message.json';
|
||||||
const FEED_MAX_REFRESH = 5 * 60; //Seconds
|
private const FEED_MAX_REFRESH = 5 * 60; //Seconds
|
||||||
|
|
||||||
//Weather
|
//Weather
|
||||||
const WEATHER_HOOK = 'https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline';
|
private const WEATHER_HOOK = 'https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline';
|
||||||
const WEATHER_PARAM = array(
|
private const WEATHER_PARAM = [
|
||||||
'key' => Settings::WEATHER_TOKEN,
|
'key' => Settings::WEATHER_TOKEN,
|
||||||
'unitGroup' => 'metric',
|
'unitGroup' => 'metric',
|
||||||
'lang' => 'en',
|
'lang' => 'en',
|
||||||
'include' => 'current',
|
'include' => 'current',
|
||||||
'iconSet' => 'icons2'
|
'iconSet' => 'icons2'
|
||||||
);
|
];
|
||||||
|
|
||||||
//Timezone
|
//Timezone
|
||||||
const TIMEZONE_HOOK = 'http://api.geonames.org/timezoneJSON';
|
private const TIMEZONE_HOOK = 'http://api.geonames.org/timezoneJSON';
|
||||||
const TIMEZONE_PARAM = array(
|
|
||||||
'username' => Settings::TIMEZONE_USER
|
|
||||||
);
|
|
||||||
|
|
||||||
//DB Tables
|
//DB Tables
|
||||||
const SPOT_TABLE = 'spots';
|
public const SPOT_TABLE = 'spots';
|
||||||
const FEED_TABLE = 'feeds';
|
public const FEED_TABLE = 'feeds';
|
||||||
const MSG_TABLE = 'messages';
|
public const MSG_TABLE = 'messages';
|
||||||
|
|
||||||
//Hide/Display values
|
//Hide/Display values
|
||||||
const MSG_HIDDEN = 0;
|
public const MSG_HIDDEN = 0;
|
||||||
const MSG_DISPLAYED = 1;
|
public const MSG_DISPLAYED = 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database Handle
|
* Database Handle
|
||||||
@@ -75,10 +72,10 @@ class Feed extends PhpObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function createFeedId($oProjectId) {
|
public function createFeedId($oProjectId) {
|
||||||
$this->setFeedId($this->oDb->insertRow(self::FEED_TABLE, array(
|
$this->setFeedId($this->oDb->insertRow(self::FEED_TABLE, [
|
||||||
Db::getId(Project::PROJ_TABLE) => $oProjectId,
|
Db::getId(Project::PROJ_TABLE) => $oProjectId,
|
||||||
'status' => 'INACTIVE'
|
'status' => 'INACTIVE'
|
||||||
)));
|
]));
|
||||||
return $this->getFeedId();
|
return $this->getFeedId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,14 +92,14 @@ class Feed extends PhpObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function getSpots() {
|
public function getSpots() {
|
||||||
$asSpots = $this->oDb->selectRows(array('from'=>self::SPOT_TABLE));
|
$asSpots = $this->oDb->selectRows(['from'=>self::SPOT_TABLE]);
|
||||||
foreach($asSpots as &$asSpot) $asSpot['id'] = $asSpot[Db::getId(self::SPOT_TABLE)];
|
foreach($asSpots as &$asSpot) $asSpot['id'] = $asSpot[Db::getId(self::SPOT_TABLE)];
|
||||||
return $asSpots;
|
return $asSpots;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFeeds($iFeedId=0) {
|
public function getFeeds($iFeedId=0) {
|
||||||
$asInfo = array('from'=>self::FEED_TABLE);
|
$asInfo = ['from'=>self::FEED_TABLE];
|
||||||
if($iFeedId > 0) $asInfo['constraint'] = array(Db::getId(self::FEED_TABLE)=>$iFeedId);
|
if($iFeedId > 0) $asInfo['constraint'] = [Db::getId(self::FEED_TABLE)=>$iFeedId];
|
||||||
$asFeeds = $this->oDb->selectRows($asInfo);
|
$asFeeds = $this->oDb->selectRows($asInfo);
|
||||||
|
|
||||||
foreach($asFeeds as &$asFeed) $asFeed['id'] = $asFeed[Db::getId(self::FEED_TABLE)];
|
foreach($asFeeds as &$asFeed) $asFeed['id'] = $asFeed[Db::getId(self::FEED_TABLE)];
|
||||||
@@ -114,21 +111,21 @@ class Feed extends PhpObject {
|
|||||||
return array_shift($asFeeds);
|
return array_shift($asFeeds);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getMessages($asConstraints=array()) {
|
public function getMessages($asConstraints=[]) {
|
||||||
$sFeedIdCol = Db::getId(self::FEED_TABLE, true);
|
$sFeedIdCol = Db::getId(self::FEED_TABLE, true);
|
||||||
$asInfo = array(
|
$asInfo = [
|
||||||
'select' => array(
|
'select' => [
|
||||||
Db::getId(self::MSG_TABLE), 'ref_msg_id', 'type', //ID
|
Db::getId(self::MSG_TABLE), 'ref_msg_id', 'type', //ID
|
||||||
'latitude', 'longitude', //Position
|
'latitude', 'longitude', //Position
|
||||||
'site_time', 'timezone', 'unix_time', //Time
|
'site_time', 'timezone', 'unix_time', //Time
|
||||||
'weather_icon', 'weather_cond', 'weather_temp' //Weather
|
'weather_icon', 'weather_cond', 'weather_temp' //Weather
|
||||||
),
|
],
|
||||||
'from' => self::MSG_TABLE,
|
'from' => self::MSG_TABLE,
|
||||||
'join' => array(self::FEED_TABLE => Db::getId(self::FEED_TABLE)),
|
'join' => [self::FEED_TABLE => Db::getId(self::FEED_TABLE)],
|
||||||
'constraint'=> array($sFeedIdCol => $this->getFeedId(), 'display' => self::MSG_DISPLAYED),
|
'constraint'=> [$sFeedIdCol => $this->getFeedId(), 'display' => self::MSG_DISPLAYED],
|
||||||
'constOpe' => array($sFeedIdCol => "=", 'display' => "="),
|
'constOpe' => [$sFeedIdCol => '=', 'display' => '='],
|
||||||
'orderBy' => array('site_time'=>'ASC')
|
'orderBy' => ['site_time'=>'ASC']
|
||||||
);
|
];
|
||||||
if(!empty($asConstraints)) $asInfo = array_merge($asInfo, $asConstraints);
|
if(!empty($asConstraints)) $asInfo = array_merge($asInfo, $asConstraints);
|
||||||
|
|
||||||
$asResult = $this->oDb->selectRows($asInfo);
|
$asResult = $this->oDb->selectRows($asInfo);
|
||||||
@@ -137,7 +134,7 @@ class Feed extends PhpObject {
|
|||||||
$iCount = 0;
|
$iCount = 0;
|
||||||
foreach($asResult as &$asMsg) {
|
foreach($asResult as &$asMsg) {
|
||||||
if($asMsg['weather_icon'] == '' && $iCount < 3) {
|
if($asMsg['weather_icon'] == '' && $iCount < 3) {
|
||||||
$asWeather = $this->getWeather(array($asMsg['latitude'], $asMsg['longitude']), $asMsg['unix_time']);
|
$asWeather = $this->getWeather([$asMsg['latitude'], $asMsg['longitude']], $asMsg['unix_time']);
|
||||||
$asMsg = array_merge($asMsg, $asWeather);
|
$asMsg = array_merge($asMsg, $asWeather);
|
||||||
$this->oDb->updateRow(self::MSG_TABLE, $asMsg[Db::getId(self::MSG_TABLE)], $asWeather, false);
|
$this->oDb->updateRow(self::MSG_TABLE, $asMsg[Db::getId(self::MSG_TABLE)], $asWeather, false);
|
||||||
$iCount++;
|
$iCount++;
|
||||||
@@ -148,7 +145,7 @@ class Feed extends PhpObject {
|
|||||||
return $asResult;
|
return $asResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLastMessageId($asConstraints=array()) {
|
public function getLastMessageId($asConstraints=[]) {
|
||||||
$asMessages = $this->getMessages($asConstraints);
|
$asMessages = $this->getMessages($asConstraints);
|
||||||
return end($asMessages)[Db::getId(self::MSG_TABLE)] ?? 0;
|
return end($asMessages)[Db::getId(self::MSG_TABLE)] ?? 0;
|
||||||
}
|
}
|
||||||
@@ -172,7 +169,7 @@ class Feed extends PhpObject {
|
|||||||
$sTimeZone = date_default_timezone_get();
|
$sTimeZone = date_default_timezone_get();
|
||||||
$oDateTime = new \DateTime('@'.$iTimestamp);
|
$oDateTime = new \DateTime('@'.$iTimestamp);
|
||||||
$oDateTime->setTimezone(new \DateTimeZone($sTimeZone));
|
$oDateTime->setTimezone(new \DateTimeZone($sTimeZone));
|
||||||
$asWeather = $this->getWeather(array($sLat, $sLng), $iTimestamp);
|
$asWeather = $this->getWeather([$sLat, $sLng], $iTimestamp);
|
||||||
|
|
||||||
$asMsg = [
|
$asMsg = [
|
||||||
'ref_msg_id' => $iTimestamp.'/man',
|
'ref_msg_id' => $iTimestamp.'/man',
|
||||||
@@ -203,53 +200,53 @@ class Feed extends PhpObject {
|
|||||||
|
|
||||||
//Fix unstable Spot API Structure
|
//Fix unstable Spot API Structure
|
||||||
if(array_key_exists('message', $asMsgs)) $asMsgs = $asMsgs['message']; //Sometimes adds an extra "message" level
|
if(array_key_exists('message', $asMsgs)) $asMsgs = $asMsgs['message']; //Sometimes adds an extra "message" level
|
||||||
if(!array_key_exists(0, $asMsgs)) $asMsgs = array($asMsgs); //Jumps a level when there is only 1 message
|
if(!array_key_exists(0, $asMsgs)) $asMsgs = [$asMsgs]; //Jumps a level when there is only 1 message
|
||||||
|
|
||||||
//Update Spot, Feed & Messages
|
//Update Spot, Feed & Messages
|
||||||
if(!empty($asMsgs) && array_key_exists('messengerId', $asMsgs[0])) {
|
if(!empty($asMsgs) && array_key_exists('messengerId', $asMsgs[0])) {
|
||||||
|
|
||||||
//Update Spot Info from the first message
|
//Update Spot Info from the first message
|
||||||
$asSpotInfo = array(
|
$asSpotInfo = [
|
||||||
'ref_spot_id' => $asMsgs[0]['messengerId'],
|
'ref_spot_id' => $asMsgs[0]['messengerId'],
|
||||||
'name' => $asMsgs[0]['messengerName'],
|
'name' => $asMsgs[0]['messengerName'],
|
||||||
'model' => $asMsgs[0]['modelId']
|
'model' => $asMsgs[0]['modelId']
|
||||||
);
|
];
|
||||||
$iSpotId = $this->oDb->insertUpdateRow(self::SPOT_TABLE, $asSpotInfo, array('ref_spot_id'));
|
$iSpotId = $this->oDb->insertUpdateRow(self::SPOT_TABLE, $asSpotInfo, ['ref_spot_id']);
|
||||||
|
|
||||||
//Update Feed Info and last update date
|
//Update Feed Info and last update date
|
||||||
$asFeedInfo = array(
|
$asFeedInfo = [
|
||||||
'ref_feed_id' => $asFeed['id'],
|
'ref_feed_id' => $asFeed['id'],
|
||||||
Db::getId(self::SPOT_TABLE) => $iSpotId,
|
Db::getId(self::SPOT_TABLE) => $iSpotId,
|
||||||
'name' => $asFeed['name'],
|
'name' => $asFeed['name'],
|
||||||
'description' => $asFeed['description'],
|
'description' => $asFeed['description'],
|
||||||
'status' => $asFeed['status'],
|
'status' => $asFeed['status'],
|
||||||
'last_update' => $sNow
|
'last_update' => $sNow
|
||||||
);
|
];
|
||||||
$iFeedId = $this->oDb->insertUpdateRow(self::FEED_TABLE, $asFeedInfo, array('ref_feed_id'));
|
$iFeedId = $this->oDb->insertUpdateRow(self::FEED_TABLE, $asFeedInfo, ['ref_feed_id']);
|
||||||
|
|
||||||
//Update Messages
|
//Update Messages
|
||||||
foreach($asMsgs as $asMsg) {
|
foreach($asMsgs as $asMsg) {
|
||||||
$asMsg = array(
|
$asMsg = [
|
||||||
'ref_msg_id' => $asMsg['id'],
|
'ref_msg_id' => $asMsg['id'],
|
||||||
Db::getId(self::FEED_TABLE) => $iFeedId,
|
Db::getId(self::FEED_TABLE) => $iFeedId,
|
||||||
'type' => $asMsg['messageType'],
|
'type' => $asMsg['messageType'],
|
||||||
'latitude' => $asMsg['latitude'],
|
'latitude' => $asMsg['latitude'],
|
||||||
'longitude' => $asMsg['longitude'],
|
'longitude' => $asMsg['longitude'],
|
||||||
'iso_time' => $asMsg['dateTime'], //ISO 8601 time (backup)
|
'iso_time' => $asMsg['dateTime'], //ISO 8601 time (backup)
|
||||||
'site_time' => date(Db::TIMESTAMP_FORMAT, $asMsg['unixTime']), //Conversion to Site Time
|
'site_time' => date(Db::TIMESTAMP_FORMAT, $asMsg['unixTime']), //Conversion to Site Time
|
||||||
'timezone' => $this->getTimeZone(array($asMsg['latitude'], $asMsg['longitude']), $asMsg['unixTime']),
|
'timezone' => $this->getTimeZone($asMsg['latitude'], $asMsg['longitude']), //Get message time zone
|
||||||
'unix_time' => $asMsg['unixTime'], //UNIX Time (backup)
|
'unix_time' => $asMsg['unixTime'], //UNIX Time (backup)
|
||||||
'content' => $asMsg['messageContent'],
|
'content' => $asMsg['messageContent'],
|
||||||
'battery_state' => $asMsg['batteryState']
|
'battery_state' => $asMsg['batteryState']
|
||||||
);
|
];
|
||||||
|
|
||||||
$iMsgId = $this->oDb->selectId(self::MSG_TABLE, array('ref_msg_id'=>$asMsg['ref_msg_id']));
|
$iMsgId = $this->oDb->selectId(self::MSG_TABLE, ['ref_msg_id'=>$asMsg['ref_msg_id']]);
|
||||||
if(!$iMsgId) {
|
if(!$iMsgId) {
|
||||||
//First Catch
|
//First Catch
|
||||||
$asMsg['posted_on'] = $sNow;
|
$asMsg['posted_on'] = $sNow;
|
||||||
|
|
||||||
//Weather Data
|
//Weather Data
|
||||||
$asMsg = array_merge($asMsg, $this->getWeather(array($asMsg['latitude'], $asMsg['longitude']), $asMsg['unix_time']));
|
$asMsg = array_merge($asMsg, $this->getWeather([$asMsg['latitude'], $asMsg['longitude']], $asMsg['unix_time']));
|
||||||
|
|
||||||
$this->oDb->insertRow(self::MSG_TABLE, $asMsg);
|
$this->oDb->insertRow(self::MSG_TABLE, $asMsg);
|
||||||
$bNewMsg = true;
|
$bNewMsg = true;
|
||||||
@@ -258,7 +255,7 @@ class Feed extends PhpObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else $this->oDb->updateRow(self::FEED_TABLE, $this->getFeedId(), array('last_update'=>$sNow));
|
else $this->oDb->updateRow(self::FEED_TABLE, $this->getFeedId(), ['last_update'=>$sNow]);
|
||||||
|
|
||||||
return $bNewMsg;
|
return $bNewMsg;
|
||||||
}
|
}
|
||||||
@@ -281,20 +278,22 @@ class Feed extends PhpObject {
|
|||||||
$sWeatherIcon = 'unknown';
|
$sWeatherIcon = 'unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
//Get Condition ID
|
//Get Condition Language ID
|
||||||
$sCondKey = (new Translator(self::WEATHER_PARAM['lang']))->getTranslationKey($sWeatherCond);
|
$sCondLangId = (new Translator(self::WEATHER_PARAM['lang']))->getTranslationKey($sWeatherCond);
|
||||||
|
|
||||||
return array(
|
return [
|
||||||
'weather_icon' => $sWeatherIcon,
|
'weather_icon' => $sWeatherIcon,
|
||||||
'weather_cond' => $sCondKey,
|
'weather_cond' => $sCondLangId,
|
||||||
'weather_temp' => floatval($sWeatherTemp)
|
'weather_temp' => floatval($sWeatherTemp)
|
||||||
);
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getTimeZone($asLatLng, $iTimeStamp) {
|
private function getTimeZone($iLat, $iLng) {
|
||||||
$asParams = self::TIMEZONE_PARAM;
|
$asParams = [
|
||||||
$asParams['lat'] = $asLatLng[0];
|
'username' => Settings::TIMEZONE_USER,
|
||||||
$asParams['lng'] = $asLatLng[1];
|
'lat' => $iLat,
|
||||||
|
'lng' => $iLng
|
||||||
|
];
|
||||||
$sApiUrl = self::TIMEZONE_HOOK.'?'.http_build_query($asParams);
|
$sApiUrl = self::TIMEZONE_HOOK.'?'.http_build_query($asParams);
|
||||||
|
|
||||||
$asTimeZone = json_decode(file_get_contents($sApiUrl), true);
|
$asTimeZone = json_decode(file_get_contents($sApiUrl), true);
|
||||||
@@ -303,32 +302,40 @@ class Feed extends PhpObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private function retrieveFeed() {
|
private function retrieveFeed() {
|
||||||
$sContent = '[]';
|
if($this->sRefFeedId == '') {
|
||||||
if($this->sRefFeedId !='') {
|
$sContent = '{"response":{"errors":{"error":{"code":"","text":"Feed reference ID missing","description":"Feed reference ID missing"}}}}';
|
||||||
|
}
|
||||||
|
else {
|
||||||
$sUrl = self::FEED_HOOK.$this->sRefFeedId.self::FEED_TYPE_JSON;
|
$sUrl = self::FEED_HOOK.$this->sRefFeedId.self::FEED_TYPE_JSON;
|
||||||
$sContent = file_get_contents($sUrl);
|
$sContent = file_get_contents($sUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
return json_decode($sContent, true);
|
return json_decode($sContent, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function updateField($sField, $oValue) {
|
private function updateField($sField, $oValue) {
|
||||||
$bResult = ($this->oDb->updateRow(self::FEED_TABLE, $this->getFeedId(), array($sField=>$oValue)) > 0);
|
$bResult = ($this->oDb->updateRow(self::FEED_TABLE, $this->getFeedId(), [$sField=>$oValue]) > 0);
|
||||||
$this->setFeedId($this->getFeedId());
|
$this->setFeedId($this->getFeedId());
|
||||||
|
|
||||||
return $bResult;
|
return $bResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete() {
|
public function delete() {
|
||||||
$asResult = array();
|
$bSuccess = false;
|
||||||
if($this->getFeedId() > 0) {
|
$sLangId = '';
|
||||||
$asResult = array(
|
$asLangParams = [];
|
||||||
'id' => $this->getFeedId(),
|
$asData = [];
|
||||||
'del' => $this->oDb->deleteRow(self::FEED_TABLE, $this->getFeedId()),
|
|
||||||
'desc' => $this->oDb->getLastError()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else $asResult = array('del'=>false, 'desc'=>'Error while setting project: no Feed ID');
|
|
||||||
|
|
||||||
return $asResult;
|
if($this->getFeedId() > 0) {
|
||||||
|
$asData['id'] = $this->getFeedId();
|
||||||
|
$bSuccess = $this->oDb->deleteRow(self::FEED_TABLE, $this->getFeedId());
|
||||||
|
if(!$bSuccess) $sLangId = 'error.commit_db';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$sLangId = 'error.impossible_value';
|
||||||
|
$asLangParams = [$this->getFeedId(), 'feed ID'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Livetrail::getResult($bSuccess, $sLangId, $asData, $asLangParams);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -7,8 +7,8 @@ use \Settings;
|
|||||||
abstract class Geo extends PhpObject {
|
abstract class Geo extends PhpObject {
|
||||||
protected const EXT = '';
|
protected const EXT = '';
|
||||||
|
|
||||||
const GEO_FOLDER = 'geo';
|
protected const GEO_FOLDER = 'geo';
|
||||||
const OPT_SIMPLE = 'simplification';
|
protected const OPT_SIMPLE = 'simplification';
|
||||||
|
|
||||||
protected array $asTracks;
|
protected array $asTracks;
|
||||||
protected string $sFilePath;
|
protected string $sFilePath;
|
||||||
@@ -16,7 +16,7 @@ abstract class Geo extends PhpObject {
|
|||||||
public function __construct(string $sCodeName) {
|
public function __construct(string $sCodeName) {
|
||||||
parent::__construct(get_class($this), Settings::DEBUG, PhpObject::MODE_HTML);
|
parent::__construct(get_class($this), Settings::DEBUG, PhpObject::MODE_HTML);
|
||||||
$this->sFilePath = self::getBackEndFilePath($sCodeName);
|
$this->sFilePath = self::getBackEndFilePath($sCodeName);
|
||||||
$this->asTracks = array();
|
$this->asTracks = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
//Access from backend
|
//Access from backend
|
||||||
@@ -32,4 +32,4 @@ abstract class Geo extends PhpObject {
|
|||||||
public function getLog() {
|
public function getLog() {
|
||||||
return $this->getCleanMessageStack(PhpObject::NOTICE_TAB);
|
return $this->getCleanMessageStack(PhpObject::NOTICE_TAB);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-24
@@ -3,11 +3,11 @@
|
|||||||
namespace Franzz\Livetrail;
|
namespace Franzz\Livetrail;
|
||||||
|
|
||||||
class GeoJson extends Geo {
|
class GeoJson extends Geo {
|
||||||
|
protected const EXT = '.geojson';
|
||||||
|
|
||||||
const EXT = '.geojson';
|
private const MAX_FILESIZE = 2; //MB
|
||||||
const MAX_FILESIZE = 2; //MB
|
private const MAX_DEVIATION_FLAT = 0.1; //10%
|
||||||
const MAX_DEVIATION_FLAT = 0.1; //10%
|
private const MAX_DEVIATION_ELEV = 0.1; //10%
|
||||||
const MAX_DEVIATION_ELEV = 0.1; //10%
|
|
||||||
|
|
||||||
public function __construct($sCodeName) {
|
public function __construct($sCodeName) {
|
||||||
parent::__construct($sCodeName);
|
parent::__construct($sCodeName);
|
||||||
@@ -38,7 +38,7 @@ class GeoJson extends Geo {
|
|||||||
$iGlobalInvalidPointCount = 0;
|
$iGlobalInvalidPointCount = 0;
|
||||||
$iGlobalPointCount = 0;
|
$iGlobalPointCount = 0;
|
||||||
|
|
||||||
$this->asTracks = array();
|
$this->asTracks = [];
|
||||||
foreach($asTracks as $asTrackProps) {
|
foreach($asTracks as $asTrackProps) {
|
||||||
$asOptions = $this->parseOptions($asTrackProps['cmt']);
|
$asOptions = $this->parseOptions($asTrackProps['cmt']);
|
||||||
|
|
||||||
@@ -64,18 +64,18 @@ class GeoJson extends Geo {
|
|||||||
continue 2; //discard tracks
|
continue 2; //discard tracks
|
||||||
}
|
}
|
||||||
|
|
||||||
$asTrack = array(
|
$asTrack = [
|
||||||
'type' => 'Feature',
|
'type' => 'Feature',
|
||||||
'properties' => array(
|
'properties' => [
|
||||||
'name' => $asTrackProps['name'],
|
'name' => $asTrackProps['name'],
|
||||||
'type' => $sType,
|
'type' => $sType,
|
||||||
'description' => $asTrackProps['desc']
|
'description' => $asTrackProps['desc']
|
||||||
),
|
],
|
||||||
'geometry' => array(
|
'geometry' => [
|
||||||
'type' => 'LineString',
|
'type' => 'LineString',
|
||||||
'coordinates' => array()
|
'coordinates' => []
|
||||||
)
|
]
|
||||||
);
|
];
|
||||||
|
|
||||||
if($sType != 'hitchhiking' && str_contains($asTrackProps['desc'], ' ➜ ')) {
|
if($sType != 'hitchhiking' && str_contains($asTrackProps['desc'], ' ➜ ')) {
|
||||||
list($sFrom, $sTo) = explode(' ➜ ', $asTrackProps['desc']);
|
list($sFrom, $sTo) = explode(' ➜ ', $asTrackProps['desc']);
|
||||||
@@ -86,9 +86,9 @@ class GeoJson extends Geo {
|
|||||||
$asTrackPoints = $asTrackProps['points'];
|
$asTrackPoints = $asTrackProps['points'];
|
||||||
$iPointCount = count($asTrackPoints);
|
$iPointCount = count($asTrackPoints);
|
||||||
$iInvalidPointCount = 0;
|
$iInvalidPointCount = 0;
|
||||||
$asPrevPoint = array();
|
$asPrevPoint = [];
|
||||||
foreach($asTrackPoints as $iIndex=>$asPoint) {
|
foreach($asTrackPoints as $iIndex=>$asPoint) {
|
||||||
$asNextPoint = ($iIndex < ($iPointCount - 1))?$asTrackPoints[$iIndex + 1]:array();
|
$asNextPoint = ($iIndex < ($iPointCount - 1))?$asTrackPoints[$iIndex + 1]:[];
|
||||||
if($bSimplify && !empty($asPrevPoint) && !empty($asNextPoint)) {
|
if($bSimplify && !empty($asPrevPoint) && !empty($asNextPoint)) {
|
||||||
if(!$this->isPointValid($asPrevPoint, $asPoint, $asNextPoint)) {
|
if(!$this->isPointValid($asPrevPoint, $asPoint, $asNextPoint)) {
|
||||||
$iInvalidPointCount++;
|
$iInvalidPointCount++;
|
||||||
@@ -112,11 +112,11 @@ class GeoJson extends Geo {
|
|||||||
$this->addNotice('Sorting off-tracks');
|
$this->addNotice('Sorting off-tracks');
|
||||||
|
|
||||||
//Find first & last track points
|
//Find first & last track points
|
||||||
$asTracksEnds = array();
|
$asTracksEnds = [];
|
||||||
$asTracks = array();
|
$asTracks = [];
|
||||||
foreach($this->asTracks as $iTrackId=>$asTrack) {
|
foreach($this->asTracks as $iTrackId=>$asTrack) {
|
||||||
$sTrackId = 't'.$iTrackId;
|
$sTrackId = 't'.$iTrackId;
|
||||||
$asTracksEnds[$sTrackId] = array('first'=>reset($asTrack['geometry']['coordinates']), 'last'=>end($asTrack['geometry']['coordinates']));
|
$asTracksEnds[$sTrackId] = ['first'=>reset($asTrack['geometry']['coordinates']), 'last'=>end($asTrack['geometry']['coordinates'])];
|
||||||
$asTracks[$sTrackId] = $asTrack;
|
$asTracks[$sTrackId] = $asTrack;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,14 +153,14 @@ class GeoJson extends Geo {
|
|||||||
//Move track
|
//Move track
|
||||||
unset($asTracks[$sTrackId]);
|
unset($asTracks[$sTrackId]);
|
||||||
$iOffset = array_search($sConnectedTrackId, array_keys($asTracks)) + $iPosition;
|
$iOffset = array_search($sConnectedTrackId, array_keys($asTracks)) + $iPosition;
|
||||||
$asTracks = array_slice($asTracks, 0, $iOffset) + array($sTrackId => $asTrack) + array_slice($asTracks, $iOffset);
|
$asTracks = array_slice($asTracks, 0, $iOffset) + [$sTrackId => $asTrack] + array_slice($asTracks, $iOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->asTracks = array_values($asTracks);
|
$this->asTracks = array_values($asTracks);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCenter() {
|
public function getCenter() {
|
||||||
$asCoords = array();
|
$asCoords = [];
|
||||||
$asMainTracks = array_filter($this->asTracks, function ($astrack) {return $astrack['properties']['type'] == 'main';});
|
$asMainTracks = array_filter($this->asTracks, function ($astrack) {return $astrack['properties']['type'] == 'main';});
|
||||||
foreach($asMainTracks as $asMainTrack) {
|
foreach($asMainTracks as $asMainTrack) {
|
||||||
foreach($asMainTrack['geometry']['coordinates'] as $aiCoords) {
|
foreach($asMainTrack['geometry']['coordinates'] as $aiCoords) {
|
||||||
@@ -173,7 +173,7 @@ class GeoJson extends Geo {
|
|||||||
|
|
||||||
private function parseOptions($sComment) {
|
private function parseOptions($sComment) {
|
||||||
$sComment = strip_tags(html_entity_decode($sComment));
|
$sComment = strip_tags(html_entity_decode($sComment));
|
||||||
$asOptions = array(self::OPT_SIMPLE=>'');
|
$asOptions = [self::OPT_SIMPLE=>''];
|
||||||
foreach(explode("\n", $sComment) as $sLine) {
|
foreach(explode("\n", $sComment) as $sLine) {
|
||||||
$asOptions[mb_strtolower(trim(mb_strstr($sLine, ':', true)))] = mb_strtolower(trim(mb_substr(mb_strstr($sLine, ':'), 1)));
|
$asOptions[mb_strtolower(trim(mb_strstr($sLine, ':', true)))] = mb_strtolower(trim(mb_substr(mb_strstr($sLine, ':'), 1)));
|
||||||
}
|
}
|
||||||
@@ -189,8 +189,8 @@ class GeoJson extends Geo {
|
|||||||
|
|
||||||
//Path Turn Check -> -> -> ->
|
//Path Turn Check -> -> -> ->
|
||||||
//Law of Cosines (vector): angle = arccos(OA.OB / ||OA||.||OB||)
|
//Law of Cosines (vector): angle = arccos(OA.OB / ||OA||.||OB||)
|
||||||
$fVectorOA = array('lon'=>($asPointA['lon'] - $asPointO['lon']), 'lat'=> ($asPointA['lat'] - $asPointO['lat']));
|
$fVectorOA = ['lon'=>($asPointA['lon'] - $asPointO['lon']), 'lat'=> ($asPointA['lat'] - $asPointO['lat'])];
|
||||||
$fVectorOB = array('lon'=>($asPointB['lon'] - $asPointO['lon']), 'lat'=> ($asPointB['lat'] - $asPointO['lat']));
|
$fVectorOB = ['lon'=>($asPointB['lon'] - $asPointO['lon']), 'lat'=> ($asPointB['lat'] - $asPointO['lat'])];
|
||||||
|
|
||||||
$fLengthOA = sqrt(pow($asPointA['lon'] - $asPointO['lon'], 2) + pow($asPointA['lat'] - $asPointO['lat'], 2));
|
$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));
|
$fLengthOB = sqrt(pow($asPointO['lon'] - $asPointB['lon'], 2) + pow($asPointO['lat'] - $asPointB['lat'], 2));
|
||||||
@@ -210,10 +210,10 @@ class GeoJson extends Geo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private function buildGeoJson() {
|
private function buildGeoJson() {
|
||||||
return json_encode(array('type'=>'FeatureCollection', 'features'=>$this->asTracks));
|
return json_encode(['type'=>'FeatureCollection', 'features'=>$this->asTracks]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function getDistance($asPointA, $asPointB) {
|
private static function getDistance($asPointA, $asPointB) {
|
||||||
$fLatFrom = $asPointA[1];
|
$fLatFrom = $asPointA[1];
|
||||||
$fLonFrom = $asPointA[0];
|
$fLonFrom = $asPointA[0];
|
||||||
$fLatTo = $asPointB[1];
|
$fLatTo = $asPointB[1];
|
||||||
|
|||||||
+7
-7
@@ -5,7 +5,7 @@ use Franzz\Objects\ToolBox;
|
|||||||
|
|
||||||
class Gpx extends Geo {
|
class Gpx extends Geo {
|
||||||
|
|
||||||
const EXT = '.gpx';
|
public const EXT = '.gpx';
|
||||||
|
|
||||||
public function __construct($sCodeName) {
|
public function __construct($sCodeName) {
|
||||||
parent::__construct($sCodeName);
|
parent::__construct($sCodeName);
|
||||||
@@ -25,21 +25,21 @@ class Gpx extends Geo {
|
|||||||
//Tracks
|
//Tracks
|
||||||
$this->addNotice('Converting '.count($oXml->trk).' tracks');
|
$this->addNotice('Converting '.count($oXml->trk).' tracks');
|
||||||
foreach($oXml->trk as $aoTrack) {
|
foreach($oXml->trk as $aoTrack) {
|
||||||
$asTrack = array(
|
$asTrack = [
|
||||||
'name' => (string) $aoTrack->name,
|
'name' => (string) $aoTrack->name,
|
||||||
'desc' => str_replace("\n", '', ToolBox::fixEOL((strip_tags($aoTrack->desc)))),
|
'desc' => str_replace("\n", '', ToolBox::fixEOL((strip_tags($aoTrack->desc)))),
|
||||||
'cmt' => ToolBox::fixEOL((strip_tags($aoTrack->cmt))),
|
'cmt' => ToolBox::fixEOL((strip_tags($aoTrack->cmt))),
|
||||||
'color' => (string) $aoTrack->extensions->children('gpxx', true)->TrackExtension->DisplayColor,
|
'color' => (string) $aoTrack->extensions->children('gpxx', true)->TrackExtension->DisplayColor,
|
||||||
'points'=> array()
|
'points'=> []
|
||||||
);
|
];
|
||||||
|
|
||||||
foreach($aoTrack->trkseg as $asSegment) {
|
foreach($aoTrack->trkseg as $asSegment) {
|
||||||
foreach($asSegment as $asPoint) {
|
foreach($asSegment as $asPoint) {
|
||||||
$asTrack['points'][] = array(
|
$asTrack['points'][] = [
|
||||||
'lon' => (float) $asPoint['lon'],
|
'lon' => (float) $asPoint['lon'],
|
||||||
'lat' => (float) $asPoint['lat'],
|
'lat' => (float) $asPoint['lat'],
|
||||||
'ele' => (int) $asPoint->ele
|
'ele' => (int) $asPoint->ele
|
||||||
);
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$this->asTracks[] = $asTrack;
|
$this->asTracks[] = $asTrack;
|
||||||
@@ -49,4 +49,4 @@ class Gpx extends Geo {
|
|||||||
$this->addNotice('Ignoring '.count($oXml->wpt).' waypoints');
|
$this->addNotice('Ignoring '.count($oXml->wpt).' waypoints');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+256
-247
@@ -5,7 +5,6 @@ use Franzz\Objects\Db;
|
|||||||
use Franzz\Objects\Main;
|
use Franzz\Objects\Main;
|
||||||
use Franzz\Objects\Translator;
|
use Franzz\Objects\Translator;
|
||||||
use Franzz\Objects\ToolBox;
|
use Franzz\Objects\ToolBox;
|
||||||
use Franzz\Objects\Mask;
|
|
||||||
use \Settings;
|
use \Settings;
|
||||||
|
|
||||||
/* Timezones
|
/* Timezones
|
||||||
@@ -32,27 +31,25 @@ use \Settings;
|
|||||||
* - timezone: Site Timezone (stored user's timezone for emails)
|
* - timezone: Site Timezone (stored user's timezone for emails)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class Livetrail extends Main
|
class Livetrail extends Main {
|
||||||
{
|
|
||||||
//Database
|
//Database
|
||||||
const POST_TABLE = 'posts';
|
public const POST_TABLE = 'posts';
|
||||||
|
|
||||||
const FEED_CHUNK_SIZE = 15;
|
private const FEED_CHUNK_SIZE = 15;
|
||||||
const MAIL_CHUNK_SIZE = 5;
|
private const MAIL_CHUNK_SIZE = 5;
|
||||||
|
|
||||||
const DEFAULT_LANG = 'en';
|
public const DEFAULT_LANG = 'en';
|
||||||
const PROJECT_NAME = 'LiveTrail';
|
public const PROJECT_NAME = 'LiveTrail';
|
||||||
|
|
||||||
const MAIN_PAGE = 'index';
|
private const MAIN_PAGE = 'index';
|
||||||
const VITE_APP = 'src/app.js';
|
private const VITE_APP = 'src/app.js';
|
||||||
|
|
||||||
private Project $oProject;
|
private Project $oProject;
|
||||||
private Media $oMedia;
|
private Media $oMedia;
|
||||||
private User $oUser;
|
private User $oUser;
|
||||||
private Map $oMap;
|
private Map $oMap;
|
||||||
|
|
||||||
public function __construct($sProcessPage, $sTimezone)
|
public function __construct($sProcessPage, $sTimezone) {
|
||||||
{
|
|
||||||
parent::__construct($sProcessPage, true, $sTimezone);
|
parent::__construct($sProcessPage, true, $sTimezone);
|
||||||
|
|
||||||
$this->oUser = new User($this->oDb);
|
$this->oUser = new User($this->oDb);
|
||||||
@@ -65,116 +62,114 @@ class Livetrail extends Main
|
|||||||
$this->oMap = new Map($this->oDb);
|
$this->oMap = new Map($this->oDb);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function install()
|
protected function install() {
|
||||||
{
|
|
||||||
//Install DB
|
//Install DB
|
||||||
$this->oDb->install();
|
$this->oDb->install();
|
||||||
|
|
||||||
//Add first user
|
//Add first user
|
||||||
$iUserId = $this->oDb->insertRow(User::USER_TABLE, array(
|
$iUserId = $this->oDb->insertRow(User::USER_TABLE, [
|
||||||
'name' => 'Admin',
|
'name' => 'Admin',
|
||||||
'email' => 'admin@admin.com',
|
'email' => 'admin@admin.com',
|
||||||
'language' => self::DEFAULT_LANG,
|
'language' => self::DEFAULT_LANG,
|
||||||
'timezone' => date_default_timezone_get(),
|
'timezone' => date_default_timezone_get(),
|
||||||
'subscribed'=> User::USER_SUBSCRIBED,
|
'subscribed'=> User::USER_SUBSCRIBED,
|
||||||
'clearance' => User::CLEARANCE_ADMIN
|
'clearance' => User::CLEARANCE_ADMIN
|
||||||
));
|
]);
|
||||||
$this->oUser->setUserId($iUserId);
|
$this->oUser->setUserId($iUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getSqlOptions()
|
protected function getSqlOptions() {
|
||||||
{
|
return
|
||||||
return array
|
[
|
||||||
(
|
'tables' =>
|
||||||
'tables' => array
|
[
|
||||||
(
|
Feed::MSG_TABLE => ['ref_msg_id', Db::getId(Feed::FEED_TABLE), 'type', 'latitude', 'longitude', 'iso_time', 'site_time', 'timezone', 'unix_time', 'content', 'battery_state', 'posted_on', 'weather_icon', 'weather_cond', 'weather_temp', 'display'],
|
||||||
Feed::MSG_TABLE => array('ref_msg_id', Db::getId(Feed::FEED_TABLE), 'type', 'latitude', 'longitude', 'iso_time', 'site_time', 'timezone', 'unix_time', 'content', 'battery_state', 'posted_on', 'weather_icon', 'weather_cond', 'weather_temp', 'display'),
|
Feed::FEED_TABLE => ['ref_feed_id', Db::getId(Feed::SPOT_TABLE), Db::getId(Project::PROJ_TABLE), 'name', 'description', 'status', 'last_update'],
|
||||||
Feed::FEED_TABLE => array('ref_feed_id', Db::getId(Feed::SPOT_TABLE), Db::getId(Project::PROJ_TABLE), 'name', 'description', 'status', 'last_update'),
|
Feed::SPOT_TABLE => ['ref_spot_id', 'name', 'model'],
|
||||||
Feed::SPOT_TABLE => array('ref_spot_id', 'name', 'model'),
|
Project::PROJ_TABLE => ['name', 'codename', 'active_from', 'active_to'],
|
||||||
Project::PROJ_TABLE => array('name', 'codename', 'active_from', 'active_to'),
|
self::POST_TABLE => [Db::getId(Project::PROJ_TABLE), Db::getId(User::USER_TABLE), 'name', 'content', 'site_time', 'timezone'],
|
||||||
self::POST_TABLE => array(Db::getId(Project::PROJ_TABLE), Db::getId(User::USER_TABLE), 'name', 'content', 'site_time', 'timezone'),
|
Media::MEDIA_TABLE => [Db::getId(Project::PROJ_TABLE), 'filename', 'type', 'taken_on', 'posted_on', 'timezone', 'latitude', 'longitude', 'altitude', 'width', 'height', 'rotate', 'comment'],
|
||||||
Media::MEDIA_TABLE => array(Db::getId(Project::PROJ_TABLE), 'filename', 'type', 'taken_on', 'posted_on', 'timezone', 'latitude', 'longitude', 'altitude', 'width', 'height', 'rotate', 'comment'),
|
User::USER_TABLE => ['name', 'email', 'password', 'token', 'token_exp', 'gravatar', 'language', 'timezone', 'subscribed', 'clearance'],
|
||||||
User::USER_TABLE => array('name', 'email', 'password', 'token', 'token_exp', 'gravatar', 'language', 'timezone', 'subscribed', 'clearance'),
|
Map::MAP_TABLE => ['codename', 'pattern', 'token', 'tile_size', 'min_zoom', 'max_zoom', 'attribution'],
|
||||||
Map::MAP_TABLE => array('codename', 'pattern', 'token', 'tile_size', 'min_zoom', 'max_zoom', 'attribution'),
|
Map::MAPPING_TABLE => [Db::getId(Map::MAP_TABLE) , Db::getId(Project::PROJ_TABLE)]
|
||||||
Map::MAPPING_TABLE => array(Db::getId(Map::MAP_TABLE) , Db::getId(Project::PROJ_TABLE))
|
],
|
||||||
),
|
'types' =>
|
||||||
'types' => array
|
[
|
||||||
(
|
'clearance' => 'TINYINT(1) DEFAULT '.User::CLEARANCE_USER,
|
||||||
'clearance' => "TINYINT(1) DEFAULT ".User::CLEARANCE_USER,
|
'active_from' => 'TIMESTAMP DEFAULT 0',
|
||||||
'active_from' => "TIMESTAMP DEFAULT 0",
|
'active_to' => 'TIMESTAMP DEFAULT 0',
|
||||||
'active_to' => "TIMESTAMP DEFAULT 0",
|
'battery_state' => 'VARCHAR(10)',
|
||||||
'battery_state' => "VARCHAR(10)",
|
'codename' => 'VARCHAR(100)',
|
||||||
'codename' => "VARCHAR(100)",
|
'content' => 'LONGTEXT',
|
||||||
'content' => "LONGTEXT",
|
'comment' => 'LONGTEXT',
|
||||||
'comment' => "LONGTEXT",
|
'description' => 'VARCHAR(100)',
|
||||||
'description' => "VARCHAR(100)",
|
'email' => 'VARCHAR(320) NOT NULL',
|
||||||
'email' => "VARCHAR(320) NOT NULL",
|
'filename' => 'VARCHAR(100) NOT NULL',
|
||||||
'filename' => "VARCHAR(100) NOT NULL",
|
'iso_time' => 'VARCHAR(24)',
|
||||||
'iso_time' => "VARCHAR(24)",
|
'language' => 'VARCHAR(2)',
|
||||||
'language' => "VARCHAR(2)",
|
'last_update' => 'TIMESTAMP DEFAULT 0',
|
||||||
'last_update' => "TIMESTAMP DEFAULT 0",
|
'latitude' => 'DECIMAL(8,6)',
|
||||||
'latitude' => "DECIMAL(8,6)",
|
'longitude' => 'DECIMAL(9,6)',
|
||||||
'longitude' => "DECIMAL(9,6)",
|
'altitude' => 'SMALLINT',
|
||||||
'altitude' => "SMALLINT",
|
'model' => 'VARCHAR(20)',
|
||||||
'model' => "VARCHAR(20)",
|
'name' => 'VARCHAR(100)',
|
||||||
'name' => "VARCHAR(100)",
|
'pattern' => 'VARCHAR(200) NOT NULL',
|
||||||
'pattern' => "VARCHAR(200) NOT NULL",
|
|
||||||
'password' => "VARCHAR(255) NOT NULL DEFAULT ''",
|
'password' => "VARCHAR(255) NOT NULL DEFAULT ''",
|
||||||
'posted_on' => "TIMESTAMP DEFAULT 0",
|
'posted_on' => 'TIMESTAMP DEFAULT 0',
|
||||||
'ref_feed_id' => "VARCHAR(40)",
|
'ref_feed_id' => 'VARCHAR(40)',
|
||||||
'ref_msg_id' => "VARCHAR(15)",
|
'ref_msg_id' => 'VARCHAR(15)',
|
||||||
'ref_spot_id' => "VARCHAR(10)",
|
'ref_spot_id' => 'VARCHAR(10)',
|
||||||
'rotate' => "SMALLINT",
|
'rotate' => 'SMALLINT',
|
||||||
'site_time' => "TIMESTAMP DEFAULT 0", //DEFAULT 0 removes auto-set to current time
|
'site_time' => 'TIMESTAMP DEFAULT 0', //DEFAULT 0 removes auto-set to current time
|
||||||
'status' => "VARCHAR(10)",
|
'status' => 'VARCHAR(10)',
|
||||||
'subscribed' => "BOOLEAN DEFAULT ".User::USER_UNSUBSCRIBED,
|
'subscribed' => 'BOOLEAN DEFAULT '.User::USER_UNSUBSCRIBED,
|
||||||
'taken_on' => "TIMESTAMP DEFAULT 0",
|
'taken_on' => 'TIMESTAMP DEFAULT 0',
|
||||||
'timezone' => "CHAR(64) NOT NULL", //see mysql.time_zone_name
|
'timezone' => 'CHAR(64) NOT NULL', //see mysql.time_zone_name
|
||||||
'token' => "VARCHAR(4096)",
|
'token' => 'VARCHAR(4096)',
|
||||||
'token_exp' => "TIMESTAMP DEFAULT 0",
|
'token_exp' => 'TIMESTAMP DEFAULT 0',
|
||||||
'type' => "VARCHAR(20)",
|
'type' => 'VARCHAR(20)',
|
||||||
'unix_time' => "INT",
|
'unix_time' => 'INT',
|
||||||
'min_zoom' => "TINYINT UNSIGNED",
|
'min_zoom' => 'TINYINT UNSIGNED',
|
||||||
'max_zoom' => "TINYINT UNSIGNED",
|
'max_zoom' => 'TINYINT UNSIGNED',
|
||||||
'attribution' => "VARCHAR(100)",
|
'attribution' => 'VARCHAR(100)',
|
||||||
'gravatar' => "LONGTEXT",
|
'gravatar' => 'LONGTEXT',
|
||||||
'weather_icon' => "VARCHAR(30)",
|
'weather_icon' => 'VARCHAR(30)',
|
||||||
'weather_cond' => "VARCHAR(30)",
|
'weather_cond' => 'VARCHAR(30)',
|
||||||
'weather_temp' => "DECIMAL(3,1)",
|
'weather_temp' => 'DECIMAL(3,1)',
|
||||||
'tile_size' => "SMALLINT UNSIGNED DEFAULT 256",
|
'tile_size' => 'SMALLINT UNSIGNED DEFAULT 256',
|
||||||
'width' => "INT",
|
'width' => 'INT',
|
||||||
'height' => "INT",
|
'height' => 'INT',
|
||||||
'display' => "BOOLEAN DEFAULT ".Feed::MSG_DISPLAYED
|
'display' => 'BOOLEAN DEFAULT '.Feed::MSG_DISPLAYED
|
||||||
),
|
],
|
||||||
'constraints' => array
|
'constraints' =>
|
||||||
(
|
[
|
||||||
Feed::MSG_TABLE => array("UNIQUE KEY `uni_ref_msg_id` (`ref_msg_id`)", "INDEX(`ref_msg_id`)"),
|
Feed::MSG_TABLE => ['UNIQUE KEY `uni_ref_msg_id` (`ref_msg_id`)', 'INDEX(`ref_msg_id`)'],
|
||||||
Feed::FEED_TABLE => array("UNIQUE KEY `uni_ref_feed_id` (`ref_feed_id`)", "INDEX(`ref_feed_id`)"),
|
Feed::FEED_TABLE => ['UNIQUE KEY `uni_ref_feed_id` (`ref_feed_id`)', 'INDEX(`ref_feed_id`)'],
|
||||||
Feed::SPOT_TABLE => array("UNIQUE KEY `uni_ref_spot_id` (`ref_spot_id`)", "INDEX(`ref_spot_id`)"),
|
Feed::SPOT_TABLE => ['UNIQUE KEY `uni_ref_spot_id` (`ref_spot_id`)', 'INDEX(`ref_spot_id`)'],
|
||||||
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)"
|
Map::MAPPING_TABLE => 'default_on_generic_map_only CHECK (`default_map` = 0 OR `id_project` IS NULL)'
|
||||||
),
|
],
|
||||||
'cascading_delete' => array
|
'cascading_delete' =>
|
||||||
(
|
[
|
||||||
Feed::SPOT_TABLE => array(Feed::FEED_TABLE),
|
Feed::SPOT_TABLE => [Feed::FEED_TABLE],
|
||||||
Feed::FEED_TABLE => array(Feed::MSG_TABLE),
|
Feed::FEED_TABLE => [Feed::MSG_TABLE],
|
||||||
Project::PROJ_TABLE => array(Feed::FEED_TABLE, Media::MEDIA_TABLE, self::POST_TABLE, Map::MAPPING_TABLE),
|
Project::PROJ_TABLE => [Feed::FEED_TABLE, Media::MEDIA_TABLE, self::POST_TABLE, Map::MAPPING_TABLE],
|
||||||
Map::MAP_TABLE => array(Map::MAPPING_TABLE)
|
Map::MAP_TABLE => [Map::MAPPING_TABLE]
|
||||||
)
|
]
|
||||||
);
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAppMainPage(string $sCsrfToken='') {
|
public function getAppMainPage(string $sCsrfToken='') {
|
||||||
$asViteAssets = $this->getViteAssets();
|
$asViteAssets = $this->getViteAssets();
|
||||||
|
|
||||||
return parent::getMainPage(
|
return parent::getMainPage(
|
||||||
array(
|
[
|
||||||
'projects' => $this->oProject->getProjects(),
|
'projects' => $this->oProject->getProjects(),
|
||||||
'user' => $this->oUser->getUserInfo(),
|
'user' => $this->oUser->getUserInfo(),
|
||||||
'consts' => array(
|
'consts' => [
|
||||||
'modes' => Project::MODES,
|
'modes' => Project::MODES,
|
||||||
'clearances' => User::CLEARANCES,
|
'clearances' => User::CLEARANCES,
|
||||||
'default_timezone' => Settings::TIMEZONE,
|
'default_timezone' => Settings::TIMEZONE,
|
||||||
@@ -184,10 +179,10 @@ class Livetrail extends Main
|
|||||||
'title' => self::PROJECT_NAME,
|
'title' => self::PROJECT_NAME,
|
||||||
'default_page' => 'project',
|
'default_page' => 'project',
|
||||||
'csrf_token' => $sCsrfToken
|
'csrf_token' => $sCsrfToken
|
||||||
)
|
]
|
||||||
),
|
],
|
||||||
self::MAIN_PAGE,
|
self::MAIN_PAGE,
|
||||||
array(
|
[
|
||||||
'tags' => [
|
'tags' => [
|
||||||
'language' => $this->oLang->getLanguage(),
|
'language' => $this->oLang->getLanguage(),
|
||||||
'title' => self::PROJECT_NAME,
|
'title' => self::PROJECT_NAME,
|
||||||
@@ -197,7 +192,7 @@ class Livetrail extends Main
|
|||||||
'css' => $asViteAssets['css'],
|
'css' => $asViteAssets['css'],
|
||||||
'module' => $asViteAssets['module']
|
'module' => $asViteAssets['module']
|
||||||
]
|
]
|
||||||
)
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,31 +201,31 @@ class Livetrail extends Main
|
|||||||
$asAppImport = $asManifest[self::VITE_APP];
|
$asAppImport = $asManifest[self::VITE_APP];
|
||||||
|
|
||||||
//Recursive search for chunk imports
|
//Recursive search for chunk imports
|
||||||
$asImports = array();
|
$asImports = [];
|
||||||
$asSeenImports = array(self::VITE_APP => true);
|
$asSeenImports = [self::VITE_APP => true];
|
||||||
$this->appendViteImportedChunks($asManifest, $asAppImport, $asSeenImports, $asImports);
|
$this->appendViteImportedChunks($asManifest, $asAppImport, $asSeenImports, $asImports);
|
||||||
|
|
||||||
//CSS
|
//CSS
|
||||||
$asCssFiles = array();
|
$asCssFiles = [];
|
||||||
foreach(array_merge(array($asAppImport), $asImports) as $asChunk) {
|
foreach(array_merge([$asAppImport], $asImports) as $asChunk) {
|
||||||
foreach($asChunk['css'] ?? array() as $sCssFile) $asCssFiles[] = $sCssFile;
|
foreach($asChunk['css'] ?? [] as $sCssFile) $asCssFiles[] = $sCssFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Modules
|
//Modules
|
||||||
$asModuleFiles = array();
|
$asModuleFiles = [];
|
||||||
foreach($asImports as $asImport) {
|
foreach($asImports as $asImport) {
|
||||||
if(str_ends_with($asImport['file'] ?? '', '.js')) $asModuleFiles[] = $asImport['file'];
|
if(str_ends_with($asImport['file'] ?? '', '.js')) $asModuleFiles[] = $asImport['file'];
|
||||||
}
|
}
|
||||||
|
|
||||||
return array(
|
return [
|
||||||
'app' => $asAppImport['file'],
|
'app' => $asAppImport['file'],
|
||||||
'css' => $this->getViteAssetInstances($asCssFiles),
|
'css' => $this->getViteAssetInstances($asCssFiles),
|
||||||
'module' => $this->getViteAssetInstances($asModuleFiles)
|
'module' => $this->getViteAssetInstances($asModuleFiles)
|
||||||
);
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function appendViteImportedChunks($asManifest, $asChunk, &$asSeenImports, &$asImports) {
|
private function appendViteImportedChunks($asManifest, $asChunk, &$asSeenImports, &$asImports) {
|
||||||
foreach($asChunk['imports'] ?? array() as $sImport) {
|
foreach($asChunk['imports'] ?? [] as $sImport) {
|
||||||
if(isset($asSeenImports[$sImport]) || !isset($asManifest[$sImport])) continue;
|
if(isset($asSeenImports[$sImport]) || !isset($asManifest[$sImport])) continue;
|
||||||
|
|
||||||
$asSeenImports[$sImport] = true;
|
$asSeenImports[$sImport] = true;
|
||||||
@@ -241,7 +236,7 @@ class Livetrail extends Main
|
|||||||
|
|
||||||
private function getViteAssetInstances($asFilePaths) {
|
private function getViteAssetInstances($asFilePaths) {
|
||||||
return array_map(
|
return array_map(
|
||||||
function($sFilePath) { return array('filename' => $sFilePath); },
|
function($sFilePath) { return ['filename' => $sFilePath]; },
|
||||||
$asFilePaths
|
$asFilePaths
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -259,7 +254,7 @@ class Livetrail extends Main
|
|||||||
public function updateProject() {
|
public function updateProject() {
|
||||||
$bNewMsg = false;
|
$bNewMsg = false;
|
||||||
$bSuccess = true;
|
$bSuccess = true;
|
||||||
$sDesc = '';
|
$sLangId = '';
|
||||||
|
|
||||||
//Update all feeds belonging to the project
|
//Update all feeds belonging to the project
|
||||||
$asFeeds = $this->oProject->getFeedIds();
|
$asFeeds = $this->oProject->getFeedIds();
|
||||||
@@ -271,11 +266,11 @@ class Livetrail extends Main
|
|||||||
//Send Update Email
|
//Send Update Email
|
||||||
if($bNewMsg) {
|
if($bNewMsg) {
|
||||||
$bSuccess = $this->sendEmail();
|
$bSuccess = $this->sendEmail();
|
||||||
$sDesc = $bSuccess?'mail_sent':'mail_failure';
|
$sLangId = $bSuccess?'email.sent':'email.failure';
|
||||||
}
|
}
|
||||||
else $sDesc = 'no_new_msg';
|
else $sLangId = 'spot.no_new_msg';
|
||||||
|
|
||||||
return self::getJsonResult($bSuccess, $sDesc);
|
return self::getJsonResult($bSuccess, $sLangId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function sendEmail() {
|
private function sendEmail() {
|
||||||
@@ -283,7 +278,7 @@ class Livetrail extends Main
|
|||||||
$oEmail->setDestInfo($this->oUser->getSubscribedUsersInfo());
|
$oEmail->setDestInfo($this->oUser->getSubscribedUsersInfo());
|
||||||
|
|
||||||
//Add Position
|
//Add Position
|
||||||
$asSpotMessages = $this->getSpotMessages(array($this->oProject->getLastMessageId($this->getFeedConstraints(Feed::MSG_TABLE))));
|
$asSpotMessages = $this->getSpotMessages([$this->oProject->getLastMessageId($this->getFeedConstraints(Feed::MSG_TABLE))]);
|
||||||
$asLastMessage = array_shift($asSpotMessages);
|
$asLastMessage = array_shift($asSpotMessages);
|
||||||
$oEmail->oTemplate->setTags($asLastMessage);
|
$oEmail->oTemplate->setTags($asLastMessage);
|
||||||
$oEmail->oTemplate->setTag('date_time', 'time:'.$asLastMessage['unix_time'], 'd/m/Y, H:i');
|
$oEmail->oTemplate->setTag('date_time', 'time:'.$asLastMessage['unix_time'], 'd/m/Y, H:i');
|
||||||
@@ -294,11 +289,11 @@ class Livetrail extends Main
|
|||||||
foreach($asNews as $asPost) {
|
foreach($asNews as $asPost) {
|
||||||
if($asPost['type'] != 'message') {
|
if($asPost['type'] != 'message') {
|
||||||
$oEmail->oTemplate->newInstance('news');
|
$oEmail->oTemplate->newInstance('news');
|
||||||
$oEmail->oTemplate->setInstanceTags('news', array(
|
$oEmail->oTemplate->setInstanceTags('news', [
|
||||||
'local_server' => $this->asContext['serv_name'],
|
'local_server' => $this->asContext['serv_name'],
|
||||||
'project' => $this->oProject->getProjectCodeName(),
|
'project' => $this->oProject->getProjectCodeName(),
|
||||||
'type' => $asPost['type'],
|
'type' => $asPost['type'],
|
||||||
'id' => $asPost['id_'.$asPost['type']])
|
'id' => $asPost['id_'.$asPost['type']]]
|
||||||
);
|
);
|
||||||
$oEmail->oTemplate->addInstance($asPost['type'], $asPost);
|
$oEmail->oTemplate->addInstance($asPost['type'], $asPost);
|
||||||
$oEmail->oTemplate->setInstanceTag($asPost['type'], 'local_server', $this->asContext['serv_name']);
|
$oEmail->oTemplate->setInstanceTag($asPost['type'], 'local_server', $this->asContext['serv_name']);
|
||||||
@@ -310,8 +305,7 @@ class Livetrail extends Main
|
|||||||
return $oEmail->send();
|
return $oEmail->send();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getMarkers($asMessageIds=array(), $asMediaIds=array(), $bInternal=false)
|
public function getMarkers($asMessageIds=[], $asMediaIds=[], $bInternal=false) {
|
||||||
{
|
|
||||||
//Get messages
|
//Get messages
|
||||||
$asMessages = $this->getSpotMessages($asMessageIds);
|
$asMessages = $this->getSpotMessages($asMessageIds);
|
||||||
foreach($asMessages as &$asMessage) {
|
foreach($asMessages as &$asMessage) {
|
||||||
@@ -337,8 +331,8 @@ class Livetrail extends Main
|
|||||||
|
|
||||||
//Assign medias to closest message
|
//Assign medias to closest message
|
||||||
if(!empty($asMessages)) {
|
if(!empty($asMessages)) {
|
||||||
usort($asMessages, function($a, $b){return (int) $a['unix_time'] <=> (int) $b['unix_time'];});
|
usort($asMessages, function($a, $b) {return (int) $a['unix_time'] <=> (int) $b['unix_time'];});
|
||||||
usort($asMedias, function($a, $b){return (int) $a['unix_time'] <=> (int) $b['unix_time'];});
|
usort($asMedias, function($a, $b) {return (int) $a['unix_time'] <=> (int) $b['unix_time'];});
|
||||||
|
|
||||||
$iIndex = 0;
|
$iIndex = 0;
|
||||||
$iMaxIndex = count($asMessages) - 1;
|
$iMaxIndex = count($asMessages) - 1;
|
||||||
@@ -359,18 +353,18 @@ class Livetrail extends Main
|
|||||||
|
|
||||||
//Combine markers
|
//Combine markers
|
||||||
$asMarkers = [...$asMessages, ...$asGeoMedias];
|
$asMarkers = [...$asMessages, ...$asGeoMedias];
|
||||||
usort($asMarkers, function($a, $b){return (int) $a['unix_time'] <=> (int) $b['unix_time'];});
|
usort($asMarkers, function($a, $b) {return (int) $a['unix_time'] <=> (int) $b['unix_time'];});
|
||||||
|
|
||||||
$asResult = array(
|
$asResult = [
|
||||||
'markers' => $asMarkers,
|
'markers' => $asMarkers,
|
||||||
'maps' => $this->oMap->getProjectMaps($this->oProject->getProjectId())
|
'maps' => $this->oMap->getProjectMaps($this->oProject->getProjectId())
|
||||||
);
|
];
|
||||||
|
|
||||||
return $bInternal?$asResult:self::getJsonResult(true, '', $asResult);
|
return $bInternal?$asResult:self::getJsonResult(true, '', $asResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLastUpdate() {
|
public function getLastUpdate() {
|
||||||
$asLastUpdate = array();
|
$asLastUpdate = [];
|
||||||
$this->addTimeStamp($asLastUpdate, $this->oProject->getLastUpdate());
|
$this->addTimeStamp($asLastUpdate, $this->oProject->getLastUpdate());
|
||||||
return self::getJsonResult(true, '', $asLastUpdate);
|
return self::getJsonResult(true, '', $asLastUpdate);
|
||||||
}
|
}
|
||||||
@@ -378,13 +372,13 @@ class Livetrail extends Main
|
|||||||
public function login($sEmail, $sPassword, $sNickName) {
|
public function login($sEmail, $sPassword, $sNickName) {
|
||||||
$asResult = $this->oUser->login($sEmail, $sPassword, $this->oLang->getLanguage(), date_default_timezone_get(), $sNickName);
|
$asResult = $this->oUser->login($sEmail, $sPassword, $this->oLang->getLanguage(), date_default_timezone_get(), $sNickName);
|
||||||
|
|
||||||
if($asResult['result'] && $asResult['desc'] == 'subscribe_user') return $this->subscribe();
|
if($asResult['result'] && $asResult['data']['subscribe']) return $this->subscribe();
|
||||||
else return self::getJsonResult($asResult['result'], $asResult['desc'], $this->oUser->getUserInfo());
|
else return self::getJsonResult($asResult['result'], $asResult['desc_lang_id'], $this->oUser->getUserInfo(), $asResult['desc_lang_params']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function logout() {
|
public function logout() {
|
||||||
$asResult = $this->oUser->logout();
|
$asResult = $this->oUser->logout();
|
||||||
return self::getJsonResult($asResult['result'], $asResult['desc'], User::DEFAULT_USER);
|
return self::getJsonResult($asResult['result'], $asResult['desc_lang_id'], User::DEFAULT_USER, $asResult['desc_lang_params']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function subscribe() {
|
public function subscribe() {
|
||||||
@@ -395,38 +389,36 @@ class Livetrail extends Main
|
|||||||
$oConfEmail->setDestInfo($asUserInfo);
|
$oConfEmail->setDestInfo($asUserInfo);
|
||||||
$oConfEmail->send();
|
$oConfEmail->send();
|
||||||
}
|
}
|
||||||
return self::getJsonResult($asResult['result'], $asResult['desc'], $asUserInfo);
|
return self::getJsonResult($asResult['result'], $asResult['desc_lang_id'], $asUserInfo, $asResult['desc_lang_params']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function unsubscribe() {
|
public function unsubscribe() {
|
||||||
$asResult = $this->oUser->setSubscription(false);
|
$asResult = $this->oUser->setSubscription(false);
|
||||||
return self::getJsonResult($asResult['result'], $asResult['desc'], $this->oUser->getUserInfo());
|
return self::getJsonResult($asResult['result'], $asResult['desc_lang_id'], $this->oUser->getUserInfo(), $asResult['desc_lang_params']);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getSpotMessages($asMsgIds=array())
|
private function getSpotMessages($asMsgIds=[]) {
|
||||||
{
|
|
||||||
$asConstraints = $this->getFeedConstraints(Feed::MSG_TABLE);
|
$asConstraints = $this->getFeedConstraints(Feed::MSG_TABLE);
|
||||||
if(!empty($asMsgIds)) {
|
if(!empty($asMsgIds)) {
|
||||||
$asConstraints['constraint'][Db::getId(Feed::MSG_TABLE)] = $asMsgIds;
|
$asConstraints['constraint'][Db::getId(Feed::MSG_TABLE)] = $asMsgIds;
|
||||||
$asConstraints['constOpe'][Db::getId(Feed::MSG_TABLE)] = 'IN';
|
$asConstraints['constOpe'][Db::getId(Feed::MSG_TABLE)] = 'IN';
|
||||||
}
|
}
|
||||||
|
|
||||||
$asCombinedMessages = array();
|
$asCombinedMessages = [];
|
||||||
|
|
||||||
//Get messages from all feeds belonging to the project
|
//Get messages from all feeds belonging to the project
|
||||||
$asFeeds = $this->oProject->getFeedIds();
|
$asFeeds = $this->oProject->getFeedIds();
|
||||||
foreach($asFeeds as $iFeedId) {
|
foreach($asFeeds as $iFeedId) {
|
||||||
$oFeed = new Feed($this->oDb, $iFeedId);
|
$oFeed = new Feed($this->oDb, $iFeedId);
|
||||||
$asMessages = $oFeed->getMessages($asConstraints);
|
$asMessages = $oFeed->getMessages($asConstraints);
|
||||||
foreach($asMessages as $asMessage)
|
foreach($asMessages as $asMessage) {
|
||||||
{
|
|
||||||
$asMessage['latitude'] = floatval($asMessage['latitude']);
|
$asMessage['latitude'] = floatval($asMessage['latitude']);
|
||||||
$asMessage['longitude'] = floatval($asMessage['longitude']);
|
$asMessage['longitude'] = floatval($asMessage['longitude']);
|
||||||
$asMessage['lat_dms'] = self::decToDms($asMessage['latitude'], 'lat');
|
$asMessage['lat_dms'] = self::decToDms($asMessage['latitude'], 'lat');
|
||||||
$asMessage['lon_dms'] = self::decToDms($asMessage['longitude'], 'lon');
|
$asMessage['lon_dms'] = self::decToDms($asMessage['longitude'], 'lon');
|
||||||
$asMessage['displayed_id'] = $asMessage[Db::getId(Feed::MSG_TABLE)];
|
$asMessage['displayed_id'] = $asMessage[Db::getId(Feed::MSG_TABLE)];
|
||||||
$asMessage['static_img_url'] = $this->oMap->getMapUrl('static', array('x'=>$asMessage['longitude'], 'y'=>$asMessage['latitude']));
|
$asMessage['static_img_url'] = $this->oMap->getMapUrl('static', ['x'=>$asMessage['longitude'], 'y'=>$asMessage['latitude']]);
|
||||||
$asMessage['marker_img_url'] = $this->oMap->getMapUrl('static_marker', array('x'=>$asMessage['longitude'], 'y'=>$asMessage['latitude']));
|
$asMessage['marker_img_url'] = $this->oMap->getMapUrl('static_marker', ['x'=>$asMessage['longitude'], 'y'=>$asMessage['latitude']]);
|
||||||
|
|
||||||
$this->addTimeStamp($asMessage, $asMessage['unix_time'], $asMessage['timezone']);
|
$this->addTimeStamp($asMessage, $asMessage['unix_time'], $asMessage['timezone']);
|
||||||
$asCombinedMessages[] = $asMessage;
|
$asCombinedMessages[] = $asMessage;
|
||||||
@@ -443,8 +435,7 @@ class Livetrail extends Main
|
|||||||
* @param String $sTimeRefField Field to calculate relative times: 'taken_on' or 'posted_on'
|
* @param String $sTimeRefField Field to calculate relative times: 'taken_on' or 'posted_on'
|
||||||
* @return Array Medias info
|
* @return Array Medias info
|
||||||
*/
|
*/
|
||||||
private function getMedias($sTimeRefField, $asMediaIds=array(), $bOnlyGeoMedia=false)
|
private function getMedias($sTimeRefField, $asMediaIds=[], $bOnlyGeoMedia=false) {
|
||||||
{
|
|
||||||
//Constraints
|
//Constraints
|
||||||
$asConstraints = $this->getFeedConstraints(Media::MEDIA_TABLE, $sTimeRefField);
|
$asConstraints = $this->getFeedConstraints(Media::MEDIA_TABLE, $sTimeRefField);
|
||||||
if(!empty($asMediaIds)) {
|
if(!empty($asMediaIds)) {
|
||||||
@@ -478,13 +469,12 @@ class Livetrail extends Main
|
|||||||
return $asMedias;
|
return $asMedias;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getPosts($asPostIds=array())
|
private function getPosts($asPostIds=[]) {
|
||||||
{
|
$asInfo = [
|
||||||
$asInfo = array(
|
'select' => [Db::getFullColumnName(self::POST_TABLE, '*'), 'gravatar'],
|
||||||
'select' => array(Db::getFullColumnName(self::POST_TABLE, '*'), 'gravatar'),
|
|
||||||
'from' => self::POST_TABLE,
|
'from' => self::POST_TABLE,
|
||||||
'join' => array(User::USER_TABLE => Db::getId(User::USER_TABLE))
|
'join' => [User::USER_TABLE => Db::getId(User::USER_TABLE)]
|
||||||
);
|
];
|
||||||
$asInfo = array_merge($asInfo, $this->getFeedConstraints(self::POST_TABLE));
|
$asInfo = array_merge($asInfo, $this->getFeedConstraints(self::POST_TABLE));
|
||||||
|
|
||||||
if(!empty($asPostIds)) {
|
if(!empty($asPostIds)) {
|
||||||
@@ -518,35 +508,35 @@ class Livetrail extends Main
|
|||||||
}
|
}
|
||||||
|
|
||||||
private function getFeedConstraints($sType, $sTimeField='site_time', $sReturnFormat='array') {
|
private function getFeedConstraints($sType, $sTimeField='site_time', $sReturnFormat='array') {
|
||||||
$asConsArray = array();
|
$asConsArray = [];
|
||||||
$sConsSql = "";
|
$sConsSql = '';
|
||||||
$asActPeriod = $this->oProject->getActivePeriod();
|
$asActPeriod = $this->oProject->getActivePeriod();
|
||||||
|
|
||||||
//Filter on Project ID
|
//Filter on Project ID
|
||||||
$sConsSql = "WHERE ".Db::getId(Project::PROJ_TABLE)." = ".$this->oProject->getProjectId();
|
$sConsSql = 'WHERE '.Db::getId(Project::PROJ_TABLE).' = '.$this->oProject->getProjectId();
|
||||||
$asConsArray = array(
|
$asConsArray = [
|
||||||
'constraint'=> array(Db::getId(Project::PROJ_TABLE) => $this->oProject->getProjectId()),
|
'constraint'=> [Db::getId(Project::PROJ_TABLE) => $this->oProject->getProjectId()],
|
||||||
'constOpe' => array(Db::getId(Project::PROJ_TABLE) => "=")
|
'constOpe' => [Db::getId(Project::PROJ_TABLE) => '=']
|
||||||
);
|
];
|
||||||
|
|
||||||
//Time Filter
|
//Time Filter
|
||||||
switch($sType) {
|
switch($sType) {
|
||||||
case Feed::MSG_TABLE:
|
case Feed::MSG_TABLE:
|
||||||
$asConsArray['constraint'][$sTimeField] = $asActPeriod;
|
$asConsArray['constraint'][$sTimeField] = $asActPeriod;
|
||||||
$asConsArray['constOpe'][$sTimeField] = "BETWEEN";
|
$asConsArray['constOpe'][$sTimeField] = 'BETWEEN';
|
||||||
$asConsArray['constraint']['display'] = Feed::MSG_DISPLAYED;
|
$asConsArray['constraint']['display'] = Feed::MSG_DISPLAYED;
|
||||||
$asConsArray['constOpe']['display'] = "=";
|
$asConsArray['constOpe']['display'] = '=';
|
||||||
$sConsSql .= " AND ".$sTimeField." BETWEEN '".$asActPeriod['from']."' AND '".$asActPeriod['to']."' AND display = ".Feed::MSG_DISPLAYED;
|
$sConsSql .= ' AND '.$sTimeField." BETWEEN '".$asActPeriod['from']."' AND '".$asActPeriod['to']."' AND display = ".Feed::MSG_DISPLAYED;
|
||||||
break;
|
break;
|
||||||
case Media::MEDIA_TABLE:
|
case Media::MEDIA_TABLE:
|
||||||
$asConsArray['constraint'][$sTimeField] = $asActPeriod['to'];
|
$asConsArray['constraint'][$sTimeField] = $asActPeriod['to'];
|
||||||
$asConsArray['constOpe'][$sTimeField] = "<=";
|
$asConsArray['constOpe'][$sTimeField] = '<=';
|
||||||
$sConsSql .= " AND ".$sTimeField." <= '".$asActPeriod['to']."'";
|
$sConsSql .= ' AND '.$sTimeField." <= '".$asActPeriod['to']."'";
|
||||||
break;
|
break;
|
||||||
case self::POST_TABLE:
|
case self::POST_TABLE:
|
||||||
$asConsArray['constraint'][$sTimeField] = $asActPeriod['to'];
|
$asConsArray['constraint'][$sTimeField] = $asActPeriod['to'];
|
||||||
$asConsArray['constOpe'][$sTimeField] = "<=";
|
$asConsArray['constOpe'][$sTimeField] = '<=';
|
||||||
$sConsSql .= " AND ".$sTimeField." <= '".$asActPeriod['to']."'";
|
$sConsSql .= ' AND '.$sTimeField." <= '".$asActPeriod['to']."'";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,14 +544,14 @@ class Livetrail extends Main
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function getNewFeed($iRefIdFirst) {
|
public function getNewFeed($iRefIdFirst) {
|
||||||
$asResult = array();
|
$asResult = [];
|
||||||
$sDesc = '';
|
$sLangId = '';
|
||||||
|
|
||||||
if($this->oProject->isEditable()) {
|
if($this->oProject->isEditable()) {
|
||||||
$asMessageIds = $asMediaIds = array();
|
$asMessageIds = $asMediaIds = [];
|
||||||
|
|
||||||
//New Feed Items
|
//New Feed Items
|
||||||
$asResult = $this->getFeed($iRefIdFirst, ">", "DESC");
|
$asResult = $this->getFeed($iRefIdFirst, '>', 'DESC');
|
||||||
foreach($asResult['feed'] as $asItem) {
|
foreach($asResult['feed'] as $asItem) {
|
||||||
switch($asItem['type']) {
|
switch($asItem['type']) {
|
||||||
case 'message':
|
case 'message':
|
||||||
@@ -575,26 +565,26 @@ class Livetrail extends Main
|
|||||||
|
|
||||||
//New Markers
|
//New Markers
|
||||||
$asMarkers = $this->getMarkers(
|
$asMarkers = $this->getMarkers(
|
||||||
empty($asMessageIds)?array(0):$asMessageIds,
|
empty($asMessageIds)?[0]:$asMessageIds,
|
||||||
empty($asMediaIds)?array(0):$asMediaIds,
|
empty($asMediaIds)?[0]:$asMediaIds,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
$asResult = array_merge($asResult, $asMarkers);
|
$asResult = array_merge($asResult, $asMarkers);
|
||||||
}
|
}
|
||||||
else $sDesc = 'project.modes.histo';
|
else $sLangId = 'project.modes.histo';
|
||||||
|
|
||||||
return self::getJsonResult(true, $sDesc, $asResult);
|
return self::getJsonResult(true, $sLangId, $asResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getNextFeed($iRefIdLast=0, $bInternal=false) {
|
public function getNextFeed($iRefIdLast=0, $bInternal=false) {
|
||||||
if($this->oProject->getMode() == Project::MODE_HISTO) {
|
if($this->oProject->getMode() == Project::MODE_HISTO) {
|
||||||
$sDirection = ">";
|
$sDirection = '>';
|
||||||
$sSort = "ASC";
|
$sSort = 'ASC';
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$sDirection = "<";
|
$sDirection = '<';
|
||||||
$sSort = "DESC";
|
$sSort = 'DESC';
|
||||||
}
|
}
|
||||||
$asResult = $this->getFeed($iRefIdLast, $sDirection, $sSort);
|
$asResult = $this->getFeed($iRefIdLast, $sDirection, $sSort);
|
||||||
return $bInternal?$asResult['feed']:self::getJsonResult(true, '', $asResult);
|
return $bInternal?$asResult['feed']:self::getJsonResult(true, '', $asResult);
|
||||||
@@ -610,26 +600,26 @@ class Livetrail extends Main
|
|||||||
$sMediaIdField = Db::getId(Media::MEDIA_TABLE);
|
$sMediaIdField = Db::getId(Media::MEDIA_TABLE);
|
||||||
$sPostIdField = Db::getId(self::POST_TABLE);
|
$sPostIdField = Db::getId(self::POST_TABLE);
|
||||||
$sFeedIdField = Db::getId(Feed::FEED_TABLE);
|
$sFeedIdField = Db::getId(Feed::FEED_TABLE);
|
||||||
$sQuery = implode(" ", array(
|
$sQuery = implode(' ', [
|
||||||
"SELECT type, id, ref",
|
'SELECT type, id, ref',
|
||||||
"FROM (",
|
'FROM (',
|
||||||
"SELECT {$sProjectIdField}, {$sMsgIdField} AS id, 'message' AS type, CONCAT(UNIX_TIMESTAMP(site_time), '.0', {$sMsgIdField}) AS ref",
|
"SELECT {$sProjectIdField}, {$sMsgIdField} AS id, 'message' AS type, CONCAT(UNIX_TIMESTAMP(site_time), '.0', {$sMsgIdField}) AS ref",
|
||||||
"FROM ".Feed::MSG_TABLE,
|
'FROM '.Feed::MSG_TABLE,
|
||||||
"INNER JOIN ".Feed::FEED_TABLE." USING({$sFeedIdField})",
|
'INNER JOIN '.Feed::FEED_TABLE." USING({$sFeedIdField})",
|
||||||
$this->getFeedConstraints(Feed::MSG_TABLE, 'site_time', 'sql'),
|
$this->getFeedConstraints(Feed::MSG_TABLE, 'site_time', 'sql'),
|
||||||
"UNION",
|
'UNION',
|
||||||
"SELECT {$sProjectIdField}, {$sMediaIdField} AS id, 'media' AS type, CONCAT(UNIX_TIMESTAMP(posted_on), '.1', {$sMediaIdField}) AS ref",
|
"SELECT {$sProjectIdField}, {$sMediaIdField} AS id, 'media' AS type, CONCAT(UNIX_TIMESTAMP(posted_on), '.1', {$sMediaIdField}) AS ref",
|
||||||
"FROM ".Media::MEDIA_TABLE,
|
'FROM '.Media::MEDIA_TABLE,
|
||||||
$this->getFeedConstraints(Media::MEDIA_TABLE, 'posted_on', 'sql'),
|
$this->getFeedConstraints(Media::MEDIA_TABLE, 'posted_on', 'sql'),
|
||||||
"UNION",
|
'UNION',
|
||||||
"SELECT {$sProjectIdField}, {$sPostIdField} AS id, 'post' AS type, CONCAT(UNIX_TIMESTAMP(site_time), '.2', {$sPostIdField}) AS ref",
|
"SELECT {$sProjectIdField}, {$sPostIdField} AS id, 'post' AS type, CONCAT(UNIX_TIMESTAMP(site_time), '.2', {$sPostIdField}) AS ref",
|
||||||
"FROM ".self::POST_TABLE,
|
'FROM '.self::POST_TABLE,
|
||||||
$this->getFeedConstraints(self::POST_TABLE, 'site_time', 'sql'),
|
$this->getFeedConstraints(self::POST_TABLE, 'site_time', 'sql'),
|
||||||
") AS items",
|
') AS items',
|
||||||
($sRefId !== '0')?("WHERE ref ".$sDirection." ".$sRefId):"",
|
($sRefId !== '0')?('WHERE ref '.$sDirection.' '.$sRefId):'',
|
||||||
"ORDER BY ref ".$sSort,
|
'ORDER BY ref '.$sSort,
|
||||||
"LIMIT ".self::FEED_CHUNK_SIZE
|
'LIMIT '.self::FEED_CHUNK_SIZE
|
||||||
));
|
]);
|
||||||
|
|
||||||
//Get new chunk
|
//Get new chunk
|
||||||
$asItems = $this->oDb->getArrayQuery($sQuery, true);
|
$asItems = $this->oDb->getArrayQuery($sQuery, true);
|
||||||
@@ -642,22 +632,22 @@ class Livetrail extends Main
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Sort Table IDs by type & Get attributes
|
//Sort Table IDs by type & Get attributes
|
||||||
$asFeedIds = array('message'=>array(), 'media'=>array(), 'post'=>array());
|
$asFeedIds = ['message'=>[], 'media'=>[], 'post'=>[]];
|
||||||
foreach($asItems as $asItem) {
|
foreach($asItems as $asItem) {
|
||||||
$asFeedIds[$asItem['type']][$asItem['id']] = $asItem;
|
$asFeedIds[$asItem['type']][$asItem['id']] = $asItem;
|
||||||
}
|
}
|
||||||
$asFeedAttrs = array(
|
$asFeedAttrs = [
|
||||||
'message' => empty($asFeedIds['message'])?array():$this->getSpotMessages(array_keys($asFeedIds['message'])),
|
'message' => empty($asFeedIds['message'])?[]:$this->getSpotMessages(array_keys($asFeedIds['message'])),
|
||||||
'media' => empty($asFeedIds['media'])?array():$this->getMedias('posted_on', array_keys($asFeedIds['media'])),
|
'media' => empty($asFeedIds['media'])?[]:$this->getMedias('posted_on', array_keys($asFeedIds['media'])),
|
||||||
'post' => empty($asFeedIds['post'])?array():$this->getPosts(array_keys($asFeedIds['post']))
|
'post' => empty($asFeedIds['post'])?[]:$this->getPosts(array_keys($asFeedIds['post']))
|
||||||
);
|
];
|
||||||
|
|
||||||
//Replace Array Key with Item ID
|
//Replace Array Key with Item ID
|
||||||
$asFeeds = array();
|
$asFeeds = [];
|
||||||
foreach($asFeedAttrs as $sType=>$asFeedAttr) {
|
foreach($asFeedAttrs as $sType=>$asFeedAttr) {
|
||||||
foreach($asFeedAttr as $asFeed) {
|
foreach($asFeedAttr as $asFeed) {
|
||||||
$asFeeds[$sType][$asFeed['id_'.$sType]] = $asFeed;
|
$asFeeds[$sType][$asFeed['id_'.$sType]] = $asFeed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Assign
|
//Assign
|
||||||
@@ -665,36 +655,35 @@ class Livetrail extends Main
|
|||||||
$asItem = array_merge($asFeeds[$asItem['type']][$asItem['id']], $asItem);
|
$asItem = array_merge($asFeeds[$asItem['type']][$asItem['id']], $asItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
return array('ref_id_last'=>$iRefIdLast, 'ref_id_first'=>$iRefIdFirst, 'sort'=>$sSort, 'feed'=>$asItems);
|
return ['ref_id_last'=>$iRefIdLast, 'ref_id_first'=>$iRefIdFirst, 'sort'=>$sSort, 'feed'=>$asItems];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addPost($sName, $sPost)
|
public function addPost($sName, $sPost) {
|
||||||
{
|
|
||||||
$iPostId = 0;
|
$iPostId = 0;
|
||||||
$sDesc = '';
|
$sLangId = '';
|
||||||
|
|
||||||
if($this->oProject->isEditable()) {
|
if($this->oProject->isEditable()) {
|
||||||
$asData = array(
|
$asData = [
|
||||||
Db::getId(Project::PROJ_TABLE) => $this->oProject->getProjectId(),
|
Db::getId(Project::PROJ_TABLE) => $this->oProject->getProjectId(),
|
||||||
'name' => mb_strtolower(trim($sName)),
|
'name' => mb_strtolower(trim($sName)),
|
||||||
'content' => trim($sPost),
|
'content' => trim($sPost),
|
||||||
'site_time' => date(Db::TIMESTAMP_FORMAT), //Now in Site Time
|
'site_time' => date(Db::TIMESTAMP_FORMAT), //Now in Site Time
|
||||||
'timezone' => date_default_timezone_get() //Site Time Zone
|
'timezone' => date_default_timezone_get() //Site Time Zone
|
||||||
);
|
];
|
||||||
if($this->oUser->getUserId() > 0) $asData[Db::getId(User::USER_TABLE)] = $this->oUser->getUserId();
|
if($this->oUser->getUserId() > 0) $asData[Db::getId(User::USER_TABLE)] = $this->oUser->getUserId();
|
||||||
|
|
||||||
$iPostId = $this->oDb->insertRow(self::POST_TABLE, $asData);
|
$iPostId = $this->oDb->insertRow(self::POST_TABLE, $asData);
|
||||||
|
if($iPostId == 0) $sLangId = 'error.commit_db';
|
||||||
|
|
||||||
$this->oUser->updateNickname($sName);
|
$this->oUser->updateNickname($sName);
|
||||||
}
|
}
|
||||||
else $sDesc = 'project.modes.histo';
|
else $sLangId = 'project.modes.histo';
|
||||||
|
|
||||||
return self::getJsonResult(($iPostId > 0), $sDesc);
|
return self::getJsonResult(($iPostId > 0), $sLangId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function upload()
|
public function upload() {
|
||||||
{
|
$oUploader = new Uploader($this->oMedia);
|
||||||
$oUploader = new Uploader($this->oMedia, $this->oLang);
|
|
||||||
|
|
||||||
return $oUploader->sBody;
|
return $oUploader->sBody;
|
||||||
}
|
}
|
||||||
@@ -702,7 +691,7 @@ class Livetrail extends Main
|
|||||||
public function addComment($iMediaId, $sComment) {
|
public function addComment($iMediaId, $sComment) {
|
||||||
$oMedia = new Media($this->oDb, $this->oProject, $iMediaId);
|
$oMedia = new Media($this->oDb, $this->oProject, $iMediaId);
|
||||||
$asResult = $oMedia->setComment($sComment);
|
$asResult = $oMedia->setComment($sComment);
|
||||||
return self::getJsonResult($asResult['result'], $asResult['desc'], $asResult['data']);
|
return self::getJsonResult($asResult['result'], $asResult['desc_lang_id'], $asResult['data'], $asResult['desc_lang_params']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addPosition($sLat, $sLng, $iTimestamp) {
|
public function addPosition($sLat, $sLng, $iTimestamp) {
|
||||||
@@ -711,21 +700,21 @@ class Livetrail extends Main
|
|||||||
|
|
||||||
if($bSuccess) {
|
if($bSuccess) {
|
||||||
$bSuccess = $this->sendEmail();
|
$bSuccess = $this->sendEmail();
|
||||||
$sDesc = $bSuccess?'mail_sent':'mail_failure';
|
$sLangId = $bSuccess?'email.sent':'email.failure';
|
||||||
}
|
}
|
||||||
else $sDesc = 'error.commit_db';
|
else $sLangId = 'error.commit_db';
|
||||||
|
|
||||||
return self::getJsonResult($bSuccess, $sDesc);
|
return self::getJsonResult($bSuccess, $sLangId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAdminSettings($sType='') {
|
public function getAdminSettings() {
|
||||||
$oFeed = new Feed($this->oDb);
|
$oFeed = new Feed($this->oDb);
|
||||||
$asData = array(
|
$asData = [
|
||||||
'project' => $this->oProject->getProjects(),
|
'project' => $this->oProject->getProjects(),
|
||||||
'feed' => $oFeed->getFeeds(),
|
'feed' => $oFeed->getFeeds(),
|
||||||
'spot' => $oFeed->getSpots(),
|
'spot' => $oFeed->getSpots(),
|
||||||
'user' => $this->oUser->getSubscribedUsersInfo()
|
'user' => $this->oUser->getSubscribedUsersInfo()
|
||||||
);
|
];
|
||||||
|
|
||||||
foreach($asData['project'] as &$asProject) {
|
foreach($asData['project'] as &$asProject) {
|
||||||
$asProject['active_from'] = substr($asProject['active_from'], 0, 10);
|
$asProject['active_from'] = substr($asProject['active_from'], 0, 10);
|
||||||
@@ -737,14 +726,16 @@ class Livetrail extends Main
|
|||||||
|
|
||||||
public function setAdminSettings($sType, $iId, $sField, $sValue) {
|
public function setAdminSettings($sType, $iId, $sField, $sValue) {
|
||||||
$bSuccess = false;
|
$bSuccess = false;
|
||||||
$sDesc = '';
|
$sLangId = '';
|
||||||
$asResult = array();
|
$asLangParams = [];
|
||||||
|
$asResult = [];
|
||||||
|
|
||||||
if($this->oDb->isId($sField) && $sValue <= 0) return self::getJsonResult(false, $this->oLang->getTranslation('error.impossible_value', [$sValue, $sField]));
|
if($this->oDb->isId($sField) && $sValue <= 0) return self::getJsonResult(false, 'error.impossible_value', [], [$sValue, $sField]);
|
||||||
|
|
||||||
switch($sType) {
|
switch($sType) {
|
||||||
case 'project':
|
case 'project':
|
||||||
$oProject = new Project($this->oDb, $iId);
|
$oProject = new Project($this->oDb, $iId);
|
||||||
|
|
||||||
switch($sField) {
|
switch($sField) {
|
||||||
case 'name':
|
case 'name':
|
||||||
$bSuccess = $oProject->setProjectName($sValue);
|
$bSuccess = $oProject->setProjectName($sValue);
|
||||||
@@ -759,8 +750,18 @@ class Livetrail extends Main
|
|||||||
$bSuccess = $oProject->setActivePeriod($sValue.' 23:59:59', 'to');
|
$bSuccess = $oProject->setActivePeriod($sValue.' 23:59:59', 'to');
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
$sDesc = $this->oLang->getTranslation('error.unknown_field', $sField);
|
$sLangId = 'error.unknown_field';
|
||||||
|
$asLangParams = [$sField];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Identify missing GPX file
|
||||||
|
$sProjectCodeName = ($sField == 'codename')?$sValue:$oProject->getProjectCodeName();
|
||||||
|
if(!Converter::hasGpxFile($sProjectCodeName)) {
|
||||||
|
$bSuccess = true;
|
||||||
|
$sLangId = 'error.file_missing';
|
||||||
|
$asLangParams = ['GPX', $sProjectCodeName.Gpx::EXT];
|
||||||
|
}
|
||||||
|
|
||||||
$asResult = $oProject->getProject();
|
$asResult = $oProject->getProject();
|
||||||
$asResult['active_from'] = substr($asResult['active_from'], 0, 10);
|
$asResult['active_from'] = substr($asResult['active_from'], 0, 10);
|
||||||
$asResult['active_to'] = substr($asResult['active_to'], 0, 10);
|
$asResult['active_to'] = substr($asResult['active_to'], 0, 10);
|
||||||
@@ -778,7 +779,8 @@ class Livetrail extends Main
|
|||||||
$bSuccess = $oFeed->setProjectId($sValue);
|
$bSuccess = $oFeed->setProjectId($sValue);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
$sDesc = $this->oLang->getTranslation('error.unknown_field', $sField);
|
$sLangId = 'error.unknown_field';
|
||||||
|
$asLangParams = [$sField];
|
||||||
}
|
}
|
||||||
$asResult = $oFeed->getFeed();
|
$asResult = $oFeed->getFeed();
|
||||||
break;
|
break;
|
||||||
@@ -787,23 +789,25 @@ class Livetrail extends Main
|
|||||||
case 'clearance':
|
case 'clearance':
|
||||||
$asReturnCode = $this->oUser->setUserClearance($iId, $sValue);
|
$asReturnCode = $this->oUser->setUserClearance($iId, $sValue);
|
||||||
$bSuccess = $asReturnCode['result'];
|
$bSuccess = $asReturnCode['result'];
|
||||||
$sDesc = $asReturnCode['desc'];
|
$sLangId = $asReturnCode['desc_lang_id'];
|
||||||
|
$asLangParams = $asReturnCode['desc_lang_params'];
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
$sDesc = $this->oLang->getTranslation('error.unknown_field', $sField);
|
$sLangId = 'error.unknown_field';
|
||||||
|
$asLangParams = [$sField];
|
||||||
}
|
}
|
||||||
$asResult = $this->oUser->getUserById($iId);
|
$asResult = $this->oUser->getUserById($iId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if(!$bSuccess && $sDesc=='') $sDesc = Mask::LANG_PREFIX.'error.commit_db';
|
if(!$bSuccess && $sLangId=='') $sLangId = 'error.commit_db';
|
||||||
|
|
||||||
return self::getJsonResult($bSuccess, $sDesc, array($sType=>array($asResult)));
|
return self::getJsonResult($bSuccess, $sLangId, [$sType=>[$asResult]], $asLangParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createAdminSettings($sType) {
|
public function createAdminSettings($sType) {
|
||||||
$bSuccess = false;
|
$bSuccess = false;
|
||||||
$sDesc = '';
|
$sLangId = '';
|
||||||
$asResult = array();
|
$asResult = [];
|
||||||
|
|
||||||
switch($sType) {
|
switch($sType) {
|
||||||
case 'project':
|
case 'project':
|
||||||
@@ -814,45 +818,50 @@ class Livetrail extends Main
|
|||||||
$oFeed->createFeedId($iNewProjectId);
|
$oFeed->createFeedId($iNewProjectId);
|
||||||
|
|
||||||
$bSuccess = $iNewProjectId > 0;
|
$bSuccess = $iNewProjectId > 0;
|
||||||
$asResult = array(
|
$asResult = [
|
||||||
'project' => array($oProject->getProject()),
|
'project' => [$oProject->getProject()],
|
||||||
'feed' => array($oFeed->getFeed())
|
'feed' => [$oFeed->getFeed()]
|
||||||
);
|
];
|
||||||
break;
|
break;
|
||||||
case 'feed':
|
case 'feed':
|
||||||
$oFeed = new Feed($this->oDb);
|
$oFeed = new Feed($this->oDb);
|
||||||
$iNewFeedId = $oFeed->createFeedId($this->oProject->getProjectId());
|
$iNewFeedId = $oFeed->createFeedId($this->oProject->getProjectId());
|
||||||
$bSuccess = $iNewFeedId > 0;
|
$bSuccess = $iNewFeedId > 0;
|
||||||
$asResult = array(
|
$asResult = [
|
||||||
'feed' => array($oFeed->getFeed())
|
'feed' => [$oFeed->getFeed()]
|
||||||
);
|
];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if(!$bSuccess && $sLangId=='') $sLangId = 'error.commit_db';
|
||||||
|
|
||||||
return self::getJsonResult($bSuccess, $sDesc, $asResult);
|
return self::getJsonResult($bSuccess, $sLangId, $asResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function deleteAdminSettings($sType, $iId) {
|
public function deleteAdminSettings($sType, $iId) {
|
||||||
$bSuccess = false;
|
$bSuccess = false;
|
||||||
$sDesc = '';
|
$sLangId = '';
|
||||||
$asResult = array();
|
$asLangParams = [];
|
||||||
|
$asResult = [];
|
||||||
|
|
||||||
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'];
|
$sLangId = $asResult['project'][0]['desc_lang_id'];
|
||||||
|
$asLangParams = $asResult['project'][0]['desc_lang_params'];
|
||||||
$bSuccess = $asResult['project'][0]['del'];
|
$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 = ['feed' => [$oFeed->delete()]];
|
||||||
$sDesc = $asResult['feed'][0]['desc'];
|
$sLangId = $asResult['feed'][0]['desc_lang_id'];
|
||||||
$bSuccess = $asResult['feed'][0]['del'];
|
$asLangParams = $asResult['feed'][0]['desc_lang_params'];
|
||||||
|
$bSuccess = $asResult['feed'][0]['result'];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if(!$bSuccess && $sLangId=='') $sLangId = 'error.commit_db';
|
||||||
|
|
||||||
return self::getJsonResult($bSuccess, $sDesc, $asResult);
|
return self::getJsonResult($bSuccess, $sLangId, $asResult, $asLangParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function decToDms($dValue, $sType) {
|
public static function decToDms($dValue, $sType) {
|
||||||
@@ -879,8 +888,8 @@ class Livetrail extends Main
|
|||||||
$sDirection;
|
$sDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getNumberWithLeadingZeros($fValue, $iNbLeadingZeros, $iNbDigits){
|
public static function getNumberWithLeadingZeros($fValue, $iNbLeadingZeros, $iNbDigits) {
|
||||||
$sDecimalSeparator = ".";
|
$sDecimalSeparator = '.';
|
||||||
if($iNbDigits > 0) $iNbLeadingZeros += mb_strlen($sDecimalSeparator) + $iNbDigits;
|
if($iNbDigits > 0) $iNbLeadingZeros += mb_strlen($sDecimalSeparator) + $iNbDigits;
|
||||||
$sPattern = '%0'.$iNbLeadingZeros.$sDecimalSeparator.$iNbDigits.'f';
|
$sPattern = '%0'.$iNbLeadingZeros.$sDecimalSeparator.$iNbDigits.'f';
|
||||||
return sprintf($sPattern, $fValue);
|
return sprintf($sPattern, $fValue);
|
||||||
@@ -894,7 +903,7 @@ class Livetrail extends Main
|
|||||||
|
|
||||||
$sDate = $oDate->format('d/m/Y');
|
$sDate = $oDate->format('d/m/Y');
|
||||||
$sTime = $oDate->format('H:i');
|
$sTime = $oDate->format('H:i');
|
||||||
return $this->oLang->getTranslation('time.date_time', array($sDate, $sTime));
|
return $this->oLang->getTranslation('time.date_time', [$sDate, $sTime]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getTimeZoneDayOffset($iTime, $sLocalTimeZone) {
|
public static function getTimeZoneDayOffset($iTime, $sLocalTimeZone) {
|
||||||
|
|||||||
+10
-11
@@ -3,12 +3,11 @@
|
|||||||
namespace Franzz\Livetrail;
|
namespace Franzz\Livetrail;
|
||||||
use Franzz\Objects\PhpObject;
|
use Franzz\Objects\PhpObject;
|
||||||
use Franzz\Objects\Db;
|
use Franzz\Objects\Db;
|
||||||
use \Settings;
|
|
||||||
|
|
||||||
class Map extends PhpObject {
|
class Map extends PhpObject {
|
||||||
|
|
||||||
const MAP_TABLE = 'maps';
|
public const MAP_TABLE = 'maps';
|
||||||
const MAPPING_TABLE = 'mappings';
|
public const MAPPING_TABLE = 'mappings';
|
||||||
|
|
||||||
private Db $oDb;
|
private Db $oDb;
|
||||||
private $asMaps;
|
private $asMaps;
|
||||||
@@ -16,11 +15,11 @@ class Map 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->asMaps = array();
|
$this->asMaps = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function setMaps() {
|
private function setMaps() {
|
||||||
$asMaps = $this->oDb->selectRows(array('from'=>self::MAP_TABLE));
|
$asMaps = $this->oDb->selectRows(['from'=>self::MAP_TABLE]);
|
||||||
foreach($asMaps as $asMap) $this->asMaps[$asMap['codename']] = $asMap;
|
foreach($asMaps as $asMap) $this->asMaps[$asMap['codename']] = $asMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,15 +30,15 @@ class Map extends PhpObject {
|
|||||||
|
|
||||||
public function getProjectMaps($iProjectId) {
|
public function getProjectMaps($iProjectId) {
|
||||||
$asMappings = $this->oDb->selectRows(
|
$asMappings = $this->oDb->selectRows(
|
||||||
array(
|
[
|
||||||
'select' => array(Db::getId(self::MAP_TABLE), 'default_map'),
|
'select' => [Db::getId(self::MAP_TABLE), 'default_map'],
|
||||||
'from' => self::MAPPING_TABLE,
|
'from' => self::MAPPING_TABLE,
|
||||||
'constraint'=> array("IFNULL(id_project, {$iProjectId})" => $iProjectId)
|
'constraint'=> ["IFNULL(id_project, {$iProjectId})" => $iProjectId]
|
||||||
),
|
],
|
||||||
Db::getId(self::MAP_TABLE)
|
Db::getId(self::MAP_TABLE)
|
||||||
);
|
);
|
||||||
|
|
||||||
$asProjectMaps = array();
|
$asProjectMaps = [];
|
||||||
foreach($this->getMaps() as $asMap) {
|
foreach($this->getMaps() as $asMap) {
|
||||||
if(array_key_exists($asMap['id_map'], $asMappings)) {
|
if(array_key_exists($asMap['id_map'], $asMappings)) {
|
||||||
$asMap['default_map'] = $asMappings[$asMap['id_map']];
|
$asMap['default_map'] = $asMappings[$asMap['id_map']];
|
||||||
@@ -63,4 +62,4 @@ class Map extends PhpObject {
|
|||||||
|
|
||||||
return $sUrl;
|
return $sUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+44
-48
@@ -4,24 +4,22 @@ namespace Franzz\Livetrail;
|
|||||||
use Franzz\Objects\PhpObject;
|
use Franzz\Objects\PhpObject;
|
||||||
use Franzz\Objects\Db;
|
use Franzz\Objects\Db;
|
||||||
use Franzz\Objects\ToolBox;
|
use Franzz\Objects\ToolBox;
|
||||||
use \Settings;
|
|
||||||
|
|
||||||
class Media extends PhpObject {
|
class Media extends PhpObject {
|
||||||
|
|
||||||
//DB Tables
|
//DB Tables
|
||||||
const MEDIA_TABLE = 'medias';
|
public const MEDIA_TABLE = 'medias';
|
||||||
|
|
||||||
//Media folders (works because /public/files is a symlink of /files)
|
//Media folders (works because /public/files is a symlink of /files)
|
||||||
const MEDIA_FOLDER = 'files';
|
public const MEDIA_FOLDER = 'files';
|
||||||
const THUMB_FOLDER = self::MEDIA_FOLDER.'/thumbs';
|
public const THUMB_FOLDER = self::MEDIA_FOLDER.'/thumbs';
|
||||||
|
|
||||||
const THUMB_MAX_WIDTH = 400;
|
private const THUMB_MAX_WIDTH = 400;
|
||||||
|
|
||||||
private Db $oDb;
|
private Db $oDb;
|
||||||
private Project $oProject;
|
private Project $oProject;
|
||||||
private $asMedia;
|
private $asMedia;
|
||||||
private $asMedias;
|
private $asMedias;
|
||||||
//private $sSystemType;
|
|
||||||
|
|
||||||
private $iMediaId;
|
private $iMediaId;
|
||||||
|
|
||||||
@@ -29,9 +27,8 @@ class Media extends PhpObject {
|
|||||||
parent::__construct(__CLASS__);
|
parent::__construct(__CLASS__);
|
||||||
$this->oDb = &$oDb;
|
$this->oDb = &$oDb;
|
||||||
$this->oProject = &$oProject;
|
$this->oProject = &$oProject;
|
||||||
$this->asMedia = array();
|
$this->asMedia = [];
|
||||||
$this->asMedias = array();
|
$this->asMedias = [];
|
||||||
//$this->sSystemType = (substr(php_uname(), 0, 7) == "Windows")?'win':'unix';
|
|
||||||
$this->setMediaId($iMediaId);
|
$this->setMediaId($iMediaId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,16 +45,16 @@ class Media extends PhpObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function setComment($sComment) {
|
public function setComment($sComment) {
|
||||||
$sError = '';
|
$sLangId = '';
|
||||||
$asData = array();
|
$asData = [];
|
||||||
if($this->iMediaId > 0) {
|
if($this->iMediaId > 0) {
|
||||||
$bResult = $this->oDb->updateRow(self::MEDIA_TABLE, $this->iMediaId, array('comment'=>$sComment));
|
$bResult = $this->oDb->updateRow(self::MEDIA_TABLE, $this->iMediaId, ['comment'=>$sComment]);
|
||||||
if(!$bResult) $sError = 'error.commit_db';
|
if(!$bResult) $sLangId = 'error.commit_db';
|
||||||
else $asData = $this->getInfo();
|
else $asData = $this->getInfo();
|
||||||
}
|
}
|
||||||
else $sError = 'media.no_id';
|
else $sLangId = 'media.no_id';
|
||||||
|
|
||||||
return Livetrail::getResult(($sError==''), $sError, $asData);
|
return Livetrail::getResult(($sLangId==''), $sLangId, $asData);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getMediasInfo($oMediaIds=null) {
|
public function getMediasInfo($oMediaIds=null) {
|
||||||
@@ -66,11 +63,11 @@ class Media extends PhpObject {
|
|||||||
|
|
||||||
if($bOwnMedia && empty($this->asMedia) || !$bOwnMedia && empty($this->asMedias) || $bConstraintArray) {
|
if($bOwnMedia && empty($this->asMedia) || !$bOwnMedia && empty($this->asMedias) || $bConstraintArray) {
|
||||||
if($this->oProject->getProjectId()) {
|
if($this->oProject->getProjectId()) {
|
||||||
$asParams = array(
|
$asParams = [
|
||||||
'select' => array(Db::getId(self::MEDIA_TABLE), 'filename', 'taken_on', 'posted_on', 'timezone', 'latitude', 'longitude', 'altitude', 'width', 'height', 'rotate', 'type AS subtype', 'comment'),
|
'select' => [Db::getId(self::MEDIA_TABLE), 'filename', 'taken_on', 'posted_on', 'timezone', 'latitude', 'longitude', 'altitude', 'width', 'height', 'rotate', 'type AS subtype', 'comment'],
|
||||||
'from' => self::MEDIA_TABLE,
|
'from' => self::MEDIA_TABLE,
|
||||||
'constraint'=> array(Db::getId(Project::PROJ_TABLE) => $this->oProject->getProjectId())
|
'constraint'=> [Db::getId(Project::PROJ_TABLE) => $this->oProject->getProjectId()]
|
||||||
);
|
];
|
||||||
if($bOwnMedia) $asParams['constraint'][Db::getId(self::MEDIA_TABLE)] = $oMediaIds;
|
if($bOwnMedia) $asParams['constraint'][Db::getId(self::MEDIA_TABLE)] = $oMediaIds;
|
||||||
if($bConstraintArray) $asParams = array_merge($asParams, $oMediaIds);
|
if($bConstraintArray) $asParams = array_merge($asParams, $oMediaIds);
|
||||||
|
|
||||||
@@ -86,7 +83,7 @@ class Media extends PhpObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $bOwnMedia?$this->asMedia:$asMedias;
|
return $bOwnMedia?$this->asMedia:$asMedias ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getInfo() {
|
public function getInfo() {
|
||||||
@@ -98,14 +95,14 @@ class Media extends PhpObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function addMedia($sMediaName, $sMethod='upload') {
|
public function addMedia($sMediaName, $sMethod='upload') {
|
||||||
$sError = '';
|
$sLangId = '';
|
||||||
$asParams = array();
|
$asParams = [];
|
||||||
if(!$this->isProjectEditable() && $sMethod!='sync') {
|
if(!$this->isProjectEditable() && $sMethod!='sync') {
|
||||||
$sError = 'upload.mode_archived';
|
$sLangId = 'upload.mode_archived';
|
||||||
$asParams[] = $this->oProject->getProjectCodeName();
|
$asParams[] = $this->oProject->getProjectCodeName();
|
||||||
}
|
}
|
||||||
elseif($this->oDb->pingValue(self::MEDIA_TABLE, array('filename'=>$sMediaName)) && $sMethod!='sync') {
|
elseif($this->oDb->pingValue(self::MEDIA_TABLE, ['filename'=>$sMediaName]) && $sMethod!='sync') {
|
||||||
$sError = 'upload.media.exists';
|
$sLangId = 'upload.media.exists';
|
||||||
$asParams[] = $sMediaName;
|
$asParams[] = $sMediaName;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -113,7 +110,7 @@ class Media extends PhpObject {
|
|||||||
|
|
||||||
//Converting times to Site Time Zone, by using date()
|
//Converting times to Site Time Zone, by using date()
|
||||||
//Media Timezone is kept in a separate field for later conversion to Local Time
|
//Media Timezone is kept in a separate field for later conversion to Local Time
|
||||||
$asDbInfo = array(
|
$asDbInfo = [
|
||||||
Db::getId(Project::PROJ_TABLE) => $this->oProject->getProjectId(),
|
Db::getId(Project::PROJ_TABLE) => $this->oProject->getProjectId(),
|
||||||
'filename' => $sMediaName,
|
'filename' => $sMediaName,
|
||||||
'taken_on' => date(Db::TIMESTAMP_FORMAT, ($asMediaInfo['taken_ts'] > 0)?$asMediaInfo['taken_ts']:$asMediaInfo['file_ts']),
|
'taken_on' => date(Db::TIMESTAMP_FORMAT, ($asMediaInfo['taken_ts'] > 0)?$asMediaInfo['taken_ts']:$asMediaInfo['file_ts']),
|
||||||
@@ -126,23 +123,22 @@ class Media extends PhpObject {
|
|||||||
'height' => $asMediaInfo['height'],
|
'height' => $asMediaInfo['height'],
|
||||||
'rotate' => $asMediaInfo['rotate'],
|
'rotate' => $asMediaInfo['rotate'],
|
||||||
'type' => $asMediaInfo['type']
|
'type' => $asMediaInfo['type']
|
||||||
);
|
];
|
||||||
|
|
||||||
if($sMethod=='sync') $iMediaId = $this->oDb->insertUpdateRow(self::MEDIA_TABLE, $asDbInfo, array('filename'));
|
if($sMethod=='sync') $iMediaId = $this->oDb->insertUpdateRow(self::MEDIA_TABLE, $asDbInfo, ['filename']);
|
||||||
else $iMediaId = $this->oDb->insertRow(self::MEDIA_TABLE, $asDbInfo);
|
else $iMediaId = $this->oDb->insertRow(self::MEDIA_TABLE, $asDbInfo);
|
||||||
|
|
||||||
if(!$iMediaId) $sError = 'error.commit_db';
|
if(!$iMediaId) $sLangId = 'error.commit_db';
|
||||||
else {
|
else {
|
||||||
$this->setMediaId($iMediaId);
|
$this->setMediaId($iMediaId);
|
||||||
$asParams = $this->getInfo(); //Creates thumbnail
|
$asParams = $this->getInfo(); //Creates thumbnail
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Livetrail::getResult(($sError==''), $sError, $asParams);
|
return Livetrail::getResult(($sLangId==''), $sLangId, $asParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getMediaInfoFromFile($sMediaName)
|
private function getMediaInfoFromFile($sMediaName) {
|
||||||
{
|
|
||||||
$sMediaPath = self::getMediaPath($sMediaName);
|
$sMediaPath = self::getMediaPath($sMediaName);
|
||||||
$sType = self::getMediaType($sMediaName);
|
$sType = self::getMediaType($sMediaName);
|
||||||
$iPostedOn = filemtime($sMediaPath);
|
$iPostedOn = filemtime($sMediaPath);
|
||||||
@@ -156,8 +152,8 @@ class Media extends PhpObject {
|
|||||||
$iAlt = null;
|
$iAlt = null;
|
||||||
switch($sType) {
|
switch($sType) {
|
||||||
case 'video':
|
case 'video':
|
||||||
$asResult = array();
|
$asResult = [];
|
||||||
$sParams = implode(' ', array(
|
$sParams = implode(' ', [
|
||||||
'-loglevel error', //Remove comments
|
'-loglevel error', //Remove comments
|
||||||
'-select_streams v:0', //First video channel
|
'-select_streams v:0', //First video channel
|
||||||
'-show_entries '. //filter tags : Width, Height, Creation Time, Location & Rotation
|
'-show_entries '. //filter tags : Width, Height, Creation Time, Location & Rotation
|
||||||
@@ -166,7 +162,7 @@ class Media extends PhpObject {
|
|||||||
'stream=width,height',
|
'stream=width,height',
|
||||||
'-print_format json', //output format: json
|
'-print_format json', //output format: json
|
||||||
'-i' //input file
|
'-i' //input file
|
||||||
));
|
]);
|
||||||
exec('ffprobe '.$sParams.' '.escapeshellarg($sMediaPath), $asResult);
|
exec('ffprobe '.$sParams.' '.escapeshellarg($sMediaPath), $asResult);
|
||||||
$asExif = json_decode(implode('', $asResult), true);
|
$asExif = json_decode(implode('', $asResult), true);
|
||||||
|
|
||||||
@@ -191,7 +187,7 @@ class Media extends PhpObject {
|
|||||||
break;
|
break;
|
||||||
case 'image':
|
case 'image':
|
||||||
$asExif = @exif_read_data($sMediaPath, 0, true);
|
$asExif = @exif_read_data($sMediaPath, 0, true);
|
||||||
if($asExif === false) $asExif = array();
|
if($asExif === false) $asExif = [];
|
||||||
list($iWidth, $iHeight) = getimagesize($sMediaPath);
|
list($iWidth, $iHeight) = getimagesize($sMediaPath);
|
||||||
|
|
||||||
//Posted On
|
//Posted On
|
||||||
@@ -220,8 +216,7 @@ class Media extends PhpObject {
|
|||||||
|
|
||||||
//Orientation
|
//Orientation
|
||||||
if(array_key_exists('IFD0', $asExif) && array_key_exists('Orientation', $asExif['IFD0'])) {
|
if(array_key_exists('IFD0', $asExif) && array_key_exists('Orientation', $asExif['IFD0'])) {
|
||||||
switch($asExif['IFD0']['Orientation'])
|
switch($asExif['IFD0']['Orientation']) {
|
||||||
{
|
|
||||||
case 1: $sRotate = '0'; break; //None
|
case 1: $sRotate = '0'; break; //None
|
||||||
case 3: $sRotate = '180'; break; //Flip over
|
case 3: $sRotate = '180'; break; //Flip over
|
||||||
case 6: $sRotate = '90'; break; //Clockwise
|
case 6: $sRotate = '90'; break; //Clockwise
|
||||||
@@ -239,7 +234,7 @@ class Media extends PhpObject {
|
|||||||
$iTakenOn = $oTakenOn->format('U');
|
$iTakenOn = $oTakenOn->format('U');
|
||||||
}
|
}
|
||||||
|
|
||||||
return array(
|
return [
|
||||||
'timezone' => $sTimeZone,
|
'timezone' => $sTimeZone,
|
||||||
'latitude' => $fLat,
|
'latitude' => $fLat,
|
||||||
'longitude' => $fLng,
|
'longitude' => $fLng,
|
||||||
@@ -250,11 +245,10 @@ class Media extends PhpObject {
|
|||||||
'height' => $iHeight,
|
'height' => $iHeight,
|
||||||
'rotate' => $sRotate,
|
'rotate' => $sRotate,
|
||||||
'type' => $sType
|
'type' => $sType
|
||||||
);
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getMediaThumbnail($sMediaName)
|
private function getMediaThumbnail($sMediaName) {
|
||||||
{
|
|
||||||
$sMediaPath = self::getMediaPath($sMediaName);
|
$sMediaPath = self::getMediaPath($sMediaName);
|
||||||
$sThumbPath = self::getMediaPath($sMediaName, 'thumbnail');
|
$sThumbPath = self::getMediaPath($sMediaName, 'thumbnail');
|
||||||
|
|
||||||
@@ -267,22 +261,24 @@ class Media extends PhpObject {
|
|||||||
case 'video':
|
case 'video':
|
||||||
//Get a screenshot of the video 1 second in
|
//Get a screenshot of the video 1 second in
|
||||||
$sTempPath = self::getMediaPath(uniqid('temp_').'.png');
|
$sTempPath = self::getMediaPath(uniqid('temp_').'.png');
|
||||||
$asResult = array();
|
$asResult = [];
|
||||||
$sParams = implode(' ', array(
|
$sParams = implode(' ', [
|
||||||
'-i '.escapeshellarg($sMediaPath), //input file
|
'-i '.escapeshellarg($sMediaPath), //input file
|
||||||
'-ss 00:00:01.000', //Image taken after x seconds
|
'-ss 00:00:01.000', //Image taken after x seconds
|
||||||
'-vframes 1', //number of video frames to output
|
'-vframes 1', //number of video frames to output
|
||||||
escapeshellarg($sTempPath), //output file
|
escapeshellarg($sTempPath), //output file
|
||||||
));
|
]);
|
||||||
exec('ffmpeg '.$sParams, $asResult);
|
exec('ffmpeg '.$sParams, $asResult);
|
||||||
|
|
||||||
//Resize
|
//Resize
|
||||||
$asThumbInfo = ToolBox::createThumbnail($sTempPath, self::THUMB_MAX_WIDTH, 0, $sThumbPath, true);
|
$asThumbInfo = ToolBox::createThumbnail($sTempPath, self::THUMB_MAX_WIDTH, 0, $sThumbPath, true);
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
$asThumbInfo = ['error'=>'error.unknown_type', 'out'=>''];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
else $asThumbInfo = array('error'=>'', 'out'=>$sThumbPath);
|
else $asThumbInfo = ['error'=>'', 'out'=>$sThumbPath];
|
||||||
|
|
||||||
return ($asThumbInfo['error']=='')?$asThumbInfo['out']:$sMediaPath;
|
return ($asThumbInfo['error']=='')?$asThumbInfo['out']:$sMediaPath;
|
||||||
}
|
}
|
||||||
@@ -326,6 +322,6 @@ class Media extends PhpObject {
|
|||||||
|
|
||||||
private static function getLatLngAltFromISO6709($sIso6709) {
|
private static function getLatLngAltFromISO6709($sIso6709) {
|
||||||
preg_match('/^(?P<lat>[\+\-][0,1]?\d{2}\.\d+)(?P<lng>[\+\-][0,1]?\d{2}\.\d+)(?P<alt>[\+\-]\d+)?/', $sIso6709, $asMatches);
|
preg_match('/^(?P<lat>[\+\-][0,1]?\d{2}\.\d+)(?P<lng>[\+\-][0,1]?\d{2}\.\d+)(?P<alt>[\+\-]\d+)?/', $sIso6709, $asMatches);
|
||||||
return array(floatval($asMatches['lat']), floatval($asMatches['lng']), floatval($asMatches['alt'] ?? 0));
|
return [floatval($asMatches['lat']), floatval($asMatches['lng']), floatval($asMatches['alt'] ?? 0)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-42
@@ -3,18 +3,17 @@
|
|||||||
namespace Franzz\Livetrail;
|
namespace Franzz\Livetrail;
|
||||||
use Franzz\Objects\PhpObject;
|
use Franzz\Objects\PhpObject;
|
||||||
use Franzz\Objects\Db;
|
use Franzz\Objects\Db;
|
||||||
use \Settings;
|
|
||||||
|
|
||||||
class Project extends PhpObject {
|
class Project extends PhpObject {
|
||||||
|
|
||||||
//Spot Mode
|
//Spot Mode
|
||||||
const MODE_PREVIZ = 'P';
|
public const MODE_PREVIZ = 'P';
|
||||||
const MODE_BLOG = 'B';
|
public const MODE_BLOG = 'B';
|
||||||
const MODE_HISTO = 'H';
|
public const MODE_HISTO = 'H';
|
||||||
const MODES = array('previz'=>self::MODE_PREVIZ, 'blog'=>self::MODE_BLOG, 'histo'=>self::MODE_HISTO);
|
public const MODES = ['previz'=>self::MODE_PREVIZ, 'blog'=>self::MODE_BLOG, 'histo'=>self::MODE_HISTO];
|
||||||
|
|
||||||
//DB Tables
|
//DB Tables
|
||||||
const PROJ_TABLE = 'projects';
|
public const PROJ_TABLE = 'projects';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database Handle
|
* Database Handle
|
||||||
@@ -22,13 +21,11 @@ class Project extends PhpObject {
|
|||||||
*/
|
*/
|
||||||
private $oDb;
|
private $oDb;
|
||||||
|
|
||||||
|
|
||||||
private $iProjectId;
|
private $iProjectId;
|
||||||
private $sName;
|
private $sName;
|
||||||
private $sCodeName;
|
private $sCodeName;
|
||||||
private $sMode;
|
private $sMode;
|
||||||
private $asActive;
|
private $asActive;
|
||||||
private $asGeo;
|
|
||||||
|
|
||||||
public function __construct(Db &$oDb, $iProjectId=0) {
|
public function __construct(Db &$oDb, $iProjectId=0) {
|
||||||
parent::__construct(__CLASS__);
|
parent::__construct(__CLASS__);
|
||||||
@@ -53,17 +50,17 @@ class Project extends PhpObject {
|
|||||||
* Mode --P--][--------B--------][--P--][-----------B---------------][---P---][-----B-----][---------H----------
|
* Mode --P--][--------B--------][--P--][-----------B---------------][---P---][-----B-----][---------H----------
|
||||||
*/
|
*/
|
||||||
$sQuery =
|
$sQuery =
|
||||||
"SELECT MAX(id_project) ".
|
'SELECT MAX(id_project) '.
|
||||||
"FROM projects ".
|
'FROM projects '.
|
||||||
"WHERE active_to = (".
|
'WHERE active_to = ('.
|
||||||
"SELECT MIN(active_to) ". //Select closest project in the future
|
'SELECT MIN(active_to) '. //Select closest project in the future
|
||||||
"FROM projects ".
|
'FROM projects '.
|
||||||
"WHERE active_to > NOW() ". //Select Next project
|
'WHERE active_to > NOW() '. //Select Next project
|
||||||
"OR active_to = (". //In case there is no next project, select the last one
|
'OR active_to = ('. //In case there is no next project, select the last one
|
||||||
"SELECT MAX(active_to) ".
|
'SELECT MAX(active_to) '.
|
||||||
"FROM projects".
|
'FROM projects'.
|
||||||
")".
|
')'.
|
||||||
")";
|
')';
|
||||||
$asResult = $this->oDb->getArrayQuery($sQuery, true);
|
$asResult = $this->oDb->getArrayQuery($sQuery, true);
|
||||||
$this->iProjectId = array_shift($asResult);
|
$this->iProjectId = array_shift($asResult);
|
||||||
}
|
}
|
||||||
@@ -72,7 +69,7 @@ class Project extends PhpObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function createProjectId() {
|
public function createProjectId() {
|
||||||
$this->setProjectId($this->oDb->insertRow(self::PROJ_TABLE, array('codename'=>'')));
|
$this->setProjectId($this->oDb->insertRow(self::PROJ_TABLE, ['codename'=>'']));
|
||||||
return $this->getProjectId();
|
return $this->getProjectId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,33 +111,33 @@ class Project extends PhpObject {
|
|||||||
return $this->oDb->selectColumn(
|
return $this->oDb->selectColumn(
|
||||||
Feed::FEED_TABLE,
|
Feed::FEED_TABLE,
|
||||||
Db::getId(Feed::FEED_TABLE),
|
Db::getId(Feed::FEED_TABLE),
|
||||||
array(Db::getId(self::PROJ_TABLE) => $this->getProjectId())
|
[Db::getId(self::PROJ_TABLE) => $this->getProjectId()]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getProjects($iProjectId=0) {
|
public function getProjects($iProjectId=0) {
|
||||||
$bSpecificProj = ($iProjectId > 0);
|
$bSpecificProj = ($iProjectId > 0);
|
||||||
$sDefaultProjectCodeName = $this->getProjectCodeName();
|
$sDefaultProjectCodeName = $this->getProjectCodeName();
|
||||||
$asInfo = array(
|
$asInfo = [
|
||||||
'select'=> array(
|
'select'=> [
|
||||||
Db::getId(self::PROJ_TABLE)." AS id",
|
Db::getId(self::PROJ_TABLE).' AS id',
|
||||||
'codename',
|
'codename',
|
||||||
'name',
|
'name',
|
||||||
'latitude',
|
'latitude',
|
||||||
'longitude',
|
'longitude',
|
||||||
'active_from',
|
'active_from',
|
||||||
'active_to',
|
'active_to',
|
||||||
"IF(NOW() BETWEEN active_from AND active_to, 1, IF(NOW() < active_from, 0, 2)) AS mode"
|
'IF(NOW() BETWEEN active_from AND active_to, 1, IF(NOW() < active_from, 0, 2)) AS mode'
|
||||||
),
|
],
|
||||||
'from' => self::PROJ_TABLE,
|
'from' => self::PROJ_TABLE,
|
||||||
'orderBy' => array('active_from' => 'ASC')
|
'orderBy' => ['active_from' => 'ASC']
|
||||||
);
|
];
|
||||||
if($bSpecificProj) $asInfo['constraint'] = array(Db::getId(self::PROJ_TABLE)=>$iProjectId);
|
if($bSpecificProj) $asInfo['constraint'] = [Db::getId(self::PROJ_TABLE)=>$iProjectId];
|
||||||
|
|
||||||
$asProjects = $this->oDb->selectRows($asInfo, 'codename');
|
$asProjects = $this->oDb->selectRows($asInfo, 'codename');
|
||||||
foreach($asProjects as $sCodeName => &$asProject) {
|
foreach($asProjects as $sCodeName => &$asProject) {
|
||||||
//Update Geo JSON
|
//Update geoJSON
|
||||||
if($sCodeName != '' && (!Converter::isGeoJsonValid($sCodeName) || !$asProject['latitude'] && !$asProject['longitude'])) {
|
if(Converter::hasGpxFile($sCodeName) && (!Converter::isGeoJsonValid($sCodeName) || !$asProject['latitude'] && !$asProject['longitude'])) {
|
||||||
$aiCenter = Converter::convertToGeoJson($sCodeName)['center'];
|
$aiCenter = Converter::convertToGeoJson($sCodeName)['center'];
|
||||||
$this->oDb->updateRow(self::PROJ_TABLE, $asProject['id'], ['latitude' => $aiCenter[1], 'longitude' => $aiCenter[0]]);
|
$this->oDb->updateRow(self::PROJ_TABLE, $asProject['id'], ['latitude' => $aiCenter[1], 'longitude' => $aiCenter[0]]);
|
||||||
$asProject['latitude'] = $aiCenter[1];
|
$asProject['latitude'] = $aiCenter[1];
|
||||||
@@ -176,7 +173,7 @@ class Project extends PhpObject {
|
|||||||
return $iLastUpdate;
|
return $iLastUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLastMessageId($asConstraints=array()): int {
|
public function getLastMessageId($asConstraints=[]): int {
|
||||||
$iLastMsg = 0;
|
$iLastMsg = 0;
|
||||||
|
|
||||||
$asFeedIds = $this->getFeedIds();
|
$asFeedIds = $this->getFeedIds();
|
||||||
@@ -194,34 +191,35 @@ class Project extends PhpObject {
|
|||||||
$this->sName = $asProject['name'];
|
$this->sName = $asProject['name'];
|
||||||
$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 = ['from'=>$asProject['active_from'], 'to'=>$asProject['active_to']];
|
||||||
$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');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function updateField($sField, $oValue) {
|
private function updateField($sField, $oValue) {
|
||||||
$bResult = ($this->oDb->updateRow(self::PROJ_TABLE, $this->getProjectId(), array($sField=>$oValue)) > 0);
|
$bResult = ($this->oDb->updateRow(self::PROJ_TABLE, $this->getProjectId(), [$sField=>$oValue]) > 0);
|
||||||
$this->setProjectInfo();
|
$this->setProjectInfo();
|
||||||
|
|
||||||
return $bResult;
|
return $bResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete() {
|
public function delete() {
|
||||||
$asResult = array();
|
$asResult = [];
|
||||||
if($this->getProjectId() > 0) {
|
if($this->getProjectId() > 0) {
|
||||||
$asFeedIds = $this->getFeedIds();
|
$asFeedIds = $this->getFeedIds();
|
||||||
foreach($asFeedIds as $iFeedId) {
|
foreach($asFeedIds as $iFeedId) {
|
||||||
$asResult['feed'][] = (new Feed($this->oDb, $iFeedId))->delete();
|
$asResult['feed'][] = (new Feed($this->oDb, $iFeedId))->delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
$asResult['project'][] = array(
|
$bDeleted = $this->oDb->deleteRow(self::PROJ_TABLE, $this->getProjectId());
|
||||||
|
$asResult['project'][] = [
|
||||||
'id' => $this->getProjectId(),
|
'id' => $this->getProjectId(),
|
||||||
'del' => $this->oDb->deleteRow(self::PROJ_TABLE, $this->getProjectId()),
|
'del' => $bDeleted,
|
||||||
'desc' => $this->oDb->getLastError()
|
'desc_lang_id' => $bDeleted?'':'error.commit_db',
|
||||||
);
|
'desc_lang_params' => []
|
||||||
|
];
|
||||||
}
|
}
|
||||||
else $asResult['project'][] = array('del'=>false, 'desc'=>'Error while setting project: no project ID');
|
else $asResult['project'][] = ['del'=>false, 'desc_lang_id'=>'error.impossible_value', 'desc_lang_params'=>[$this->getProjectId(), 'project ID']];
|
||||||
|
|
||||||
return $asResult;
|
return $asResult;
|
||||||
}
|
}
|
||||||
@@ -230,7 +228,7 @@ class Project extends PhpObject {
|
|||||||
return self::isModeEditable($this->getMode());
|
return self::isModeEditable($this->getMode());
|
||||||
}
|
}
|
||||||
|
|
||||||
static public function isModeEditable($sMode) {
|
public static function isModeEditable($sMode) {
|
||||||
return ($sMode != self::MODE_HISTO);
|
return ($sMode != self::MODE_HISTO);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-17
@@ -2,26 +2,21 @@
|
|||||||
|
|
||||||
namespace Franzz\Livetrail;
|
namespace Franzz\Livetrail;
|
||||||
use Franzz\Objects\UploadHandler;
|
use Franzz\Objects\UploadHandler;
|
||||||
use Franzz\Objects\Translator;
|
|
||||||
|
|
||||||
class Uploader extends UploadHandler
|
class Uploader extends UploadHandler {
|
||||||
{
|
|
||||||
private Media $oMedia;
|
private Media $oMedia;
|
||||||
private Translator $oLang;
|
|
||||||
|
|
||||||
public string $sBody;
|
public string $sBody;
|
||||||
|
|
||||||
function __construct(Media &$oMedia, Translator &$oLang)
|
public function __construct(Media &$oMedia) {
|
||||||
{
|
|
||||||
$this->oMedia = &$oMedia;
|
$this->oMedia = &$oMedia;
|
||||||
$this->oLang = &$oLang;
|
|
||||||
$this->sBody = '';
|
$this->sBody = '';
|
||||||
|
|
||||||
parent::__construct(array(
|
parent::__construct([
|
||||||
'upload_dir' => Media::MEDIA_FOLDER.'/',
|
'upload_dir' => Media::MEDIA_FOLDER.'/',
|
||||||
'image_versions' => array(),
|
'image_versions' => [],
|
||||||
'accept_file_types' => '/\.(gif|jpe?g|png|mov|mp4)$/i'
|
'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) {
|
||||||
@@ -29,7 +24,9 @@ class Uploader extends UploadHandler
|
|||||||
|
|
||||||
//Check project mode
|
//Check project mode
|
||||||
if(!$this->oMedia->isProjectEditable()) {
|
if(!$this->oMedia->isProjectEditable()) {
|
||||||
$file->error = $this->get_error_message('upload.mode_archived', array($this->oMedia->getProjectCodeName()));
|
$file->error = true;
|
||||||
|
$file->desc_lang_id = 'upload.mode_archived';
|
||||||
|
$file->desc_lang_params = [$this->oMedia->getProjectCodeName()];
|
||||||
$bResult = false;
|
$bResult = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,13 +40,22 @@ class Uploader extends UploadHandler
|
|||||||
|
|
||||||
if(empty($file->error)) {
|
if(empty($file->error)) {
|
||||||
$asResult = $this->oMedia->addMedia($file->name);
|
$asResult = $this->oMedia->addMedia($file->name);
|
||||||
if(!$asResult['result']) $file->error = $this->get_error_message($asResult['desc'], $asResult['data']);
|
if(!$asResult['result']) {
|
||||||
|
$file->error = true;
|
||||||
|
$file->desc_lang_id = $asResult['desc_lang_id'];
|
||||||
|
$file->desc_lang_params = $asResult['data'];
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
$file->original_name = basename((string) $name);
|
$file->original_name = basename((string) $name);
|
||||||
$file->id = $this->oMedia->getMediaId();
|
$file->id = $this->oMedia->getMediaId();
|
||||||
$file->thumbnail = $asResult['data']['thumb_path'];
|
$file->thumbnail = $asResult['data']['thumb_path'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(!empty($file->error)) {
|
||||||
|
if(empty($file->desc_lang_id)) $file->desc_lang_id = is_string($file->error)?$file->error:'upload.error';
|
||||||
|
if(empty($file->desc_lang_params)) $file->desc_lang_params = [];
|
||||||
|
$file->error = true;
|
||||||
|
}
|
||||||
|
|
||||||
return $file;
|
return $file;
|
||||||
}
|
}
|
||||||
@@ -58,10 +64,7 @@ class Uploader extends UploadHandler
|
|||||||
$this->sBody .= $sBodyPart;
|
$this->sBody .= $sBodyPart;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function get_error_message($sError, $asParams=array()) {
|
protected function get_error_message($sLangId, $asParams=[]) {
|
||||||
$sTranslatedError = $this->oLang->getTranslation($sError, $asParams);
|
return array_key_exists($sLangId, $this->error_messages)?'upload.error':$sLangId;
|
||||||
if($sTranslatedError) return $sTranslatedError;
|
|
||||||
elseif(array_key_exists($sError, $this->error_messages)) return $this->error_messages[$sError];
|
|
||||||
else return $sError;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+67
-63
@@ -3,28 +3,21 @@
|
|||||||
namespace Franzz\Livetrail;
|
namespace Franzz\Livetrail;
|
||||||
use Franzz\Objects\PhpObject;
|
use Franzz\Objects\PhpObject;
|
||||||
use Franzz\Objects\Db;
|
use Franzz\Objects\Db;
|
||||||
use \Settings;
|
|
||||||
|
|
||||||
class User extends PhpObject {
|
class User extends PhpObject {
|
||||||
|
|
||||||
//DB Tables
|
//DB Tables
|
||||||
const USER_TABLE = 'users';
|
public const USER_TABLE = 'users';
|
||||||
|
|
||||||
//Clearance Levels
|
//Clearance Levels
|
||||||
const CLEARANCE_USER = 0;
|
public const CLEARANCE_USER = 0;
|
||||||
const CLEARANCE_ADMIN = 9;
|
public const CLEARANCE_ADMIN = 9;
|
||||||
const CLEARANCES = array('user'=>self::CLEARANCE_USER, 'admin'=>self::CLEARANCE_ADMIN);
|
public const CLEARANCES = ['user'=>self::CLEARANCE_USER, 'admin'=>self::CLEARANCE_ADMIN];
|
||||||
|
|
||||||
const USER_SUBSCRIBED = 1;
|
public const USER_SUBSCRIBED = 1;
|
||||||
const USER_UNSUBSCRIBED = 0;
|
public const USER_UNSUBSCRIBED = 0;
|
||||||
|
|
||||||
//Session & Cookie
|
public const DEFAULT_USER = [
|
||||||
const SESSION_ID_USER = 'id_user';
|
|
||||||
const SESSION_ADMIN = 'admin_authenticated';
|
|
||||||
const COOKIE_TOKEN = 'login';
|
|
||||||
const COOKIE_DURATION = 60 * 60 * 24 * 365; //1 year
|
|
||||||
|
|
||||||
const DEFAULT_USER = array(
|
|
||||||
'id' => 0,
|
'id' => 0,
|
||||||
'id_user' => 0,
|
'id_user' => 0,
|
||||||
'name' => '',
|
'name' => '',
|
||||||
@@ -33,7 +26,13 @@ class User extends PhpObject {
|
|||||||
'timezone' => '',
|
'timezone' => '',
|
||||||
'subscribed'=> self::USER_UNSUBSCRIBED,
|
'subscribed'=> self::USER_UNSUBSCRIBED,
|
||||||
'clearance' => self::CLEARANCE_USER
|
'clearance' => self::CLEARANCE_USER
|
||||||
);
|
];
|
||||||
|
|
||||||
|
//Session & Cookie
|
||||||
|
private const SESSION_ID_USER = 'id_user';
|
||||||
|
private const SESSION_ADMIN = 'admin_authenticated';
|
||||||
|
private const COOKIE_TOKEN = 'login';
|
||||||
|
private const COOKIE_DURATION = 60 * 60 * 24 * 365; //1 year
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database Handle
|
* Database Handle
|
||||||
@@ -74,22 +73,22 @@ class User extends PhpObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function getUserById($iUserId) {
|
public function getUserById($iUserId) {
|
||||||
$asUsersInfo = array();
|
$asUsersInfo = [];
|
||||||
if($iUserId > 0) $asUsersInfo = $this->getUsersInfo($iUserId);
|
if($iUserId > 0) $asUsersInfo = $this->getUsersInfo($iUserId);
|
||||||
return empty($asUsersInfo)?array():array_shift($asUsersInfo);
|
return empty($asUsersInfo)?[]:array_shift($asUsersInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getUsersInfo($iUserId=-1) {
|
public function getUsersInfo($iUserId=-1) {
|
||||||
|
|
||||||
//Mapping between user fields and DB fields
|
//Mapping between user fields and DB fields
|
||||||
$asSelect = array_keys($this->asUserInfo);
|
$asSelect = array_keys($this->asUserInfo);
|
||||||
$asSelect[array_search('id', $asSelect)] = Db::getId(self::USER_TABLE)." AS id";
|
$asSelect[array_search('id', $asSelect)] = Db::getId(self::USER_TABLE).' AS id';
|
||||||
|
|
||||||
$asInfo = array(
|
$asInfo = [
|
||||||
'select' => $asSelect,
|
'select' => $asSelect,
|
||||||
'from' => self::USER_TABLE
|
'from' => self::USER_TABLE
|
||||||
);
|
];
|
||||||
if($iUserId != -1) $asInfo['constraint'] = array(Db::getId(self::USER_TABLE) => $iUserId);
|
if($iUserId != -1) $asInfo['constraint'] = [Db::getId(self::USER_TABLE) => $iUserId];
|
||||||
|
|
||||||
return $this->oDb->selectRows($asInfo);
|
return $this->oDb->selectRows($asInfo);
|
||||||
}
|
}
|
||||||
@@ -100,14 +99,14 @@ class User extends PhpObject {
|
|||||||
|
|
||||||
private function addUser($sEmail, $sLang, $sTimezone, $sNickName='') {
|
private function addUser($sEmail, $sLang, $sTimezone, $sNickName='') {
|
||||||
$bSuccess = false;
|
$bSuccess = false;
|
||||||
$sDesc = '';
|
$sLangId = '';
|
||||||
|
|
||||||
$iUserId = $this->oDb->insertRow(
|
$iUserId = $this->oDb->insertRow(
|
||||||
self::USER_TABLE,
|
self::USER_TABLE,
|
||||||
array('email'=>$sEmail, 'language'=>$sLang, 'timezone'=>$sTimezone)
|
['email'=>$sEmail, 'language'=>$sLang, 'timezone'=>$sTimezone]
|
||||||
);
|
);
|
||||||
|
|
||||||
if($iUserId == 0) $sDesc = 'lang:error.commit_db';
|
if($iUserId == 0) $sLangId = 'error.commit_db';
|
||||||
else $bSuccess = true;
|
else $bSuccess = true;
|
||||||
|
|
||||||
//Extra optional values
|
//Extra optional values
|
||||||
@@ -116,44 +115,45 @@ class User extends PhpObject {
|
|||||||
$this->updateGravatar($iUserId, $sEmail);
|
$this->updateGravatar($iUserId, $sEmail);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Livetrail::getResult($bSuccess, $sDesc, [Db::getId(self::USER_TABLE) => $iUserId]);
|
return Livetrail::getResult($bSuccess, $sLangId, [Db::getId(self::USER_TABLE) => $iUserId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setSubscription($bSubscribed) {
|
public function setSubscription($bSubscribed) {
|
||||||
if($this->getUserId() > 0) {
|
if($this->getUserId() > 0) {
|
||||||
$iSubscribed = $bSubscribed?1:0;
|
$iSubscribed = $bSubscribed?1:0;
|
||||||
$iUserId = $this->oDb->updateRow(self::USER_TABLE, $this->getUserId(), array('subscribed'=>$iSubscribed));
|
$iUserId = $this->oDb->updateRow(self::USER_TABLE, $this->getUserId(), ['subscribed'=>$iSubscribed]);
|
||||||
if(!$iUserId) return Livetrail::getResult(false, 'lang:error.commit_db');
|
if(!$iUserId) return Livetrail::getResult(false, 'error.commit_db');
|
||||||
$this->asUserInfo['subscribed'] = $iSubscribed;
|
$this->asUserInfo['subscribed'] = $iSubscribed;
|
||||||
return Livetrail::getResult(true, $iSubscribed?'lang:account.subscribed':'lang:account.unsubscribed');
|
return Livetrail::getResult(true, $iSubscribed?'account.subscribed':'account.unsubscribed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSubscribedUsersInfo() {
|
public function getSubscribedUsersInfo() {
|
||||||
$asSelect = array_keys($this->asUserInfo);
|
$asSelect = array_keys($this->asUserInfo);
|
||||||
$asSelect[array_search('id', $asSelect)] = Db::getId(self::USER_TABLE).' AS id';
|
$asSelect[array_search('id', $asSelect)] = Db::getId(self::USER_TABLE).' AS id';
|
||||||
return $this->oDb->selectRows(array(
|
return $this->oDb->selectRows([
|
||||||
'select'=>$asSelect,
|
'select'=>$asSelect,
|
||||||
'from'=>self::USER_TABLE,
|
'from'=>self::USER_TABLE,
|
||||||
'constraint'=>array('subscribed'=>self::USER_SUBSCRIBED)
|
'constraint'=>['subscribed'=>self::USER_SUBSCRIBED]
|
||||||
));
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function login($sEmail, $sPassword, $sLang, $sTimezone, $sNickName='') {
|
public function login($sEmail, $sPassword, $sLang, $sTimezone, $sNickName='') {
|
||||||
$bSuccess = false;
|
$bSuccess = false;
|
||||||
$sDesc = '';
|
$bSubscribe = false;
|
||||||
|
$sLangId = '';
|
||||||
$sEmail = strtolower(trim($sEmail));
|
$sEmail = strtolower(trim($sEmail));
|
||||||
|
|
||||||
//Check email value
|
//Check email value
|
||||||
if(!filter_var($sEmail, FILTER_VALIDATE_EMAIL)) {
|
if(!filter_var($sEmail, FILTER_VALIDATE_EMAIL)) {
|
||||||
$sDesc = 'lang:account.invalid_email';
|
$sLangId = 'account.invalid_email';
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
//Check Email presence in DB
|
//Check Email presence in DB
|
||||||
$asDBUser = $this->oDb->selectRow(
|
$asDBUser = $this->oDb->selectRow(
|
||||||
self::USER_TABLE,
|
self::USER_TABLE,
|
||||||
array('email' => $sEmail),
|
['email' => $sEmail],
|
||||||
array(Db::getId(self::USER_TABLE), 'password', 'clearance')
|
[Db::getId(self::USER_TABLE), 'password', 'clearance']
|
||||||
);
|
);
|
||||||
$iUserId = $asDBUser[Db::getId(self::USER_TABLE)] ?? 0;
|
$iUserId = $asDBUser[Db::getId(self::USER_TABLE)] ?? 0;
|
||||||
|
|
||||||
@@ -162,36 +162,37 @@ class User extends PhpObject {
|
|||||||
//Is Admin
|
//Is Admin
|
||||||
if($asDBUser['clearance'] >= self::CLEARANCE_ADMIN) {
|
if($asDBUser['clearance'] >= self::CLEARANCE_ADMIN) {
|
||||||
//Request a password
|
//Request a password
|
||||||
if($sPassword === '') $sDesc = empty($asDBUser['password'])?'lang:account.set_password':'lang:account.password_required';
|
if($sPassword === '') $sLangId = empty($asDBUser['password'])?'account.set_password':'account.password_required';
|
||||||
|
|
||||||
//Set password
|
//Set password
|
||||||
elseif(empty($asDBUser['password'])) {
|
elseif(empty($asDBUser['password'])) {
|
||||||
if(!$this->oDb->updateRow(self::USER_TABLE, $iUserId, array('password' => password_hash($sPassword, PASSWORD_DEFAULT)))) $sDesc = 'lang:error.commit_db';
|
if(!$this->oDb->updateRow(self::USER_TABLE, $iUserId, ['password' => password_hash($sPassword, PASSWORD_DEFAULT)])) $sLangId = 'error.commit_db';
|
||||||
else {
|
else {
|
||||||
$sDesc = 'lang:account.password_set';
|
$sLangId = 'account.password_set';
|
||||||
$bSuccess = true;
|
$bSuccess = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Check password
|
//Check password
|
||||||
elseif(password_verify($sPassword, $asDBUser['password'])) {
|
elseif(password_verify($sPassword, $asDBUser['password'])) {
|
||||||
$bSuccess = true;
|
$bSuccess = true;
|
||||||
$sDesc = 'lang:account.logged_in';
|
$sLangId = 'account.logged_in';
|
||||||
}
|
}
|
||||||
else $sDesc = 'lang:account.invalid_credentials';
|
else $sLangId = 'account.invalid_credentials';
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$bSuccess = true;
|
$bSuccess = true;
|
||||||
$sDesc = 'lang:account.logged_in';
|
$sLangId = 'account.logged_in';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
//Unknown user, create it
|
//Unknown user, create it
|
||||||
$asAddResult = $this->addUser($sEmail, $sLang, $sTimezone, $sNickName);
|
$asAddResult = $this->addUser($sEmail, $sLang, $sTimezone, $sNickName);
|
||||||
$bSuccess = $asAddResult['result'];
|
$bSuccess = $asAddResult['result'];
|
||||||
$sDesc = $bSuccess?'subscribe_user':$asAddResult['desc'];
|
$bSubscribe = $bSuccess;
|
||||||
|
$sLangId = $bSuccess?'':$asAddResult['desc_lang_id'];
|
||||||
$iUserId = $asAddResult['data'][Db::getId(self::USER_TABLE)] ?? 0;
|
$iUserId = $asAddResult['data'][Db::getId(self::USER_TABLE)] ?? 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if($bSuccess) {
|
if($bSuccess) {
|
||||||
@@ -200,46 +201,49 @@ class User extends PhpObject {
|
|||||||
$this->setTokenCookie();
|
$this->setTokenCookie();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Livetrail::getResult($bSuccess, $sDesc);
|
return Livetrail::getResult($bSuccess, $sLangId, ['subscribe'=>$bSubscribe]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function logout() {
|
public function logout() {
|
||||||
if($this->getUserId() > 0) $this->oDb->updateRow(self::USER_TABLE, $this->getUserId(), array('token' => '', 'token_exp' => '0000-00-00 00:00:00'));
|
if($this->getUserId() > 0) $this->oDb->updateRow(self::USER_TABLE, $this->getUserId(), ['token' => '', 'token_exp' => '0000-00-00 00:00:00']);
|
||||||
$this->clearSession();
|
$this->clearSession();
|
||||||
$this->clearCookie();
|
$this->clearCookie();
|
||||||
$this->setUserId(0);
|
$this->setUserId(0);
|
||||||
return Livetrail::getResult(true, 'lang:account.logged_out');
|
return Livetrail::getResult(true, 'account.logged_out');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updateNickname($sNickname) {
|
public function updateNickname($sNickname) {
|
||||||
if($this->getUserId() > 0 && $sNickname!='') $this->oDb->updateRow(self::USER_TABLE, $this->getUserId(), array('name'=>$sNickname));
|
if($this->getUserId() > 0 && $sNickname!='') $this->oDb->updateRow(self::USER_TABLE, $this->getUserId(), ['name'=>$sNickname]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function updateGravatar($iUserId, $sEmail) {
|
private function updateGravatar($iUserId, $sEmail) {
|
||||||
$sImage = ($sEmail != '')?@file_get_contents('https://www.gravatar.com/avatar/'.md5($sEmail).'.png?d=404&s=24'):'';
|
$sImage = ($sEmail != '')?@file_get_contents('https://www.gravatar.com/avatar/'.md5($sEmail).'.png?d=404&s=24'):'';
|
||||||
$this->oDb->updateRow(self::USER_TABLE, $iUserId, array('gravatar' => base64_encode($sImage)));
|
$this->oDb->updateRow(self::USER_TABLE, $iUserId, ['gravatar' => base64_encode($sImage)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function checkUserClearance($iClearance)
|
public function checkUserClearance($iClearance) {
|
||||||
{
|
|
||||||
return ($this->asUserInfo['clearance'] >= $iClearance);
|
return ($this->asUserInfo['clearance'] >= $iClearance);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setUserClearance($iUserId, $iClearance) {
|
public function setUserClearance($iUserId, $iClearance) {
|
||||||
$bSuccess = false;
|
$bSuccess = false;
|
||||||
$sDesc = '';
|
$sLangId = '';
|
||||||
|
$asLangParams = [];
|
||||||
|
|
||||||
if(!$this->checkUserClearance(self::CLEARANCE_ADMIN)) $sDesc = 'unauthorized';
|
if(!$this->checkUserClearance(self::CLEARANCE_ADMIN)) $sLangId = 'error.no_auth';
|
||||||
else {
|
else {
|
||||||
if(!in_array($iClearance, self::CLEARANCES)) $sDesc = 'Setting wrong clearance "'.$iClearance.'" to user ID "'.$iUserId.'"';
|
if(!in_array($iClearance, self::CLEARANCES)) {
|
||||||
|
$sLangId = 'error.impossible_value';
|
||||||
|
$asLangParams = [$iClearance, 'clearance'];
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
$iUserId = $this->oDb->updateRow(self::USER_TABLE, $iUserId, array('clearance'=>$iClearance));
|
$iUserId = $this->oDb->updateRow(self::USER_TABLE, $iUserId, ['clearance'=>$iClearance]);
|
||||||
if(!$iUserId) $sDesc = 'lang:error.commit_db';
|
if(!$iUserId) $sLangId = 'error.commit_db';
|
||||||
else $bSuccess = true;
|
else $bSuccess = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Livetrail::getResult($bSuccess, $sDesc);
|
return Livetrail::getResult($bSuccess, $sLangId, [], $asLangParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Session */
|
/* Session */
|
||||||
@@ -280,7 +284,7 @@ class User extends PhpObject {
|
|||||||
$asUser = $this->oDb->selectRow(
|
$asUser = $this->oDb->selectRow(
|
||||||
self::USER_TABLE,
|
self::USER_TABLE,
|
||||||
$iUserId,
|
$iUserId,
|
||||||
array('clearance', 'token', 'token_exp')
|
['clearance', 'token', 'token_exp']
|
||||||
);
|
);
|
||||||
|
|
||||||
//Check token value
|
//Check token value
|
||||||
@@ -303,10 +307,10 @@ class User extends PhpObject {
|
|||||||
$this->oDb->updateRow(
|
$this->oDb->updateRow(
|
||||||
self::USER_TABLE,
|
self::USER_TABLE,
|
||||||
$this->getUserId(),
|
$this->getUserId(),
|
||||||
array(
|
[
|
||||||
'token' => hash('sha256', $sCookieValue),
|
'token' => hash('sha256', $sCookieValue),
|
||||||
'token_exp' => date(Db::TIMESTAMP_FORMAT, time() + self::COOKIE_DURATION)
|
'token_exp' => date(Db::TIMESTAMP_FORMAT, time() + self::COOKIE_DURATION)
|
||||||
)
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->setCookie($sCookieValue, time() + self::COOKIE_DURATION);
|
$this->setCookie($sCookieValue, time() + self::COOKIE_DURATION);
|
||||||
@@ -320,13 +324,13 @@ class User extends PhpObject {
|
|||||||
setcookie(
|
setcookie(
|
||||||
self::COOKIE_TOKEN,
|
self::COOKIE_TOKEN,
|
||||||
$sValue,
|
$sValue,
|
||||||
array(
|
[
|
||||||
'expires' => $iExpires,
|
'expires' => $iExpires,
|
||||||
'path' => '/',
|
'path' => '/',
|
||||||
'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https'),
|
'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https'),
|
||||||
'httponly' => true,
|
'httponly' => true,
|
||||||
'samesite' => 'Lax'
|
'samesite' => 'Lax'
|
||||||
)
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+1209
-184
File diff suppressed because it is too large
Load Diff
+8
-3
@@ -1,6 +1,10 @@
|
|||||||
{
|
{
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^10.0.1",
|
||||||
"@vitejs/plugin-vue": "^6.0.8",
|
"@vitejs/plugin-vue": "^6.0.8",
|
||||||
|
"eslint": "^10.9.1",
|
||||||
|
"eslint-plugin-vue": "^10.10.0",
|
||||||
|
"globals": "^17.11.0",
|
||||||
"vite": "^8.1.5"
|
"vite": "^8.1.5"
|
||||||
},
|
},
|
||||||
"name": "livetrail",
|
"name": "livetrail",
|
||||||
@@ -10,7 +14,8 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite build --mode development --watch",
|
"dev": "vite build --mode development --watch",
|
||||||
"prod": "vite build"
|
"prod": "vite build",
|
||||||
|
"lint": "eslint src"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Franzz",
|
"author": "Franzz",
|
||||||
@@ -18,8 +23,8 @@
|
|||||||
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^7.2.0",
|
"@fortawesome/free-solid-svg-icons": "^7.2.0",
|
||||||
"@fortawesome/vue-fontawesome": "^3.2.0",
|
"@fortawesome/vue-fontawesome": "^3.2.0",
|
||||||
"@uppy/core": "^5.2.0",
|
"@uppy/core": "^6.0.0",
|
||||||
"@uppy/xhr-upload": "^5.2.0",
|
"@uppy/xhr-upload": "^6.0.0",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"maplibre-gl": "^6.0.0",
|
"maplibre-gl": "^6.0.0",
|
||||||
"sass": "^1.97.2",
|
"sass": "^1.97.2",
|
||||||
|
|||||||
+1
-1
@@ -4,4 +4,4 @@ require __DIR__.'/../vendor/autoload.php';
|
|||||||
|
|
||||||
use Franzz\Livetrail\Controller;
|
use Franzz\Livetrail\Controller;
|
||||||
|
|
||||||
echo (new Controller())->handle(__FILE__, $argv ?? array());
|
echo (new Controller())->handle(__FILE__, $argv ?? []);
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ Live Trail is a self-hosted web application for sharing journeys on an interacti
|
|||||||
3. Copy timezone data: mariadb-tzinfo-to-sql /usr/share/zoneinfo | mariadb -u root mysql
|
3. Copy timezone data: mariadb-tzinfo-to-sql /usr/share/zoneinfo | mariadb -u root mysql
|
||||||
4. Copy settings-sample.php to settings.php and populate
|
4. Copy settings-sample.php to settings.php and populate
|
||||||
5. Follow CI/CD script in .gitea/workflows/deploy.yml
|
5. Follow CI/CD script in .gitea/workflows/deploy.yml
|
||||||
8. Go to #admin and create a new project, feed & maps
|
6. Add a GPX file named <project_codename>.gpx to /resources/geo/
|
||||||
9. Add a GPX file named <project_codename>.gpx to /resources/geo/
|
7. Go to #admin and create a new project (with code name = <project_codename>) & feed
|
||||||
|
|
||||||
|
|
||||||
## Web Root
|
## Web Root
|
||||||
|
|
||||||
@@ -51,6 +52,13 @@ COMPOSER=composer.dev.json composer update
|
|||||||
|
|
||||||
This makes Composer link `vendor/franzz/objects` to `../objects` and autoload that namespace directly from the local source path. Production continues to use `composer.json`, which installs `franzz/objects` from its Git repository. Commit and publish `objects` changes before updating/deploying a Livetrail version that relies on them.
|
This makes Composer link `vendor/franzz/objects` to `../objects` and autoload that namespace directly from the local source path. Production continues to use `composer.json`, which installs `franzz/objects` from its Git repository. Commit and publish `objects` changes before updating/deploying a Livetrail version that relies on them.
|
||||||
|
|
||||||
|
## Before Committing
|
||||||
|
|
||||||
|
Run manually before pushing:
|
||||||
|
|
||||||
|
* `composer lint` - checks PHP style (php-cs-fixer, dry-run); `composer lint-fix` applies it. Requires the dev dependencies (`COMPOSER=composer.dev.json composer update`, see Local Development above).
|
||||||
|
* `npm run lint` - checks JS/Vue style (ESLint) on demand. It also already runs automatically as part of `npm run dev` (reports issues but never blocks the watcher) and `npm run prod` (aborts the build if any lint error is found).
|
||||||
|
|
||||||
## To Do List
|
## To Do List
|
||||||
|
|
||||||
* Add mail frequency slider
|
* Add mail frequency slider
|
||||||
|
|||||||
+69644
File diff suppressed because it is too large
Load Diff
+16
-2
@@ -55,6 +55,8 @@
|
|||||||
"subject": "Registration confirmed",
|
"subject": "Registration confirmed",
|
||||||
"thanks_subject": "You're all set!"
|
"thanks_subject": "You're all set!"
|
||||||
},
|
},
|
||||||
|
"failure": "Failed to send the update email",
|
||||||
|
"sent": "Update email sent",
|
||||||
"unsubscribe": "PS: Changed your mind?",
|
"unsubscribe": "PS: Changed your mind?",
|
||||||
"unsubscribe_button": "Unsubscribe",
|
"unsubscribe_button": "Unsubscribe",
|
||||||
"update": {
|
"update": {
|
||||||
@@ -66,9 +68,12 @@
|
|||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"commit_db": "Error committing to database",
|
"commit_db": "Error committing to database",
|
||||||
|
"file_missing": "$0 file \"$1\" is missing",
|
||||||
"impossible_value": "Value \"$0\" is not valid for field \"$1\"",
|
"impossible_value": "Value \"$0\" is not valid for field \"$1\"",
|
||||||
"no_auth": "Not authorized",
|
"no_auth": "Not authorized",
|
||||||
"unknown_field": "Unknown field \"$0\""
|
"not_found": "Unknown action",
|
||||||
|
"unknown_field": "Unknown field \"$0\"",
|
||||||
|
"unknown_type": "Unknown type \"$0\""
|
||||||
},
|
},
|
||||||
"feed": {
|
"feed": {
|
||||||
"counter": "#$0",
|
"counter": "#$0",
|
||||||
@@ -140,6 +145,7 @@
|
|||||||
"id": "Spot ID",
|
"id": "Spot ID",
|
||||||
"model": "Model",
|
"model": "Model",
|
||||||
"name": "Spot name",
|
"name": "Spot name",
|
||||||
|
"no_new_msg": "No new messages",
|
||||||
"plural": "Spots",
|
"plural": "Spots",
|
||||||
"ref_id": "Ref. Spot ID"
|
"ref_id": "Ref. Spot ID"
|
||||||
},
|
},
|
||||||
@@ -182,8 +188,16 @@
|
|||||||
},
|
},
|
||||||
"mode_archived": "Project \"$0\" is archived. Uploads are not allowed.",
|
"mode_archived": "Project \"$0\" is archived. Uploads are not allowed.",
|
||||||
"position": {
|
"position": {
|
||||||
|
"determining": "Determining position…",
|
||||||
|
"error": "Unable to determine position",
|
||||||
"new": "New position",
|
"new": "New position",
|
||||||
"title": "Add position"
|
"permission_denied": "Location permission was denied",
|
||||||
|
"sending": "Sending position…",
|
||||||
|
"success": "Position uploaded successfully",
|
||||||
|
"timeout": "Position request timed out",
|
||||||
|
"title": "Add position",
|
||||||
|
"unavailable": "Position is unavailable",
|
||||||
|
"unsupported": "This browser does not support geolocation"
|
||||||
},
|
},
|
||||||
"success": "$0 uploaded successfully"
|
"success": "$0 uploaded successfully"
|
||||||
},
|
},
|
||||||
|
|||||||
+16
-2
@@ -55,6 +55,8 @@
|
|||||||
"subject": "Confirmación",
|
"subject": "Confirmación",
|
||||||
"thanks_subject": "¡Hecho!"
|
"thanks_subject": "¡Hecho!"
|
||||||
},
|
},
|
||||||
|
"failure": "No se pudo enviar el correo de actualización",
|
||||||
|
"sent": "Correo de actualización enviado",
|
||||||
"unsubscribe": "PD: ¿Demasiados correos electrónicos?",
|
"unsubscribe": "PD: ¿Demasiados correos electrónicos?",
|
||||||
"unsubscribe_button": "Darse de baja",
|
"unsubscribe_button": "Darse de baja",
|
||||||
"update": {
|
"update": {
|
||||||
@@ -66,9 +68,12 @@
|
|||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"commit_db": "Error SQL",
|
"commit_db": "Error SQL",
|
||||||
|
"file_missing": "Falta el archivo $0 \"$1\"",
|
||||||
"impossible_value": "El valor \"$0\" no es posible para el campo \"$1\"",
|
"impossible_value": "El valor \"$0\" no es posible para el campo \"$1\"",
|
||||||
"no_auth": "Sin autorización",
|
"no_auth": "Sin autorización",
|
||||||
"unknown_field": "Campo \"$0\" desconocido"
|
"not_found": "Acción desconocida",
|
||||||
|
"unknown_field": "Campo \"$0\" desconocido",
|
||||||
|
"unknown_type": "Tipo \"$0\" desconocido"
|
||||||
},
|
},
|
||||||
"feed": {
|
"feed": {
|
||||||
"counter": "N.º $0",
|
"counter": "N.º $0",
|
||||||
@@ -140,6 +145,7 @@
|
|||||||
"id": "ID de Spot",
|
"id": "ID de Spot",
|
||||||
"model": "Modelo",
|
"model": "Modelo",
|
||||||
"name": "Spot",
|
"name": "Spot",
|
||||||
|
"no_new_msg": "No hay mensajes nuevos",
|
||||||
"plural": "Spots",
|
"plural": "Spots",
|
||||||
"ref_id": "ID de referencia de Spot"
|
"ref_id": "ID de referencia de Spot"
|
||||||
},
|
},
|
||||||
@@ -182,8 +188,16 @@
|
|||||||
},
|
},
|
||||||
"mode_archived": "El proyecto \"$0\" está archivado. No se puede cargar.",
|
"mode_archived": "El proyecto \"$0\" está archivado. No se puede cargar.",
|
||||||
"position": {
|
"position": {
|
||||||
|
"determining": "Determinando la posición…",
|
||||||
|
"error": "No se puede determinar la posición",
|
||||||
"new": "Nueva posición",
|
"new": "Nueva posición",
|
||||||
"title": "Subir posición"
|
"permission_denied": "Se ha denegado el permiso de ubicación",
|
||||||
|
"sending": "Enviando la posición…",
|
||||||
|
"success": "Posición subida correctamente",
|
||||||
|
"timeout": "La solicitud de posición ha agotado el tiempo de espera",
|
||||||
|
"title": "Subir posición",
|
||||||
|
"unavailable": "La posición no está disponible",
|
||||||
|
"unsupported": "Este navegador no admite la geolocalización"
|
||||||
},
|
},
|
||||||
"success": "$0 se ha subido correctamente."
|
"success": "$0 se ha subido correctamente."
|
||||||
},
|
},
|
||||||
|
|||||||
+16
-2
@@ -55,6 +55,8 @@
|
|||||||
"subject": "Confirmation",
|
"subject": "Confirmation",
|
||||||
"thanks_subject": "C'est tout bon !"
|
"thanks_subject": "C'est tout bon !"
|
||||||
},
|
},
|
||||||
|
"failure": "Échec de l’envoi de l’e-mail de mise à jour",
|
||||||
|
"sent": "E-mail de mise à jour envoyé",
|
||||||
"unsubscribe": "PS : Trop d'e-mails ?",
|
"unsubscribe": "PS : Trop d'e-mails ?",
|
||||||
"unsubscribe_button": "Se désinscrire",
|
"unsubscribe_button": "Se désinscrire",
|
||||||
"update": {
|
"update": {
|
||||||
@@ -66,9 +68,12 @@
|
|||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"commit_db": "Erreur lors de la requête SQL",
|
"commit_db": "Erreur lors de la requête SQL",
|
||||||
|
"file_missing": "Le fichier $0 \"$1\" est manquant",
|
||||||
"impossible_value": "La valeur \"$0\" n'est pas possible pour le champ \"$1\"",
|
"impossible_value": "La valeur \"$0\" n'est pas possible pour le champ \"$1\"",
|
||||||
"no_auth": "Pas d'autorisation",
|
"no_auth": "Pas d'autorisation",
|
||||||
"unknown_field": "Champ \"$0\" inconnu"
|
"not_found": "Action inconnue",
|
||||||
|
"unknown_field": "Champ \"$0\" inconnu",
|
||||||
|
"unknown_type": "Type \"$0\" inconnu"
|
||||||
},
|
},
|
||||||
"feed": {
|
"feed": {
|
||||||
"counter": "N°$0",
|
"counter": "N°$0",
|
||||||
@@ -140,6 +145,7 @@
|
|||||||
"id": "ID Spot",
|
"id": "ID Spot",
|
||||||
"model": "Modèle",
|
"model": "Modèle",
|
||||||
"name": "Spot",
|
"name": "Spot",
|
||||||
|
"no_new_msg": "Aucun nouveau message",
|
||||||
"plural": "Spots",
|
"plural": "Spots",
|
||||||
"ref_id": "ID Spot ref."
|
"ref_id": "ID Spot ref."
|
||||||
},
|
},
|
||||||
@@ -182,8 +188,16 @@
|
|||||||
},
|
},
|
||||||
"mode_archived": "Le projet \"$0\" a été archivé. Aucun téléversement possible.",
|
"mode_archived": "Le projet \"$0\" a été archivé. Aucun téléversement possible.",
|
||||||
"position": {
|
"position": {
|
||||||
|
"determining": "Détermination de la position…",
|
||||||
|
"error": "Impossible de déterminer la position",
|
||||||
"new": "Nouvelle position",
|
"new": "Nouvelle position",
|
||||||
"title": "Position supplémentaire"
|
"permission_denied": "L’autorisation d’accéder à la position a été refusée",
|
||||||
|
"sending": "Envoi de la position…",
|
||||||
|
"success": "Position téléversée avec succès",
|
||||||
|
"timeout": "La demande de position a expiré",
|
||||||
|
"title": "Position supplémentaire",
|
||||||
|
"unavailable": "La position n’est pas disponible",
|
||||||
|
"unsupported": "Ce navigateur ne prend pas en charge la géolocalisation"
|
||||||
},
|
},
|
||||||
"success": "$0 a été téléversé"
|
"success": "$0 a été téléversé"
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -105,7 +105,7 @@ export default {
|
|||||||
window.removeEventListener('hashchange', this.onBrowserHashChange);
|
window.removeEventListener('hashchange', this.onBrowserHashChange);
|
||||||
this.mobileMediaQuery.removeEventListener('change', this.updateMobile);
|
this.mobileMediaQuery.removeEventListener('change', this.updateMobile);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div id="main">
|
<div id="main">
|
||||||
|
|||||||
+37
-34
@@ -24,8 +24,8 @@ export default {
|
|||||||
this.setProjects();
|
this.setProjects();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
l(id) {
|
l(sLangId, asLangParams=[]) {
|
||||||
return this.lang.get(id);
|
return this.lang.get(sLangId, asLangParams);
|
||||||
},
|
},
|
||||||
addFeedback(sType, sMsg, asContext = {}) {
|
addFeedback(sType, sMsg, asContext = {}) {
|
||||||
delete asContext.a;
|
delete asContext.a;
|
||||||
@@ -43,7 +43,7 @@ export default {
|
|||||||
|
|
||||||
for(const [sType, aoElems] of Object.entries(aoElemTypes)) {
|
for(const [sType, aoElems] of Object.entries(aoElemTypes)) {
|
||||||
this.elems[sType] = {};
|
this.elems[sType] = {};
|
||||||
for(const [iKey, oElem] of Object.entries(aoElems)) {
|
for(const oElem of Object.values(aoElems)) {
|
||||||
oElem.type = sType;
|
oElem.type = sType;
|
||||||
this.elems[sType][oElem.id] = oElem;
|
this.elems[sType][oElem.id] = oElem;
|
||||||
}
|
}
|
||||||
@@ -53,29 +53,14 @@ export default {
|
|||||||
this.api.post('admin_create', {type: sType})
|
this.api.post('admin_create', {type: sType})
|
||||||
.then((aoNewElemTypes) => {
|
.then((aoNewElemTypes) => {
|
||||||
for(const [sType, aoNewElems] of Object.entries(aoNewElemTypes)) {
|
for(const [sType, aoNewElems] of Object.entries(aoNewElemTypes)) {
|
||||||
for(const [iKey, oNewElem] of Object.entries(aoNewElems)) {
|
for(const oNewElem of Object.values(aoNewElems)) {
|
||||||
oNewElem.type = sType;
|
oNewElem.type = sType;
|
||||||
this.elems[sType][oNewElem.id] = oNewElem;
|
this.elems[sType][oNewElem.id] = oNewElem;
|
||||||
this.addFeedback('success', this.l('admin.create_success'), {'create':sType});
|
this.addFeedback('success', this.l('admin.create_success'), {'create':sType});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((sMsg) => {this.addFeedback('error', sMsg, {'create':sType});});
|
.catch((oError) => {this.addFeedback('error', oError.desc_lang_text, {'create':sType});});
|
||||||
},
|
|
||||||
deleteElem(oElem) {
|
|
||||||
const asInputs = {
|
|
||||||
type: oElem.type,
|
|
||||||
id: oElem.id
|
|
||||||
};
|
|
||||||
|
|
||||||
this.api.post('admin_delete', asInputs)
|
|
||||||
.then((asData) => {
|
|
||||||
delete this.elems[asInputs.type][asInputs.id];
|
|
||||||
this.addFeedback('success', this.l('admin.delete_success'), asInputs);
|
|
||||||
})
|
|
||||||
.catch((sError) => {
|
|
||||||
this.addFeedback('error', sError, asInputs);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
updateElem(oElem, oEvent) {
|
updateElem(oElem, oEvent) {
|
||||||
if(this.saveTimer) clearTimeout(this.saveTimer);
|
if(this.saveTimer) clearTimeout(this.saveTimer);
|
||||||
@@ -90,28 +75,46 @@ export default {
|
|||||||
value: sNewVal
|
value: sNewVal
|
||||||
};
|
};
|
||||||
|
|
||||||
this.api.post('admin_set', asInputs)
|
this.api.request('admin_set', asInputs, 'POST')
|
||||||
.then((asData) => {
|
.then((oResponse) => {
|
||||||
this.elems[oElem.type][oElem.id][oEvent.target.name] = sNewVal;
|
this.elems[oElem.type][oElem.id][oEvent.target.name] = sNewVal;
|
||||||
this.addFeedback('success', this.l('admin.save_success'), asInputs);
|
const bSuccess = (oResponse.desc_lang_id == '');
|
||||||
|
const sType = bSuccess?'success':'warning';
|
||||||
|
const sText = this.l(bSuccess?'admin.save_success':oResponse.desc_lang_id, oResponse.desc_lang_params);
|
||||||
|
this.addFeedback(sType, sText, asInputs);
|
||||||
})
|
})
|
||||||
.catch((sError) => {
|
.catch((oError) => {
|
||||||
oEvent.target.value = sOldVal;
|
oEvent.target.value = sOldVal;
|
||||||
this.addFeedback('error', sError, asInputs);
|
this.addFeedback('error', oError.desc_lang_text, asInputs);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
deleteElem(oElem) {
|
||||||
|
const asInputs = {
|
||||||
|
type: oElem.type,
|
||||||
|
id: oElem.id
|
||||||
|
};
|
||||||
|
|
||||||
|
this.api.post('admin_delete', asInputs)
|
||||||
|
.then(() => {
|
||||||
|
delete this.elems[asInputs.type][asInputs.id];
|
||||||
|
this.addFeedback('success', this.l('admin.delete_success'), asInputs);
|
||||||
|
})
|
||||||
|
.catch((oError) => {
|
||||||
|
this.addFeedback('error', oError.desc_lang_text, asInputs);
|
||||||
|
});
|
||||||
|
},
|
||||||
queue(oElem, oEvent) {
|
queue(oElem, oEvent) {
|
||||||
if(this.saveTimer) clearTimeout(this.saveTimer);
|
if(this.saveTimer) clearTimeout(this.saveTimer);
|
||||||
this.saveTimer = setTimeout(() => {this.updateElem(oElem, oEvent);}, 2000);
|
this.saveTimer = setTimeout(() => {this.updateElem(oElem, oEvent);}, 2000);
|
||||||
},
|
},
|
||||||
updateProject() {
|
updateProject() {
|
||||||
this.api.post('update_project')
|
this.api.request('update_project', {}, 'POST')
|
||||||
.then((asData, sMsg) => {this.addFeedback('success', sMsg, {'update':'project'});})
|
.then((oResponse) => {this.addFeedback('success', oResponse.desc_lang_text, {'update':'project'});})
|
||||||
.catch((sMsg) => {this.addFeedback('error', sMsg, {'update':'project'});});
|
.catch((oError) => {this.addFeedback('error', oError.desc_lang_text, {'update':'project'});});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div id="admin">
|
<div id="admin">
|
||||||
@@ -131,7 +134,7 @@ export default {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="project in elems.project">
|
<tr v-for="project in elems.project" :key="project.id">
|
||||||
<td>{{ project.id }}</td>
|
<td>{{ project.id }}</td>
|
||||||
<td><AdminInput :type="'text'" :name="'name'" :elem="project" /></td>
|
<td><AdminInput :type="'text'" :name="'name'" :elem="project" /></td>
|
||||||
<td>{{ project.mode }}</td>
|
<td>{{ project.mode }}</td>
|
||||||
@@ -160,7 +163,7 @@ export default {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="feed in elems.feed">
|
<tr v-for="feed in elems.feed" :key="feed.id">
|
||||||
<td>{{ feed.id }}</td>
|
<td>{{ feed.id }}</td>
|
||||||
<td><AdminInput :type="'text'" :name="'ref_feed_id'" :elem="feed" /></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_spot'" :elem="feed" /></td>
|
||||||
@@ -186,7 +189,7 @@ export default {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="spot in elems.spot">
|
<tr v-for="spot in elems.spot" :key="spot.id">
|
||||||
<td>{{ spot.id }}</td>
|
<td>{{ spot.id }}</td>
|
||||||
<td>{{ spot.ref_spot_id }}</td>
|
<td>{{ spot.ref_spot_id }}</td>
|
||||||
<td>{{ spot.name }}</td>
|
<td>{{ spot.name }}</td>
|
||||||
@@ -209,7 +212,7 @@ export default {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="user in elems.user">
|
<tr v-for="user in elems.user" :key="user.id">
|
||||||
<td>{{ user.id }}</td>
|
<td>{{ user.id }}</td>
|
||||||
<td class="left">{{ user.name }}</td>
|
<td class="left">{{ user.name }}</td>
|
||||||
<td class="left">{{ user.email }}</td>
|
<td class="left">{{ user.email }}</td>
|
||||||
@@ -225,7 +228,7 @@ export default {
|
|||||||
<AppButton :classes="'refresh'" :text="l('project.update_messages')" :icon="'refresh'" @click="updateProject" />
|
<AppButton :classes="'refresh'" :text="l('project.update_messages')" :icon="'refresh'" @click="updateProject" />
|
||||||
</div>
|
</div>
|
||||||
<div id="feedback" class="feedback">
|
<div id="feedback" class="feedback">
|
||||||
<p v-for="feedback in feedbacks" :class="feedback.type">{{ feedback.msg }}</p>
|
<p v-for="(feedback, index) in feedbacks" :key="index" :class="feedback.type">{{ feedback.msg }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
return this.elem[this.name];
|
return this.elem[this.name];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export default {
|
|||||||
iconClasses: String,
|
iconClasses: String,
|
||||||
iconSize: String
|
iconSize: String
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<button :class="classes"><AppIcon :icon="icon" :text="text" :classes="iconClasses" :size="iconSize" /></button>
|
<button :class="classes"><AppIcon :icon="icon" :text="text" :classes="iconClasses" :size="iconSize" /></button>
|
||||||
|
|||||||
@@ -18,14 +18,6 @@ export default {
|
|||||||
transform: String
|
transform: String
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
iconClassNames() {
|
|
||||||
return [
|
|
||||||
'app-icon',
|
|
||||||
this.icon,
|
|
||||||
...(this.classes || '').split(/\s+/),
|
|
||||||
this.margin?'margin-'+this.margin:null
|
|
||||||
].filter(Boolean).join(' ');
|
|
||||||
},
|
|
||||||
resolvedFixedWidth() {
|
resolvedFixedWidth() {
|
||||||
return (this.width == 'fixed') || null;
|
return (this.width == 'fixed') || null;
|
||||||
},
|
},
|
||||||
@@ -45,7 +37,7 @@ export default {
|
|||||||
return this.transform || null;
|
return this.transform || null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -93,4 +85,4 @@ export default {
|
|||||||
margin-left: var.$text-spacing;
|
margin-left: var.$text-spacing;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default {
|
|||||||
].filter(Boolean).join(' ');
|
].filter(Boolean).join(' ');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -0,0 +1,585 @@
|
|||||||
|
<script>
|
||||||
|
import AppIcon from '@components/AppIcon';
|
||||||
|
import { getStyleProperty } from '@scripts/common';
|
||||||
|
|
||||||
|
/* lightbox (https://github.com/lokesh/lightbox2) converted to a vue component and improved to support videos */
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
AppIcon
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
alwaysShowNavOnTouchDevices: {type: Boolean, default: false},
|
||||||
|
positionFromTop: {type: Number, default: 50},
|
||||||
|
wrapAround: {type: Boolean, default: false},
|
||||||
|
disableScrolling: {type: Boolean, default: false},
|
||||||
|
sanitizeTitle: {type: Boolean, default: false},
|
||||||
|
hasVideo: {type: Boolean, default: true},
|
||||||
|
maxWidth: {type: Number, default: null},
|
||||||
|
maxHeight: {type: Number, default: null}
|
||||||
|
},
|
||||||
|
emits: ['media-change', 'closing'],
|
||||||
|
data() {
|
||||||
|
/*
|
||||||
|
fadeDuration/imageFadeDuration/resizeDuration read --trans-quick/--trans-slow
|
||||||
|
directly rather than being passed down as props from Project.vue: they're
|
||||||
|
declared on :root (_common.scss), so they're available immediately.
|
||||||
|
No need to wait on Project.vue's own $el to mount first, and no risk of
|
||||||
|
a parent-to-child prop update lagging behind the first time this opens.
|
||||||
|
*/
|
||||||
|
const fadeDuration = parseFloat(getStyleProperty('--trans-quick'));
|
||||||
|
const resizeDuration = parseFloat(getStyleProperty('--trans-slow'));
|
||||||
|
|
||||||
|
return {
|
||||||
|
album: [],
|
||||||
|
currentImageIndex: 0,
|
||||||
|
gMouseDownOffsetX: 0,
|
||||||
|
gMouseDownOffsetY: 0,
|
||||||
|
resizeTimer: null,
|
||||||
|
containerPadding: null,
|
||||||
|
imageBorderWidth: null,
|
||||||
|
videoBorderWidth: null,
|
||||||
|
fadeDuration,
|
||||||
|
imageFadeDuration: fadeDuration,
|
||||||
|
resizeDuration
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.setVisible(this.$refs.overlay, false);
|
||||||
|
this.setVisible(this.$refs.lightboxEl, false);
|
||||||
|
|
||||||
|
this.containerPadding = this.getBoxMetrics(this.$refs.container, 'padding');
|
||||||
|
this.imageBorderWidth = this.getBoxMetrics(this.$refs.image, 'border');
|
||||||
|
this.videoBorderWidth = this.getBoxMetrics(this.$refs.video, 'border');
|
||||||
|
|
||||||
|
this.$refs.nav.addEventListener('wheel', this.onWheel, {passive: false});
|
||||||
|
this.$refs.nav.addEventListener('mousedown', this.onDragStart);
|
||||||
|
window.addEventListener('mouseup', this.onDragEnd);
|
||||||
|
|
||||||
|
this.enable();
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
this.disable();
|
||||||
|
if(this.resizeTimer) clearTimeout(this.resizeTimer);
|
||||||
|
window.removeEventListener('mouseup', this.onDragEnd);
|
||||||
|
window.removeEventListener('resize', this.sizeOverlay);
|
||||||
|
window.removeEventListener('mousemove', this.onDragMove);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
enable() {
|
||||||
|
document.body.addEventListener('click', this.onBodyClick);
|
||||||
|
},
|
||||||
|
disable() {
|
||||||
|
document.body.removeEventListener('click', this.onBodyClick);
|
||||||
|
},
|
||||||
|
onBodyClick(event) {
|
||||||
|
const link = event.target.closest('a[data-lightbox], area[data-lightbox]');
|
||||||
|
if(!link) return;
|
||||||
|
event.preventDefault();
|
||||||
|
this.start(link);
|
||||||
|
},
|
||||||
|
start(link) {
|
||||||
|
this.sizeOverlay();
|
||||||
|
this.album = [];
|
||||||
|
let imageNumber = 0;
|
||||||
|
const setName = link.getAttribute('data-lightbox');
|
||||||
|
|
||||||
|
const links = [...document.querySelectorAll(`${link.tagName}[data-lightbox="${CSS.escape(setName)}"]`)];
|
||||||
|
links.forEach((item, index) => {
|
||||||
|
this.addToAlbum(item);
|
||||||
|
if(item === link) imageNumber = index;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fade(this.$refs.overlay, true, this.fadeDuration);
|
||||||
|
this.fade(this.$refs.lightboxEl, true, this.fadeDuration);
|
||||||
|
|
||||||
|
if(this.disableScrolling) document.body.classList.add('lb-disable-scrolling');
|
||||||
|
|
||||||
|
window.addEventListener('resize', this.sizeOverlay);
|
||||||
|
this.changeImage(imageNumber);
|
||||||
|
},
|
||||||
|
end(dispose = false) {
|
||||||
|
this.disableKeyboardNav();
|
||||||
|
this.$refs.video?.pause();
|
||||||
|
this.$refs.video?.removeAttribute('src');
|
||||||
|
this.$refs.container?.classList.remove('lb-video-nav', 'moveable', 'moving');
|
||||||
|
window.removeEventListener('resize', this.sizeOverlay);
|
||||||
|
window.removeEventListener('mousemove', this.onDragMove);
|
||||||
|
|
||||||
|
if(dispose) {
|
||||||
|
this.album = [];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.fade(this.$refs.lightboxEl, false, this.fadeDuration);
|
||||||
|
this.fade(this.$refs.overlay, false, this.fadeDuration);
|
||||||
|
this.$emit('closing');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.disableScrolling) document.body.classList.remove('lb-disable-scrolling');
|
||||||
|
},
|
||||||
|
addToAlbum(link) {
|
||||||
|
const img = link.querySelector('img');
|
||||||
|
this.album.push({
|
||||||
|
alt: link.getAttribute('data-alt') || '',
|
||||||
|
link: link.getAttribute('href'),
|
||||||
|
title: link.getAttribute('data-title') || link.getAttribute('title') || '',
|
||||||
|
orientation: parseInt(link.getAttribute('data-orientation') || '0', 10),
|
||||||
|
type: link.getAttribute('data-type') || 'image',
|
||||||
|
id: link.getAttribute('data-id'),
|
||||||
|
width: parseInt(img?.getAttribute('width') || '0', 10),
|
||||||
|
height: parseInt(img?.getAttribute('height') || '0', 10),
|
||||||
|
set: link.getAttribute('data-lightbox') || ''
|
||||||
|
});
|
||||||
|
},
|
||||||
|
hasMediaAfterCurrent() {
|
||||||
|
return this.currentImageIndex < this.album.length - 1;
|
||||||
|
},
|
||||||
|
refreshAlbum() {
|
||||||
|
const current = this.album[this.currentImageIndex];
|
||||||
|
if(!current?.set) return;
|
||||||
|
|
||||||
|
const links = [...document.querySelectorAll(`a[data-lightbox="${CSS.escape(current.set)}"], area[data-lightbox="${CSS.escape(current.set)}"]`)];
|
||||||
|
if(!links.length) return;
|
||||||
|
|
||||||
|
const existingKeys = new Set(this.album.map((media) => this.getMediaKey(media)));
|
||||||
|
links.forEach((link) => {
|
||||||
|
const key = this.getLinkMediaKey(link);
|
||||||
|
if(existingKeys.has(key)) return;
|
||||||
|
|
||||||
|
this.addToAlbum(link);
|
||||||
|
existingKeys.add(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateNav();
|
||||||
|
},
|
||||||
|
getMediaKey(media) {
|
||||||
|
return `${media.set}:${media.id}`;
|
||||||
|
},
|
||||||
|
getLinkMediaKey(link) {
|
||||||
|
return `${link.getAttribute('data-lightbox') || ''}:${link.getAttribute('data-id')}`;
|
||||||
|
},
|
||||||
|
getMaxSizes(mediaType) {
|
||||||
|
let maxWidth = window.innerWidth - this.containerPadding.left - this.containerPadding.right;
|
||||||
|
let maxHeight = window.innerHeight - this.containerPadding.top - this.containerPadding.bottom - this.positionFromTop;
|
||||||
|
const border = mediaType === 'image' ? this.imageBorderWidth : this.videoBorderWidth;
|
||||||
|
maxWidth -= border.left + border.right;
|
||||||
|
maxHeight -= border.top + border.bottom;
|
||||||
|
maxHeight -= this.getDataContainerHeight(maxWidth + this.containerPadding.left + this.containerPadding.right + border.left + border.right);
|
||||||
|
|
||||||
|
return {
|
||||||
|
maxWidth: Math.max(maxWidth, 1),
|
||||||
|
maxHeight: Math.max(maxHeight, 1)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getDataContainerHeight(width = null) {
|
||||||
|
if(!this.$refs.dataContainer) return 0;
|
||||||
|
|
||||||
|
const currentWidth = this.$refs.dataContainer.style.width;
|
||||||
|
if(width !== null) this.$refs.dataContainer.style.width = `${width}px`;
|
||||||
|
const height = Math.ceil(this.$refs.dataContainer.getBoundingClientRect().height || this.$refs.dataContainer.offsetHeight || 0);
|
||||||
|
this.$refs.dataContainer.style.width = currentWidth;
|
||||||
|
|
||||||
|
return height;
|
||||||
|
},
|
||||||
|
getMediaSize(media, maxWidth, maxHeight) {
|
||||||
|
if(media.width <= maxWidth && media.height <= maxHeight) {
|
||||||
|
return {
|
||||||
|
width: media.width,
|
||||||
|
height: media.height
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const widthRatio = media.width / maxWidth;
|
||||||
|
const heightRatio = media.height / maxHeight;
|
||||||
|
|
||||||
|
if(widthRatio > heightRatio) {
|
||||||
|
return {
|
||||||
|
width: maxWidth,
|
||||||
|
height: Math.round(media.height / widthRatio)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: Math.round(media.width / heightRatio),
|
||||||
|
height: maxHeight
|
||||||
|
};
|
||||||
|
},
|
||||||
|
fitSizeWithDataContainer(size, mediaType) {
|
||||||
|
const border = mediaType === 'image' ? this.imageBorderWidth : this.videoBorderWidth;
|
||||||
|
const maxOuterHeight = Math.max(window.innerHeight - this.positionFromTop, 1);
|
||||||
|
let fittedSize = size;
|
||||||
|
|
||||||
|
for(let i = 0; i < 5; i++) {
|
||||||
|
const containerWidth = fittedSize.width + this.containerPadding.left + this.containerPadding.right + border.left + border.right;
|
||||||
|
const containerHeight = fittedSize.height + this.containerPadding.top + this.containerPadding.bottom + border.top + border.bottom;
|
||||||
|
const dataHeight = this.getDataContainerHeight(containerWidth);
|
||||||
|
const overflow = Math.ceil(containerHeight + dataHeight - maxOuterHeight);
|
||||||
|
if(overflow <= 0 || fittedSize.height <= 1) break;
|
||||||
|
|
||||||
|
const height = Math.max(fittedSize.height - overflow, 1);
|
||||||
|
fittedSize = {
|
||||||
|
width: Math.max(Math.round(fittedSize.width * (height / fittedSize.height)), 1),
|
||||||
|
height
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return fittedSize;
|
||||||
|
},
|
||||||
|
updateSize(index) {
|
||||||
|
const media = this.album[index];
|
||||||
|
const maxSizes = this.getMaxSizes(media.type);
|
||||||
|
const maxWidth = this.maxWidth ? Math.min(this.maxWidth, maxSizes.maxWidth) : maxSizes.maxWidth;
|
||||||
|
const maxHeight = this.maxHeight ? Math.min(this.maxHeight, maxSizes.maxHeight) : maxSizes.maxHeight;
|
||||||
|
const size = this.fitSizeWithDataContainer(this.getMediaSize(media, maxWidth, maxHeight), media.type);
|
||||||
|
|
||||||
|
const target = media.type === 'video' ? this.$refs.video : this.$refs.image;
|
||||||
|
target.width = size.width;
|
||||||
|
target.height = size.height;
|
||||||
|
this.sizeContainer(size.width, size.height, media.type);
|
||||||
|
},
|
||||||
|
changeImage(index) {
|
||||||
|
const media = this.album[index];
|
||||||
|
if(!media) return;
|
||||||
|
|
||||||
|
this.updateDetails(media, false);
|
||||||
|
this.hideElements([this.$refs.dataContainer]);
|
||||||
|
this.disableKeyboardNav();
|
||||||
|
this.fade(this.$refs.overlay, true, this.fadeDuration);
|
||||||
|
this.fade(this.$refs.loader, true, 200);
|
||||||
|
this.hideElements([this.$refs.image, this.$refs.video, this.$refs.nav, this.$refs.prev, this.$refs.next]);
|
||||||
|
this.resetImageTransform();
|
||||||
|
this.$refs.outerContainer.classList.add('animating');
|
||||||
|
this.$refs.container.classList.remove('moveable', 'moving', 'lb-video-nav');
|
||||||
|
this.currentImageIndex = index;
|
||||||
|
|
||||||
|
this.$emit('media-change', media);
|
||||||
|
|
||||||
|
if(media.type === 'video') {
|
||||||
|
this.$refs.image.removeAttribute('src');
|
||||||
|
this.$refs.container.classList.add('lb-video-nav');
|
||||||
|
this.$refs.video.onloadedmetadata = () => {
|
||||||
|
media.width = this.$refs.video.videoWidth;
|
||||||
|
media.height = this.$refs.video.videoHeight;
|
||||||
|
this.$refs.video.onloadedmetadata = null;
|
||||||
|
this.updateSize(index);
|
||||||
|
};
|
||||||
|
this.$refs.video.src = media.link;
|
||||||
|
} else {
|
||||||
|
this.$refs.video.pause();
|
||||||
|
this.$refs.video.removeAttribute('src');
|
||||||
|
this.$refs.image.onload = () => {
|
||||||
|
this.$refs.image.alt = media.alt;
|
||||||
|
let width = this.$refs.image.naturalWidth;
|
||||||
|
let height = this.$refs.image.naturalHeight;
|
||||||
|
if(Math.abs(media.orientation) === 90 && width > height) {
|
||||||
|
const tmp = width;
|
||||||
|
width = height;
|
||||||
|
height = tmp;
|
||||||
|
}
|
||||||
|
media.width = width;
|
||||||
|
media.height = height;
|
||||||
|
this.$refs.image.onload = null;
|
||||||
|
this.updateSize(index);
|
||||||
|
};
|
||||||
|
this.$refs.image.src = media.link;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sizeOverlay() {
|
||||||
|
if(this.resizeTimer) clearTimeout(this.resizeTimer);
|
||||||
|
if(!this.album.length) return;
|
||||||
|
|
||||||
|
this.resizeTimer = window.setTimeout(() => {
|
||||||
|
const current = this.album[this.currentImageIndex];
|
||||||
|
if(!current) return;
|
||||||
|
if(current.type === 'image') this.changeImage(this.currentImageIndex);
|
||||||
|
else this.updateSize(this.currentImageIndex);
|
||||||
|
}, 200);
|
||||||
|
},
|
||||||
|
sizeContainer(width, height, mediaType = 'image') {
|
||||||
|
const border = mediaType === 'image' ? this.imageBorderWidth : this.videoBorderWidth;
|
||||||
|
const newWidth = width + this.containerPadding.left + this.containerPadding.right + border.left + border.right;
|
||||||
|
const newHeight = height + this.containerPadding.top + this.containerPadding.bottom + border.top + border.bottom;
|
||||||
|
const dataHeight = this.getDataContainerHeight(newWidth);
|
||||||
|
|
||||||
|
this.$refs.outerContainer.style.transition = `width ${this.resizeDuration}ms, height ${this.resizeDuration}ms`;
|
||||||
|
this.$refs.outerContainer.style.width = `${newWidth}px`;
|
||||||
|
this.$refs.outerContainer.style.height = `${newHeight + dataHeight}px`;
|
||||||
|
this.$refs.container.style.height = `${newHeight}px`;
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
this.$refs.overlay.focus();
|
||||||
|
this.showImage();
|
||||||
|
this.$refs.outerContainer.style.transition = '';
|
||||||
|
}, this.resizeDuration);
|
||||||
|
},
|
||||||
|
showImage() {
|
||||||
|
this.fade(this.$refs.loader, false, 0);
|
||||||
|
if(this.hasVideo && this.album[this.currentImageIndex].type === 'video') this.fade(this.$refs.video, true, this.imageFadeDuration);
|
||||||
|
else this.fade(this.$refs.image, true, this.imageFadeDuration);
|
||||||
|
|
||||||
|
this.updateNav();
|
||||||
|
this.updateDetails();
|
||||||
|
this.preloadNeighboringImages();
|
||||||
|
this.enableKeyboardNav();
|
||||||
|
},
|
||||||
|
updateNav() {
|
||||||
|
this.setVisible(this.$refs.nav, true);
|
||||||
|
this.setVisible(this.$refs.prev, false);
|
||||||
|
this.setVisible(this.$refs.next, false);
|
||||||
|
|
||||||
|
const alwaysShowNav = ('ontouchstart' in window) && this.alwaysShowNavOnTouchDevices;
|
||||||
|
if(this.album.length <= 1) return;
|
||||||
|
|
||||||
|
if(this.wrapAround) {
|
||||||
|
this.setVisible(this.$refs.prev, true);
|
||||||
|
this.setVisible(this.$refs.next, true);
|
||||||
|
} else {
|
||||||
|
if(this.currentImageIndex > 0) this.setVisible(this.$refs.prev, true);
|
||||||
|
if(this.currentImageIndex < this.album.length - 1) this.setVisible(this.$refs.next, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(alwaysShowNav) {
|
||||||
|
this.$refs.prev.style.opacity = '1';
|
||||||
|
this.$refs.next.style.opacity = '1';
|
||||||
|
} else {
|
||||||
|
this.$refs.prev.style.opacity = '';
|
||||||
|
this.$refs.next.style.opacity = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateDetails(media = this.album[this.currentImageIndex], show = true) {
|
||||||
|
if(!media) return;
|
||||||
|
|
||||||
|
if(media.title) {
|
||||||
|
if(this.sanitizeTitle) this.$refs.caption.textContent = media.title;
|
||||||
|
else this.$refs.caption.innerHTML = media.title;
|
||||||
|
if(show) this.fade(this.$refs.caption, true, 200);
|
||||||
|
else this.setVisible(this.$refs.caption, true);
|
||||||
|
} else {
|
||||||
|
this.$refs.caption.textContent = '';
|
||||||
|
this.setVisible(this.$refs.caption, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(show) {
|
||||||
|
this.fade(this.$refs.closeButton, true, 200);
|
||||||
|
this.$refs.outerContainer.classList.remove('animating');
|
||||||
|
this.fade(this.$refs.dataContainer, true, this.resizeDuration);
|
||||||
|
} else {
|
||||||
|
this.setVisible(this.$refs.closeButton, true);
|
||||||
|
this.setVisible(this.$refs.dataContainer, false);
|
||||||
|
this.$refs.dataContainer.style.transition = '';
|
||||||
|
this.$refs.dataContainer.style.opacity = '0';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
preloadNeighboringImages() {
|
||||||
|
const next = this.album[this.currentImageIndex + 1];
|
||||||
|
const prev = this.album[this.currentImageIndex - 1];
|
||||||
|
if(next && next.type === 'image') {
|
||||||
|
const preloadNext = new Image();
|
||||||
|
preloadNext.src = next.link;
|
||||||
|
}
|
||||||
|
if(prev && prev.type === 'image') {
|
||||||
|
const preloadPrev = new Image();
|
||||||
|
preloadPrev.src = prev.link;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enableKeyboardNav() {
|
||||||
|
this.disableKeyboardNav();
|
||||||
|
this.$refs.lightboxEl.addEventListener('keyup', this.keyboardAction);
|
||||||
|
this.$refs.overlay.addEventListener('keyup', this.keyboardAction);
|
||||||
|
},
|
||||||
|
disableKeyboardNav() {
|
||||||
|
this.$refs.lightboxEl?.removeEventListener('keyup', this.keyboardAction);
|
||||||
|
this.$refs.overlay?.removeEventListener('keyup', this.keyboardAction);
|
||||||
|
},
|
||||||
|
keyboardAction(event) {
|
||||||
|
switch(event.key) {
|
||||||
|
case 'Escape':
|
||||||
|
event.stopPropagation();
|
||||||
|
this.end();
|
||||||
|
break;
|
||||||
|
case 'ArrowLeft':
|
||||||
|
if(this.currentImageIndex !== 0) this.changeImage(this.currentImageIndex - 1);
|
||||||
|
else if(this.wrapAround && this.album.length > 1) this.changeImage(this.album.length - 1);
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
if(this.currentImageIndex !== this.album.length - 1) this.changeImage(this.currentImageIndex + 1);
|
||||||
|
else if(this.wrapAround && this.album.length > 1) this.changeImage(0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCloseKeyup(event) {
|
||||||
|
if(event.key === 'Enter' || event.key === ' ') this.end();
|
||||||
|
},
|
||||||
|
onPrevClick() {
|
||||||
|
if(this.currentImageIndex === 0) this.changeImage(this.album.length - 1);
|
||||||
|
else this.changeImage(this.currentImageIndex - 1);
|
||||||
|
},
|
||||||
|
onNextClick() {
|
||||||
|
if(this.currentImageIndex === this.album.length - 1) this.changeImage(0);
|
||||||
|
else this.changeImage(this.currentImageIndex + 1);
|
||||||
|
},
|
||||||
|
onOuterContainerClick(event) {
|
||||||
|
if(event.target === this.$refs.outerContainer) this.end();
|
||||||
|
},
|
||||||
|
onLightboxClick(event) {
|
||||||
|
if(event.target === this.$refs.lightboxEl) this.end();
|
||||||
|
},
|
||||||
|
onWheel(event) {
|
||||||
|
const media = this.album[this.currentImageIndex];
|
||||||
|
if(!media || media.type === 'video') return;
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const rect = this.$refs.image.getBoundingClientRect();
|
||||||
|
const oldTransform = this.getImageTransform();
|
||||||
|
const oldZoom = oldTransform.scale;
|
||||||
|
const maxZoom = Math.max(media.width / Math.max(this.$refs.image.width, 1), media.height / Math.max(this.$refs.image.height, 1), 1);
|
||||||
|
const newZoom = Math.min(Math.max(oldZoom + (-Math.sign(event.deltaY) / 10), 1), maxZoom);
|
||||||
|
|
||||||
|
const imageCenterX = rect.left + rect.width / 2 - oldTransform.translateX;
|
||||||
|
const imageCenterY = rect.top + rect.height / 2 - oldTransform.translateY;
|
||||||
|
const cursorX = event.clientX - imageCenterX;
|
||||||
|
const cursorY = event.clientY - imageCenterY;
|
||||||
|
const zoomRatio = newZoom / oldZoom;
|
||||||
|
const transform = this.clampImageTransform({
|
||||||
|
scale: newZoom,
|
||||||
|
translateX: cursorX - zoomRatio * (cursorX - oldTransform.translateX),
|
||||||
|
translateY: cursorY - zoomRatio * (cursorY - oldTransform.translateY)
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$refs.container.classList.toggle('moveable', newZoom > 1);
|
||||||
|
this.setImageTransform(transform);
|
||||||
|
},
|
||||||
|
onDragStart(event) {
|
||||||
|
const scale = parseFloat(this.$refs.image.style.getPropertyValue('--scale') || '1');
|
||||||
|
if(scale <= 1) return;
|
||||||
|
|
||||||
|
this.gMouseDownOffsetX = event.clientX - parseFloat(this.$refs.image.style.getPropertyValue('--translate-x') || '0');
|
||||||
|
this.gMouseDownOffsetY = event.clientY - parseFloat(this.$refs.image.style.getPropertyValue('--translate-y') || '0');
|
||||||
|
this.$refs.container.classList.add('moving');
|
||||||
|
window.addEventListener('mousemove', this.onDragMove);
|
||||||
|
},
|
||||||
|
onDragMove(event) {
|
||||||
|
const zoom = parseFloat(this.$refs.image.style.getPropertyValue('--scale') || '1');
|
||||||
|
const transform = this.clampImageTransform({
|
||||||
|
scale: zoom,
|
||||||
|
translateX: event.clientX - this.gMouseDownOffsetX,
|
||||||
|
translateY: event.clientY - this.gMouseDownOffsetY
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setImageTransform(transform);
|
||||||
|
},
|
||||||
|
onDragEnd() {
|
||||||
|
window.removeEventListener('mousemove', this.onDragMove);
|
||||||
|
this.$refs.container?.classList.remove('moving');
|
||||||
|
},
|
||||||
|
getBoxMetrics(element, type) {
|
||||||
|
const styles = getComputedStyle(element);
|
||||||
|
return {
|
||||||
|
top: parseInt(styles[`${type}-top-width`], 10) || 0,
|
||||||
|
right: parseInt(styles[`${type}-right-width`], 10) || 0,
|
||||||
|
bottom: parseInt(styles[`${type}-bottom-width`], 10) || 0,
|
||||||
|
left: parseInt(styles[`${type}-left-width`], 10) || 0
|
||||||
|
};
|
||||||
|
},
|
||||||
|
resetImageTransform() {
|
||||||
|
this.setImageTransform({scale: 1, translateX: 0, translateY: 0});
|
||||||
|
},
|
||||||
|
getImageTransform() {
|
||||||
|
return {
|
||||||
|
scale: parseFloat(this.$refs.image.style.getPropertyValue('--scale') || '1'),
|
||||||
|
translateX: parseFloat(this.$refs.image.style.getPropertyValue('--translate-x') || '0'),
|
||||||
|
translateY: parseFloat(this.$refs.image.style.getPropertyValue('--translate-y') || '0')
|
||||||
|
};
|
||||||
|
},
|
||||||
|
clampImageTransform(transform) {
|
||||||
|
const maxTranslateX = (transform.scale - 1) * this.$refs.image.width / 2;
|
||||||
|
const maxTranslateY = (transform.scale - 1) * this.$refs.image.height / 2;
|
||||||
|
|
||||||
|
return {
|
||||||
|
scale: transform.scale,
|
||||||
|
translateX: Math.max(Math.min(transform.translateX, maxTranslateX), -maxTranslateX),
|
||||||
|
translateY: Math.max(Math.min(transform.translateY, maxTranslateY), -maxTranslateY)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
setImageTransform(transform) {
|
||||||
|
if(!this.$refs.image) return;
|
||||||
|
this.$refs.image.style.setProperty('--scale', String(transform.scale));
|
||||||
|
this.$refs.image.style.setProperty('--translate-x', `${transform.translateX}px`);
|
||||||
|
this.$refs.image.style.setProperty('--translate-y', `${transform.translateY}px`);
|
||||||
|
},
|
||||||
|
hideElements(elements) {
|
||||||
|
elements.forEach((element) => {
|
||||||
|
this.setVisible(element, false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setVisible(element, visible) {
|
||||||
|
if(!element) return;
|
||||||
|
element.style.visibility = visible ? 'visible' : 'hidden';
|
||||||
|
element.style.pointerEvents = visible ? '' : 'none';
|
||||||
|
},
|
||||||
|
fade(element, show, duration, done) {
|
||||||
|
if(!element) return;
|
||||||
|
|
||||||
|
const safeDuration = duration || 0;
|
||||||
|
element.style.transition = `opacity ${safeDuration}ms`;
|
||||||
|
if(show) {
|
||||||
|
this.setVisible(element, true);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
element.style.opacity = element === this.$refs.overlay ? '0.8' : '1';
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
element.style.opacity = '0';
|
||||||
|
element.style.pointerEvents = 'none';
|
||||||
|
window.setTimeout(() => {
|
||||||
|
this.setVisible(element, false);
|
||||||
|
}, safeDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(typeof done === 'function') {
|
||||||
|
window.setTimeout(done, safeDuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<div id="lightboxOverlay" ref="overlay" tabindex="-1" class="lightboxOverlay" @click="end()"></div>
|
||||||
|
<div id="lightbox" ref="lightboxEl" tabindex="-1" class="lightbox" @click="onLightboxClick">
|
||||||
|
<div class="lb-outerContainer" ref="outerContainer" @click.stop="onOuterContainerClick">
|
||||||
|
<div class="lb-container" ref="container">
|
||||||
|
<img class="lb-image" ref="image" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" alt="" />
|
||||||
|
<video class="lb-video" ref="video" controls autoplay></video>
|
||||||
|
<div class="lb-nav" ref="nav">
|
||||||
|
<div class="lb-prev-area">
|
||||||
|
<a class="lb-prev" ref="prev" aria-label="Previous image" href="" role="button" @click.prevent="onPrevClick">
|
||||||
|
<AppIcon :icon="'prev'" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="lb-next-area">
|
||||||
|
<a class="lb-next" ref="next" aria-label="Next image" href="" role="button" @click.prevent="onNextClick"><AppIcon :icon="'next'" /></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="lb-loader" ref="loader" @click.prevent="end()">
|
||||||
|
<a class="lb-cancel" ref="cancel" href="#">
|
||||||
|
<AppIcon :icon="'cancel'" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="lb-dataContainer desktop" ref="dataContainer" @click="end()">
|
||||||
|
<div class="lb-data">
|
||||||
|
<div class="lb-details">
|
||||||
|
<span class="lb-caption" ref="caption"></span>
|
||||||
|
</div>
|
||||||
|
<div class="lb-closeContainer">
|
||||||
|
<a class="lb-close" ref="closeButton" href="#" role="button" @click.prevent.stop="end()" @keyup="onCloseKeyup">
|
||||||
|
<AppIcon :icon="'close'" :classes="'fa-lg'" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
+76
-534
@@ -1,47 +1,19 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Map, Marker, LngLatBounds, LngLat, Popup, ScaleControl, NavigationControl, setWorkerUrl } from 'maplibre-gl';
|
import Lightbox from '@components/Lightbox';
|
||||||
import maplibreWorkerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url';
|
import ProjectMap, { BASE_MAP_PADDING } from '@components/ProjectMap';
|
||||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
import { getStyleProperty } from '@scripts/common';
|
||||||
|
|
||||||
import { createApp } from 'vue';
|
|
||||||
|
|
||||||
import Lightbox from '@scripts/lightbox';
|
|
||||||
|
|
||||||
import AppIcon from '@components/AppIcon';
|
import AppIcon from '@components/AppIcon';
|
||||||
import AppIconStack from '@components/AppIconStack';
|
|
||||||
import ProjectPopup from '@components/ProjectPopup';
|
|
||||||
import ProjectFeed from '@components/ProjectFeed';
|
import ProjectFeed from '@components/ProjectFeed';
|
||||||
import ProjectSettings from '@components/ProjectSettings';
|
import ProjectSettings from '@components/ProjectSettings';
|
||||||
|
|
||||||
setWorkerUrl(maplibreWorkerUrl);
|
|
||||||
|
|
||||||
class GroupedScaleControl {
|
|
||||||
constructor(options) {
|
|
||||||
this.scale = new ScaleControl(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
onAdd(map) {
|
|
||||||
this.container = document.createElement('div');
|
|
||||||
this.container.className = 'maplibregl-ctrl maplibregl-ctrl-group';
|
|
||||||
|
|
||||||
const scaleElement = this.scale.onAdd(map);
|
|
||||||
scaleElement.classList.remove('maplibregl-ctrl');
|
|
||||||
this.container.appendChild(scaleElement);
|
|
||||||
|
|
||||||
return this.container;
|
|
||||||
}
|
|
||||||
|
|
||||||
onRemove() {
|
|
||||||
this.scale.onRemove();
|
|
||||||
this.container.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
AppIcon,
|
AppIcon,
|
||||||
ProjectFeed,
|
ProjectFeed,
|
||||||
ProjectSettings
|
ProjectSettings,
|
||||||
|
ProjectMap,
|
||||||
|
Lightbox
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -51,26 +23,12 @@ export default {
|
|||||||
},
|
},
|
||||||
feed: null,
|
feed: null,
|
||||||
settings: null,
|
settings: null,
|
||||||
track: null,
|
|
||||||
markers: [],
|
|
||||||
markerProps: {
|
|
||||||
project: {mainClasses: 'project', iconMain: 'marker', iconSub: 'project'},
|
|
||||||
image: {mainClasses: 'media', iconMain: 'marker', iconSub: 'image'},
|
|
||||||
video: {mainClasses: 'media', iconMain: 'marker', iconSub: 'video'},
|
|
||||||
message: {mainClasses: 'message', iconMain: 'marker', iconSub: 'footprint', iconSubTransform: 'rotate-270'}
|
|
||||||
},
|
|
||||||
project: null,
|
project: null,
|
||||||
modeHisto: null,
|
modeHisto: null,
|
||||||
baseMaps: [],
|
baseMaps: [],
|
||||||
baseMap: null,
|
baseMap: null,
|
||||||
terrainEnabled: false,
|
terrainEnabled: false,
|
||||||
map: null,
|
|
||||||
mapInitializing: false,
|
mapInitializing: false,
|
||||||
markerHeight: 32, //FIXME
|
|
||||||
mapPadding: 16 + 32, //1rem + marker height
|
|
||||||
maxZoom: 15,
|
|
||||||
initialPitch: 45,
|
|
||||||
lightbox: null,
|
|
||||||
hikes: {
|
hikes: {
|
||||||
colors: {},
|
colors: {},
|
||||||
width: null,
|
width: null,
|
||||||
@@ -78,8 +36,11 @@ export default {
|
|||||||
lineWidthTransition: {duration:null}
|
lineWidthTransition: {duration:null}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
popup: {content: null, element: null},
|
overview: {
|
||||||
overview: {id: 0, codename:'overview', name: this.lang.get('project.overview')},
|
id: 0,
|
||||||
|
codename: 'overview',
|
||||||
|
name: this.lang.get('project.overview')
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -91,17 +52,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
baseMap(sNewBaseMap, sOldBaseMap) {
|
|
||||||
if(this.map?.isStyleLoaded()) {
|
|
||||||
if(sOldBaseMap && this.map.getLayer(sOldBaseMap)) this.map.setLayoutProperty(sOldBaseMap, 'visibility', 'none');
|
|
||||||
if(sNewBaseMap && this.map.getLayer(sNewBaseMap)) this.map.setLayoutProperty(sNewBaseMap, 'visibility', 'visible');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
terrainEnabled(bEnabled) {
|
|
||||||
if(!this.map?.isStyleLoaded()) return;
|
|
||||||
if(bEnabled) this.addTerrain();
|
|
||||||
else this.removeTerrain();
|
|
||||||
},
|
|
||||||
'hash.items.0'(newProjectCodename, oldProjectCodename) { //hash.items.0 = Project Code Name
|
'hash.items.0'(newProjectCodename, oldProjectCodename) { //hash.items.0 = Project Code Name
|
||||||
if(newProjectCodename != oldProjectCodename) {
|
if(newProjectCodename != oldProjectCodename) {
|
||||||
this.hash.items = [newProjectCodename]; //Force removal of direct link
|
this.hash.items = [newProjectCodename]; //Force removal of direct link
|
||||||
@@ -114,14 +64,15 @@ export default {
|
|||||||
return {
|
return {
|
||||||
map: {
|
map: {
|
||||||
panToBetweenPanels: this.panToBetweenPanels,
|
panToBetweenPanels: this.panToBetweenPanels,
|
||||||
openMarkerPopup: this.openMarkerPopup,
|
openMarkerPopup: (iMarkerId, sMarkerType) => this.$refs.mapCtrl.openMarkerPopup(iMarkerId, sMarkerType),
|
||||||
closePopup: this.closePopup,
|
closePopup: () => this.$refs.mapCtrl.closePopup(),
|
||||||
isMarkerVisible: this.isMarkerVisible
|
isMarkerVisible: (oLngLat) => this.$refs.mapCtrl.isMarkerVisible(oLngLat),
|
||||||
|
findMarkerByMediaId: (iMediaId) => this.$refs.mapCtrl.findMarkerByMediaId(iMediaId)
|
||||||
},
|
},
|
||||||
project: this
|
project: this
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
inject: ['api', 'lang', 'hash', 'projects', 'user', 'consts', 'isMobile'],
|
inject: ['api', 'lang', 'hash', 'projects', 'consts', 'isMobile'],
|
||||||
mounted() {
|
mounted() {
|
||||||
//Starts default project init() through watcher
|
//Starts default project init() through watcher
|
||||||
if(this.hash.items.length == 0) {
|
if(this.hash.items.length == 0) {
|
||||||
@@ -130,23 +81,22 @@ export default {
|
|||||||
else this.init();
|
else this.init();
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
this.quit();
|
this.$refs.lightbox.end(true);
|
||||||
|
this.$refs.mapCtrl.destroy();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async init() {
|
async init() {
|
||||||
this.initLightbox();
|
|
||||||
this.hikes.colors = {
|
this.hikes.colors = {
|
||||||
'main': this.getStyleProperty('--track-main'),
|
'main': getStyleProperty('--track-main'),
|
||||||
'off-track': this.getStyleProperty('--track-off-track'),
|
'off-track': getStyleProperty('--track-off-track'),
|
||||||
'hitchhiking': this.getStyleProperty('--track-hitchhiking')
|
'hitchhiking': getStyleProperty('--track-hitchhiking')
|
||||||
};
|
};
|
||||||
this.hikes.width = parseFloat(this.getStyleProperty('--track-width'));
|
this.hikes.width = parseFloat(getStyleProperty('--track-width'));
|
||||||
this.hikes.transitions.lineWidthTransition.duration = parseFloat(this.getStyleProperty('--trans-quick'));
|
this.hikes.transitions.lineWidthTransition.duration = parseFloat(getStyleProperty('--trans-quick'));
|
||||||
|
|
||||||
//Reset values
|
//Reset values
|
||||||
this.track = null;
|
|
||||||
this.project = null;
|
this.project = null;
|
||||||
this.removeMapContent();
|
this.$refs.mapCtrl.removeMapContent();
|
||||||
|
|
||||||
//Build Map
|
//Build Map
|
||||||
this.mapInitializing = true;
|
this.mapInitializing = true;
|
||||||
@@ -154,11 +104,6 @@ export default {
|
|||||||
else await this.initOverview();
|
else await this.initOverview();
|
||||||
this.mapInitializing = false;
|
this.mapInitializing = false;
|
||||||
},
|
},
|
||||||
quit() {
|
|
||||||
this.lightbox.end(true);
|
|
||||||
this.lightbox = null;
|
|
||||||
this.removeMap();
|
|
||||||
},
|
|
||||||
async initOverview() {
|
async initOverview() {
|
||||||
this.modeHisto = true;
|
this.modeHisto = true;
|
||||||
this.hash.items = [this.overview.codename];
|
this.hash.items = [this.overview.codename];
|
||||||
@@ -174,485 +119,70 @@ export default {
|
|||||||
const pMapReady = this.initProjectMap();
|
const pMapReady = this.initProjectMap();
|
||||||
await this.feed.init(pMapReady);
|
await this.feed.init(pMapReady);
|
||||||
},
|
},
|
||||||
initLightbox() {
|
async onLightboxMediaChange(oMedia) {
|
||||||
if(!this.lightbox) {
|
this.hash.items = [this.project.codename, 'media', oMedia.id];
|
||||||
this.lightbox = new Lightbox({
|
if(oMedia.set == 'post-medias') {
|
||||||
alwaysShowNavOnTouchDevices: true,
|
(await this.feed.findPost('media', oMedia.id))?.panMapToMarker();
|
||||||
fadeDuration: parseFloat(this.getStyleProperty('--trans-quick')),
|
if(!this.$refs.lightbox.hasMediaAfterCurrent()) {
|
||||||
imageFadeDuration: parseFloat(this.getStyleProperty('--trans-quick')),
|
await this.feed.getNextFeed();
|
||||||
positionFromTop: 0,
|
this.$refs.lightbox.refreshAlbum();
|
||||||
resizeDuration: parseFloat(this.getStyleProperty('--trans-slow')),
|
}
|
||||||
hasVideo: true,
|
|
||||||
onMediaChange: async (oMedia) => {
|
|
||||||
this.hash.items = [this.project.codename, 'media', oMedia.id];
|
|
||||||
if(oMedia.set == 'post-medias') {
|
|
||||||
(await this.feed.findPost('media', oMedia.id))?.panMapToMarker();
|
|
||||||
if(!this.lightbox.hasMediaAfterCurrent()) {
|
|
||||||
await this.feed.getNextFeed();
|
|
||||||
this.lightbox.refreshAlbum();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClosing: () => {this.hash.items = [this.hash.items[0]];}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onLightboxClosing() {
|
||||||
|
this.hash.items = [this.hash.items[0]];
|
||||||
|
},
|
||||||
async initProjectMap() {
|
async initProjectMap() {
|
||||||
[
|
const [{maps: baseMaps, markers}, track] = await Promise.all([
|
||||||
{maps: this.baseMaps, markers: this.markers},
|
|
||||||
this.track
|
|
||||||
] = await Promise.all([
|
|
||||||
this.api.get('markers', {id_project: this.project.id}),
|
this.api.get('markers', {id_project: this.project.id}),
|
||||||
this.api.getAsset(this.project.geofilepath)
|
this.api.getAsset(this.project.geofilepath)
|
||||||
]);
|
]);
|
||||||
|
this.baseMaps = baseMaps;
|
||||||
|
|
||||||
await this.initMap();
|
await this.$refs.mapCtrl.initMap({
|
||||||
|
project: this.project,
|
||||||
|
hikes: this.hikes,
|
||||||
|
baseMaps,
|
||||||
|
markers,
|
||||||
|
track,
|
||||||
|
padding: this.getMapPadding()
|
||||||
|
});
|
||||||
},
|
},
|
||||||
async initOverviewMap() {
|
async initOverviewMap() {
|
||||||
this.baseMaps = this.consts.default_maps;
|
this.baseMaps = this.consts.default_maps;
|
||||||
this.markers = Object.values(this.projects).map((asProject) => ({
|
const markers = Object.values(this.projects).map((asProject) => ({
|
||||||
type: 'project',
|
type: 'project',
|
||||||
subtype: 'project',
|
subtype: 'project',
|
||||||
...asProject,
|
...asProject,
|
||||||
opacityWhenCovered: 0.3
|
opacityWhenCovered: 0.3
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await this.initMap();
|
await this.$refs.mapCtrl.initMap({
|
||||||
},
|
project: null,
|
||||||
async initMap() {
|
hikes: this.hikes,
|
||||||
//Build map
|
baseMaps: this.baseMaps,
|
||||||
if(!this.map) this.addMap();
|
markers,
|
||||||
this.updateMapPadding();
|
track: null,
|
||||||
|
padding: this.getMapPadding()
|
||||||
//Force wait for load event
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
if(this.map.isStyleLoaded()) resolve();
|
|
||||||
else this.map.once('load', resolve);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.map.resize();
|
|
||||||
this.setInitialProjectCamera();
|
|
||||||
|
|
||||||
//Add content: Base Maps, Tracks, Markers
|
|
||||||
this.addMapContent();
|
|
||||||
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
if(this.map.loaded() && this.map.areTilesLoaded()) resolve();
|
|
||||||
else this.map.once('idle', resolve);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
addMap() {
|
addNewMarkers(aoMarkers) {
|
||||||
this.map = new Map({
|
this.$refs.mapCtrl.addNewMarkers(aoMarkers);
|
||||||
container: 'map',
|
|
||||||
aroundCenter: true,
|
|
||||||
style: {
|
|
||||||
version: 8,
|
|
||||||
projection: {type: 'globe'},
|
|
||||||
sky: {
|
|
||||||
'sky-color': this.getStyleProperty('--space'),
|
|
||||||
'horizon-color': this.getStyleProperty('--horizon'),
|
|
||||||
'sky-horizon-blend': 0.35,
|
|
||||||
'atmosphere-blend': 0.8
|
|
||||||
},
|
|
||||||
sources: {},
|
|
||||||
layers: []
|
|
||||||
},
|
|
||||||
attributionControl: false
|
|
||||||
});
|
|
||||||
this.map.addControl(new GroupedScaleControl({unit: 'metric'}), 'bottom-right');
|
|
||||||
this.map.addControl(new NavigationControl({showZoom: false, visualizePitch: true}), 'bottom-right');
|
|
||||||
},
|
|
||||||
removeMap() {
|
|
||||||
this.removeMapContent();
|
|
||||||
this.map?.remove();
|
|
||||||
this.map = null;
|
|
||||||
},
|
|
||||||
addMapContent() {
|
|
||||||
this.baseMaps.forEach(this.addBaseMap);
|
|
||||||
if(this.terrainEnabled) this.addTerrain();
|
|
||||||
this.addTrack();
|
|
||||||
this.markers.forEach(this.addMarker);
|
|
||||||
},
|
|
||||||
removeMapContent() {
|
|
||||||
if(!this.map) return;
|
|
||||||
|
|
||||||
this.closePopup();
|
|
||||||
this.removeTrack();
|
|
||||||
this.markers.forEach(this.removeMarker);
|
|
||||||
this.removeTerrain();
|
|
||||||
this.baseMaps.forEach(this.removeBaseMap);
|
|
||||||
},
|
|
||||||
addTerrain() {
|
|
||||||
// MapLibre terrain's fog matrix is only implemented for Mercator.
|
|
||||||
this.map.setProjection({type: 'mercator'});
|
|
||||||
if(!this.map.getSource('terrain-dem')) {
|
|
||||||
this.map.addSource('terrain-dem', {
|
|
||||||
type: 'raster-dem',
|
|
||||||
tiles: ['https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png'],
|
|
||||||
tileSize: 256,
|
|
||||||
maxzoom: 15,
|
|
||||||
encoding: 'terrarium'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if(!this.map.getSource('hillshade-dem')) {
|
|
||||||
this.map.addSource('hillshade-dem', {
|
|
||||||
type: 'raster-dem',
|
|
||||||
tiles: ['https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png'],
|
|
||||||
tileSize: 256,
|
|
||||||
maxzoom: 13,
|
|
||||||
encoding: 'terrarium'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if(!this.map.getLayer('terrain-hillshade')) {
|
|
||||||
this.map.addLayer({
|
|
||||||
id: 'terrain-hillshade',
|
|
||||||
type: 'hillshade',
|
|
||||||
source: 'hillshade-dem',
|
|
||||||
paint: {
|
|
||||||
'hillshade-exaggeration': 0.35,
|
|
||||||
'hillshade-shadow-color': '#2d342b',
|
|
||||||
'hillshade-highlight-color': '#ffffff'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.map.setTerrain({source: 'terrain-dem', exaggeration: 1.25});
|
|
||||||
},
|
|
||||||
removeTerrain() {
|
|
||||||
if(!this.map) return;
|
|
||||||
if(this.map.getTerrain()) this.map.setTerrain(null);
|
|
||||||
if(this.map.getLayer('terrain-hillshade')) this.map.removeLayer('terrain-hillshade');
|
|
||||||
if(this.map.getSource('hillshade-dem')) this.map.removeSource('hillshade-dem');
|
|
||||||
if(this.map.getSource('terrain-dem')) this.map.removeSource('terrain-dem');
|
|
||||||
this.map.setProjection({type: 'globe'});
|
|
||||||
},
|
|
||||||
addBaseMap(asBaseMap) {
|
|
||||||
if(asBaseMap.default_map) this.baseMap = asBaseMap.codename;
|
|
||||||
if(this.map.getSource(asBaseMap.codename) && this.map.getLayer(asBaseMap.codename)) return;
|
|
||||||
this.map.addSource(asBaseMap.codename, {
|
|
||||||
type: 'raster',
|
|
||||||
tiles: [asBaseMap.pattern],
|
|
||||||
tileSize: asBaseMap.tile_size
|
|
||||||
});
|
|
||||||
this.map.addLayer({
|
|
||||||
id: asBaseMap.codename,
|
|
||||||
type: 'raster',
|
|
||||||
source: asBaseMap.codename,
|
|
||||||
'layout': {'visibility': asBaseMap.default_map?'visible':'none'},
|
|
||||||
minZoom: asBaseMap.min_zoom,
|
|
||||||
maxZoom: asBaseMap.max_zoom
|
|
||||||
});
|
|
||||||
},
|
|
||||||
removeBaseMap(asBaseMap) {
|
|
||||||
if(this.map.getLayer(asBaseMap.codename)) this.map.removeLayer(asBaseMap.codename);
|
|
||||||
if(this.map.getSource(asBaseMap.codename)) this.map.removeSource(asBaseMap.codename);
|
|
||||||
},
|
|
||||||
addTrack() {
|
|
||||||
if(!this.track) return;
|
|
||||||
|
|
||||||
this.track.features.forEach((oFeature, iFeatureId) => {
|
|
||||||
oFeature.properties.track_id = iFeatureId;
|
|
||||||
});
|
|
||||||
this.map.addSource('track', {
|
|
||||||
'type': 'geojson',
|
|
||||||
'data': this.track
|
|
||||||
});
|
|
||||||
|
|
||||||
//Color mapping
|
|
||||||
let asColorMapping = ['match', ['get', 'type']];
|
|
||||||
for(const [sHikeType, sColor] of Object.entries(this.hikes.colors)) {
|
|
||||||
asColorMapping.push(sHikeType);
|
|
||||||
asColorMapping.push(sColor);
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//Enlarged track (click hit box)
|
|
||||||
this.map.addLayer({
|
|
||||||
'id': 'track-hitbox',
|
|
||||||
'type': 'line',
|
|
||||||
'source': 'track',
|
|
||||||
'paint': {
|
|
||||||
'line-opacity': 0,
|
|
||||||
'line-width': this.hikes.width + this.mapPadding
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.map.on('click', 'track-hitbox', this.openTrackPopup);
|
|
||||||
this.map.on('mouseenter', 'track-hitbox', this.onTrackHover);
|
|
||||||
this.map.on('mouseleave', 'track-hitbox', this.onTrackHover);
|
|
||||||
},
|
|
||||||
removeTrack() {
|
|
||||||
//Over clickable track
|
|
||||||
if(this.map.getLayer('track-hitbox')) {
|
|
||||||
this.map.off('click', 'track-hitbox', this.openTrackPopup);
|
|
||||||
this.map.off('mouseenter', 'track-hitbox', this.onTrackHover);
|
|
||||||
this.map.off('mouseleave', 'track-hitbox', this.onTrackHover);
|
|
||||||
this.map.removeLayer('track-hitbox');
|
|
||||||
}
|
|
||||||
|
|
||||||
//Actual track
|
|
||||||
if(this.map.getLayer('track')) this.map.removeLayer('track');
|
|
||||||
|
|
||||||
//Track source
|
|
||||||
if(this.map.getSource('track')) this.map.removeSource('track');
|
|
||||||
},
|
|
||||||
addMarker(oMarker) {
|
|
||||||
const $Marker = document.createElement('div');
|
|
||||||
oMarker.app = createApp(AppIconStack, this.markerProps[oMarker.subtype]);
|
|
||||||
oMarker.app.mount($Marker);
|
|
||||||
|
|
||||||
oMarker.marker = new Marker({element: $Marker, anchor: 'bottom', opacityWhenCovered: oMarker.opacityWhenCovered ?? 0})
|
|
||||||
.setLngLat([oMarker.longitude, oMarker.latitude])
|
|
||||||
.addTo(this.map);
|
|
||||||
|
|
||||||
const $MarkerElement = oMarker.marker.getElement();
|
|
||||||
$MarkerElement.addEventListener('click', (oEvent) => {this.onMarkerClick(oEvent, oMarker);});
|
|
||||||
$MarkerElement.addEventListener('mouseenter', (oEvent) => {this.onMarkerHover(oEvent, oMarker);});
|
|
||||||
$MarkerElement.addEventListener('mouseleave', (oEvent) => {this.onMarkerHover(oEvent, oMarker);});
|
|
||||||
},
|
|
||||||
removeMarker(oMarker) {
|
|
||||||
if(oMarker.app) {
|
|
||||||
oMarker.app.unmount();
|
|
||||||
delete oMarker.app;
|
|
||||||
}
|
|
||||||
if(oMarker.marker) {
|
|
||||||
oMarker.marker.remove();
|
|
||||||
delete oMarker.marker;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onTrackHover(oEvent) {
|
|
||||||
this.map.getCanvas().style.cursor = (oEvent.type == 'mouseenter')?'pointer':'';
|
|
||||||
},
|
|
||||||
onMarkerClick(oEvent, oMarker) {
|
|
||||||
oEvent.preventDefault();
|
|
||||||
oEvent.stopPropagation();
|
|
||||||
switch (oMarker.type) {
|
|
||||||
case 'project':
|
|
||||||
this.hash.items = [oMarker.codename];
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
this.openMarkerPopup(oMarker.id, oMarker.type);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onMarkerHover(oEvent, oMarker) {
|
|
||||||
switch (oMarker.type) {
|
|
||||||
case 'project':
|
|
||||||
if(oEvent.type == 'mouseenter') this.openProjectPopup(oMarker);
|
|
||||||
else this.closePopup();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openProjectPopup(oProject) {
|
|
||||||
this.openPopup({
|
|
||||||
lnglat: [oProject.longitude, oProject.latitude],
|
|
||||||
options: oProject,
|
|
||||||
offset: [0, -1 * this.markerHeight * this.getStyleProperty('--zoom-scale')]
|
|
||||||
});
|
|
||||||
},
|
|
||||||
openMarkerPopup(iMarkerId, sMarkerType) {
|
|
||||||
let oMarker = this.markers.find((oCandidate) => oCandidate.id == iMarkerId && oCandidate.type == sMarkerType);
|
|
||||||
this.openPopup({
|
|
||||||
lnglat: [oMarker.longitude, oMarker.latitude],
|
|
||||||
options: oMarker,
|
|
||||||
offset: [0, -1 * this.markerHeight * (this.isMobile?this.getStyleProperty('--zoom-scale'):1)]
|
|
||||||
});
|
|
||||||
},
|
|
||||||
openTrackPopup(oEvent) {
|
|
||||||
this.openPopup({
|
|
||||||
lnglat: oEvent.lngLat,
|
|
||||||
options: this.projects.getTrackInfo(oEvent.features[0], this.track, this.lang),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
openPopup({lnglat, options={}, offset=[0, 0]} = {}) {
|
|
||||||
this.closePopup();
|
|
||||||
const $Popup = document.createElement('div');
|
|
||||||
this.popup.element = new Popup({
|
|
||||||
anchor: 'bottom',
|
|
||||||
offset: offset,
|
|
||||||
closeButton: false
|
|
||||||
})
|
|
||||||
.setDOMContent($Popup)
|
|
||||||
.setLngLat(lnglat)
|
|
||||||
.addTo(this.map);
|
|
||||||
|
|
||||||
this.popup.content = createApp(ProjectPopup, {
|
|
||||||
options: options,
|
|
||||||
project: this.project,
|
|
||||||
hikes: this.hikes
|
|
||||||
});
|
|
||||||
this.popup.content
|
|
||||||
.provide('lang', this.lang)
|
|
||||||
.provide('consts', this.consts)
|
|
||||||
.provide('isMobile', this.isMobile)
|
|
||||||
.mount($Popup);
|
|
||||||
},
|
|
||||||
closePopup() {
|
|
||||||
if(this.popup.content) {
|
|
||||||
this.popup.content.unmount();
|
|
||||||
this.popup.content = null;
|
|
||||||
}
|
|
||||||
if(this.popup.element) {
|
|
||||||
this.popup.element.remove();
|
|
||||||
this.popup.element = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setInitialProjectCamera() {
|
|
||||||
let oHashMarker;
|
|
||||||
if(this.hash.items.length == 3) {
|
|
||||||
oHashMarker = this.markers.find((oMarker) => (
|
|
||||||
oMarker.type == this.hash.items[1] &&
|
|
||||||
oMarker.id == this.hash.items[2] &&
|
|
||||||
oMarker.longitude != null &&
|
|
||||||
oMarker.latitude != null
|
|
||||||
)) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let oLastMarker = this.markers.at(-1);
|
|
||||||
|
|
||||||
//Overview map: Center on default project
|
|
||||||
if(!this.project) {
|
|
||||||
//Center on default project
|
|
||||||
const oDefaultProject = this.projects.getDefaultProject();
|
|
||||||
|
|
||||||
//Get Map / Canvas size
|
|
||||||
const $Canvas = this.map.getCanvas();
|
|
||||||
const oMapBounds = this.map.getContainer().getBoundingClientRect();
|
|
||||||
|
|
||||||
//Adapt zoom to see whole planet
|
|
||||||
const iTargetRadius = Math.max(1, Math.min(oMapBounds.width || $Canvas.clientWidth, oMapBounds.height || $Canvas.clientHeight) / 2);
|
|
||||||
const iWorldSize = iTargetRadius * 2 * Math.PI * Math.cos(oDefaultProject.latitude * Math.PI / 180);
|
|
||||||
|
|
||||||
this.map.jumpTo({
|
|
||||||
center: new LngLat(oDefaultProject.longitude, oDefaultProject.latitude),
|
|
||||||
zoom: Math.log2(iWorldSize / 512),
|
|
||||||
pitch: 0,
|
|
||||||
bearing: 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//Direct link to marker
|
|
||||||
else if(oHashMarker) {
|
|
||||||
this.map.jumpTo({
|
|
||||||
center: new LngLat(oHashMarker.longitude, oHashMarker.latitude),
|
|
||||||
zoom: 13,
|
|
||||||
pitch: this.initialPitch
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//Blog Mode: Fit to last marker
|
|
||||||
else if(this.project.mode == this.consts.modes.blog && oLastMarker) {
|
|
||||||
this.map.jumpTo({
|
|
||||||
center: new LngLat(oLastMarker.longitude, oLastMarker.latitude),
|
|
||||||
zoom: this.maxZoom,
|
|
||||||
pitch: this.initialPitch,
|
|
||||||
bearing: 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//Pre Mode, Histo Mode, Blog Mode without markers or missing direct link marker: Fit to track
|
|
||||||
else {
|
|
||||||
let oBounds = new LngLatBounds();
|
|
||||||
const aoTrackCoordinates = [];
|
|
||||||
for(const iFeatureId in this.track.features) {
|
|
||||||
oBounds = this.track.features[iFeatureId].geometry.coordinates.reduce(
|
|
||||||
(bounds, coord) => {
|
|
||||||
aoTrackCoordinates.push(coord);
|
|
||||||
return bounds.extend(coord);
|
|
||||||
},
|
|
||||||
oBounds
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.map.fitBounds(oBounds, {
|
|
||||||
padding: this.mapPadding,
|
|
||||||
animate: false,
|
|
||||||
maxZoom: this.maxZoom,
|
|
||||||
pitch: this.initialPitch,
|
|
||||||
bearing: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
this.fixPitchedCameraCenter(aoTrackCoordinates);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fixPitchedCameraCenter(aoTrackCoordinates) {
|
|
||||||
//Project min/max coords (lat, lng) onto map rectangle corner points (x, y)
|
|
||||||
const oScreenBounds = aoTrackCoordinates.reduce((oBounds, coord) => {
|
|
||||||
const oPoint = this.map.project(coord);
|
|
||||||
return {
|
|
||||||
minX: Math.min(oBounds.minX, oPoint.x),
|
|
||||||
minY: Math.min(oBounds.minY, oPoint.y),
|
|
||||||
maxX: Math.max(oBounds.maxX, oPoint.x),
|
|
||||||
maxY: Math.max(oBounds.maxY, oPoint.y)
|
|
||||||
};
|
|
||||||
}, {
|
|
||||||
minX: Infinity,
|
|
||||||
minY: Infinity,
|
|
||||||
maxX: -Infinity,
|
|
||||||
maxY: -Infinity
|
|
||||||
});
|
|
||||||
|
|
||||||
//Current Rectangle center
|
|
||||||
const oTrackCenter = {
|
|
||||||
x: (oScreenBounds.minX + oScreenBounds.maxX) / 2,
|
|
||||||
y: (oScreenBounds.minY + oScreenBounds.maxY) / 2
|
|
||||||
};
|
|
||||||
|
|
||||||
//Convert back center point (x, y) to coords and Move map to the track center
|
|
||||||
this.map.jumpTo({
|
|
||||||
center: this.map.unproject([
|
|
||||||
oTrackCenter.x,
|
|
||||||
oTrackCenter.y
|
|
||||||
])
|
|
||||||
});
|
|
||||||
},
|
|
||||||
addNewMarkers(aoMarkers) { //FIXME Use its own marker update API
|
|
||||||
this.markers.push(...aoMarkers);
|
|
||||||
aoMarkers.forEach(this.addMarker);
|
|
||||||
},
|
},
|
||||||
panToBetweenPanels(oLngLat, iZoom, iAnimDuration=500) {
|
panToBetweenPanels(oLngLat, iZoom, iAnimDuration=500) {
|
||||||
return new Promise((resolve) => {
|
return this.$refs.mapCtrl.panTo(oLngLat, iZoom, this.getMapPadding(), iAnimDuration);
|
||||||
if(!this.map) {
|
|
||||||
resolve();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.map.once('moveend', resolve);
|
|
||||||
this.map.easeTo({
|
|
||||||
center: oLngLat,
|
|
||||||
zoom: iZoom,
|
|
||||||
padding: this.getMapPadding(),
|
|
||||||
duration: iAnimDuration
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
getMapPadding() {
|
getMapPadding() {
|
||||||
let bIsMobile = this.isMobile();
|
let bIsMobile = this.isMobile();
|
||||||
return {
|
return {
|
||||||
top: this.mapPadding,
|
top: BASE_MAP_PADDING,
|
||||||
bottom: this.mapPadding,
|
bottom: BASE_MAP_PADDING,
|
||||||
left: this.mapPadding + ((!bIsMobile && this.panels.leftOpen && this.settings)?this.settings.getWidth():0),
|
left: BASE_MAP_PADDING + ((!bIsMobile && this.panels.leftOpen && this.settings)?this.settings.getWidth():0),
|
||||||
right: this.mapPadding + ((!bIsMobile && this.panels.rightOpen && this.feed)?this.feed.getWidth():0)
|
right: BASE_MAP_PADDING + ((!bIsMobile && this.panels.rightOpen && this.feed)?this.feed.getWidth():0)
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
updateMapPadding(iDuration=0) {
|
updateMapPadding(iDuration=0) {
|
||||||
const asPadding = this.getMapPadding();
|
this.$refs.mapCtrl.setPadding(this.getMapPadding(), iDuration);
|
||||||
if(iDuration > 0) this.map.easeTo({padding: asPadding, duration: iDuration});
|
|
||||||
else this.map.jumpTo({padding: asPadding});
|
|
||||||
},
|
|
||||||
getStyleProperty(sProperty) {
|
|
||||||
return getComputedStyle(this.$el).getPropertyValue(sProperty).trim();
|
|
||||||
},
|
|
||||||
isMarkerVisible(oLngLat){
|
|
||||||
return !!this.map && this.map.getBounds().contains(oLngLat);
|
|
||||||
},
|
},
|
||||||
onPanelToggle(sPanel, bNewValue, iAnimDuration=500) {
|
onPanelToggle(sPanel, bNewValue, iAnimDuration=500) {
|
||||||
const sPanelKey = sPanel + 'Open';
|
const sPanelKey = sPanel + 'Open';
|
||||||
@@ -661,7 +191,7 @@ export default {
|
|||||||
|
|
||||||
if(bOldValue != bNewValue) {
|
if(bOldValue != bNewValue) {
|
||||||
//Adjust map center
|
//Adjust map center
|
||||||
if(!this.isMobile() && this.map) this.updateMapPadding(iAnimDuration);
|
if(!this.isMobile() && this.$refs.mapCtrl.map) this.updateMapPadding(iAnimDuration);
|
||||||
|
|
||||||
//Open Close panels
|
//Open Close panels
|
||||||
this.$el.classList.toggle('with-'+sPanel+'-panel');
|
this.$el.classList.toggle('with-'+sPanel+'-panel');
|
||||||
@@ -674,7 +204,7 @@ export default {
|
|||||||
this.settings = vPanel;
|
this.settings = vPanel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -685,7 +215,19 @@ export default {
|
|||||||
<AppIcon :icon="'map'" :classes="'flicker'" width="fixed" />
|
<AppIcon :icon="'map'" :classes="'flicker'" width="fixed" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="map"></div>
|
<ProjectMap
|
||||||
|
ref="mapCtrl"
|
||||||
|
v-model:base-map="baseMap"
|
||||||
|
:terrain-enabled="terrainEnabled"
|
||||||
|
/>
|
||||||
|
<Lightbox
|
||||||
|
ref="lightbox"
|
||||||
|
:always-show-nav-on-touch-devices="true"
|
||||||
|
:position-from-top="0"
|
||||||
|
:has-video="true"
|
||||||
|
@media-change="onLightboxMediaChange"
|
||||||
|
@closing="onLightboxClosing"
|
||||||
|
/>
|
||||||
<ProjectSettings
|
<ProjectSettings
|
||||||
:ref="setSettings"
|
:ref="setSettings"
|
||||||
:projects="projectOptions"
|
:projects="projectOptions"
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export default {
|
|||||||
this.api.request(this.user.subscribed?'subscribe':'unsubscribe', {}, 'POST')
|
this.api.request(this.user.subscribed?'subscribe':'unsubscribe', {}, 'POST')
|
||||||
.then((asResponse) => {
|
.then((asResponse) => {
|
||||||
this.user.setInfo(asResponse.data);
|
this.user.setInfo(asResponse.data);
|
||||||
this.feedbacks.push({type:asResponse.result, msg:asResponse.desc});
|
this.feedbacks.push({type:asResponse.result, msg:asResponse.desc_lang_text});
|
||||||
})
|
})
|
||||||
.catch((sDesc) => {
|
.catch((sDesc) => {
|
||||||
this.user.subscribed = !this.user.subscribed;
|
this.user.subscribed = !this.user.subscribed;
|
||||||
@@ -55,7 +55,7 @@ export default {
|
|||||||
manageLogin() {
|
manageLogin() {
|
||||||
if(this.loginLoading) return;
|
if(this.loginLoading) return;
|
||||||
|
|
||||||
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,}))$/;
|
let 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.feedbacks.push({type:'error', 'msg':this.lang.get('account.invalid_email')});
|
if(!regexEmail.test(this.user.email)) this.feedbacks.push({type:'error', 'msg':this.lang.get('account.invalid_email')});
|
||||||
else if(this.settingPassword && this.password !== this.passwordConfirmation) this.feedbacks.push({type:'error', 'msg':this.lang.get('account.password_mismatch')});
|
else if(this.settingPassword && this.password !== this.passwordConfirmation) this.feedbacks.push({type:'error', 'msg':this.lang.get('account.password_mismatch')});
|
||||||
else {
|
else {
|
||||||
@@ -64,7 +64,7 @@ export default {
|
|||||||
|
|
||||||
this.api.request(sAction, {'email': this.user.email, 'password': this.password, 'name': this.user.name}, 'POST')
|
this.api.request(sAction, {'email': this.user.email, 'password': this.password, 'name': this.user.name}, 'POST')
|
||||||
.then((asResponse) => {
|
.then((asResponse) => {
|
||||||
this.feedbacks.push({type: asResponse.result, msg: asResponse.desc});
|
this.feedbacks.push({type: asResponse.result, msg: asResponse.desc_lang_text});
|
||||||
this.user.setInfo(asResponse.data);
|
this.user.setInfo(asResponse.data);
|
||||||
this.passwordMode = false;
|
this.passwordMode = false;
|
||||||
this.settingPassword = false;
|
this.settingPassword = false;
|
||||||
@@ -72,7 +72,7 @@ export default {
|
|||||||
this.passwordConfirmation = '';
|
this.passwordConfirmation = '';
|
||||||
})
|
})
|
||||||
.catch((oError) => {
|
.catch((oError) => {
|
||||||
switch(oError.descKey) {
|
switch(oError.desc_lang_id) {
|
||||||
case 'account.set_password':
|
case 'account.set_password':
|
||||||
this.passwordMode = true;
|
this.passwordMode = true;
|
||||||
this.settingPassword = true;
|
this.settingPassword = true;
|
||||||
@@ -90,7 +90,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import Simplebar from 'simplebar-vue';
|
|||||||
|
|
||||||
import AppIcon from '@components/AppIcon';
|
import AppIcon from '@components/AppIcon';
|
||||||
import ProjectPost from '@components/ProjectPost';
|
import ProjectPost from '@components/ProjectPost';
|
||||||
import { faHistory, faThumbTackSlash } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -216,7 +215,7 @@ export default {
|
|||||||
return this.$el.getBoundingClientRect().width;
|
return this.$el.getBoundingClientRect().width;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -227,7 +226,7 @@ export default {
|
|||||||
<ProjectPost v-else :options="{type: 'poster', relative_time: lang.get('post.new_message')}" />
|
<ProjectPost v-else :options="{type: 'poster', relative_time: lang.get('post.new_message')}" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="project" v-show="!loadingPost" id="feed-posts">
|
<div v-if="project" v-show="!loadingPost" id="feed-posts">
|
||||||
<ProjectPost v-for="post in posts" :options="post" ref="posts" />
|
<ProjectPost v-for="post in posts" :key="post.ref" :options="post" ref="posts" />
|
||||||
</div>
|
</div>
|
||||||
<div id="feed-footer" v-if="loading">
|
<div id="feed-footer" v-if="loading">
|
||||||
<ProjectPost :options="{type: 'loading', headerless: true}" />
|
<ProjectPost :options="{type: 'loading', headerless: true}" />
|
||||||
|
|||||||
@@ -0,0 +1,519 @@
|
|||||||
|
<script>
|
||||||
|
import { Map, Marker, LngLatBounds, LngLat, Popup, ScaleControl, NavigationControl, setWorkerUrl } from 'maplibre-gl';
|
||||||
|
import maplibreWorkerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url';
|
||||||
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||||
|
|
||||||
|
import { createApp } from 'vue';
|
||||||
|
|
||||||
|
import { getStyleProperty } from '@scripts/common';
|
||||||
|
import ProjectPopup from '@components/ProjectPopup';
|
||||||
|
import AppIconStack from '@components/AppIconStack';
|
||||||
|
|
||||||
|
setWorkerUrl(maplibreWorkerUrl);
|
||||||
|
|
||||||
|
//Padding shared with Project.vue's own panel-width padding calculation: 1rem + marker height
|
||||||
|
export const BASE_MAP_PADDING = 16 + 32;
|
||||||
|
|
||||||
|
const MARKER_PROPS = {
|
||||||
|
project: {mainClasses: 'project', iconMain: 'marker', iconSub: 'project'},
|
||||||
|
image: {mainClasses: 'media', iconMain: 'marker', iconSub: 'image'},
|
||||||
|
video: {mainClasses: 'media', iconMain: 'marker', iconSub: 'video'},
|
||||||
|
message: {mainClasses: 'message', iconMain: 'marker', iconSub: 'footprint', iconSubTransform: 'rotate-270'}
|
||||||
|
};
|
||||||
|
|
||||||
|
class GroupedScaleControl {
|
||||||
|
constructor(options) {
|
||||||
|
this.scale = new ScaleControl(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
onAdd(map) {
|
||||||
|
this.container = document.createElement('div');
|
||||||
|
this.container.className = 'maplibregl-ctrl maplibregl-ctrl-group';
|
||||||
|
|
||||||
|
const scaleElement = this.scale.onAdd(map);
|
||||||
|
scaleElement.classList.remove('maplibregl-ctrl');
|
||||||
|
this.container.appendChild(scaleElement);
|
||||||
|
|
||||||
|
return this.container;
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemove() {
|
||||||
|
this.scale.onRemove();
|
||||||
|
this.container.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Owns the MapLibre map instance and everything drawn on it (base maps,
|
||||||
|
//terrain, track, markers, popups). Project.vue keeps the reactive state
|
||||||
|
//its own template/props need (baseMaps, hikes, project...) and drives
|
||||||
|
//this component's imperative API through a template ref, the same way
|
||||||
|
//it would drive a plain class - most of what happens here is imperative
|
||||||
|
//MapLibre calls (addLayer, jumpTo) rather than declarative rendering, so
|
||||||
|
//the <template> is just the map container div.
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
terrainEnabled: Boolean,
|
||||||
|
baseMap: String
|
||||||
|
},
|
||||||
|
emits: ['update:base-map'],
|
||||||
|
inject: ['lang', 'consts', 'isMobile', 'projects', 'hash'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
map: null,
|
||||||
|
baseMaps: [],
|
||||||
|
markers: [],
|
||||||
|
track: null,
|
||||||
|
hikes: null,
|
||||||
|
project: null,
|
||||||
|
popup: {content: null, element: null},
|
||||||
|
markerHeight: 32, //FIXME
|
||||||
|
maxZoom: 15,
|
||||||
|
initialPitch: 45
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
baseMap(sNewBaseMap, sOldBaseMap) {
|
||||||
|
if(!this.map?.isStyleLoaded()) return;
|
||||||
|
if(sOldBaseMap && this.map.getLayer(sOldBaseMap)) this.map.setLayoutProperty(sOldBaseMap, 'visibility', 'none');
|
||||||
|
if(sNewBaseMap && this.map.getLayer(sNewBaseMap)) this.map.setLayoutProperty(sNewBaseMap, 'visibility', 'visible');
|
||||||
|
},
|
||||||
|
terrainEnabled(bEnabled) {
|
||||||
|
if(!this.map?.isStyleLoaded()) return;
|
||||||
|
if(bEnabled) this.addTerrain();
|
||||||
|
else this.removeTerrain();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async initMap({project, hikes, baseMaps, markers, track, padding}) {
|
||||||
|
this.project = project;
|
||||||
|
this.hikes = hikes;
|
||||||
|
this.baseMaps = baseMaps;
|
||||||
|
this.markers = markers;
|
||||||
|
this.track = track;
|
||||||
|
|
||||||
|
if(!this.map) this.addMap();
|
||||||
|
this.setPadding(padding);
|
||||||
|
|
||||||
|
//Force wait for load event
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
if(this.map.isStyleLoaded()) resolve();
|
||||||
|
else this.map.once('load', resolve);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.map.resize();
|
||||||
|
this.setInitialProjectCamera();
|
||||||
|
|
||||||
|
//Add content: Base Maps, Tracks, Markers
|
||||||
|
this.addMapContent();
|
||||||
|
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
if(this.map.loaded() && this.map.areTilesLoaded()) resolve();
|
||||||
|
else this.map.once('idle', resolve);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
addMap() {
|
||||||
|
this.map = new Map({
|
||||||
|
container: this.$el,
|
||||||
|
aroundCenter: true,
|
||||||
|
style: {
|
||||||
|
version: 8,
|
||||||
|
projection: {type: 'globe'},
|
||||||
|
sky: {
|
||||||
|
'sky-color': getStyleProperty('--space'),
|
||||||
|
'horizon-color': getStyleProperty('--horizon'),
|
||||||
|
'sky-horizon-blend': 0.35,
|
||||||
|
'atmosphere-blend': 0.8
|
||||||
|
},
|
||||||
|
sources: {},
|
||||||
|
layers: []
|
||||||
|
},
|
||||||
|
attributionControl: false
|
||||||
|
});
|
||||||
|
this.map.addControl(new GroupedScaleControl({unit: 'metric'}), 'bottom-right');
|
||||||
|
this.map.addControl(new NavigationControl({showZoom: false, visualizePitch: true}), 'bottom-right');
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
this.removeMapContent();
|
||||||
|
this.map?.remove();
|
||||||
|
this.map = null;
|
||||||
|
},
|
||||||
|
addMapContent() {
|
||||||
|
this.baseMaps.forEach(this.addBaseMap);
|
||||||
|
if(this.terrainEnabled) this.addTerrain();
|
||||||
|
this.addTrack();
|
||||||
|
this.markers.forEach(this.addMarker);
|
||||||
|
},
|
||||||
|
removeMapContent() {
|
||||||
|
if(!this.map) return;
|
||||||
|
|
||||||
|
this.closePopup();
|
||||||
|
this.removeTrack();
|
||||||
|
this.markers.forEach(this.removeMarker);
|
||||||
|
this.removeTerrain();
|
||||||
|
this.baseMaps.forEach(this.removeBaseMap);
|
||||||
|
},
|
||||||
|
addTerrain() {
|
||||||
|
// MapLibre terrain's fog matrix is only implemented for Mercator.
|
||||||
|
this.map.setProjection({type: 'mercator'});
|
||||||
|
//One shared raster-dem source feeds both the 3D elevation mesh
|
||||||
|
//(setTerrain) and the hillshade layer below - they used to be two
|
||||||
|
//separate sources pointed at the identical tile URL, which meant
|
||||||
|
//every DEM tile was downloaded and decoded twice per viewport.
|
||||||
|
if(!this.map.getSource('terrain-dem')) {
|
||||||
|
this.map.addSource('terrain-dem', {
|
||||||
|
type: 'raster-dem',
|
||||||
|
tiles: ['https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 15,
|
||||||
|
encoding: 'terrarium'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(!this.map.getLayer('terrain-hillshade')) {
|
||||||
|
this.map.addLayer({
|
||||||
|
id: 'terrain-hillshade',
|
||||||
|
type: 'hillshade',
|
||||||
|
source: 'terrain-dem',
|
||||||
|
paint: {
|
||||||
|
'hillshade-exaggeration': 0.35,
|
||||||
|
'hillshade-shadow-color': '#2d342b',
|
||||||
|
'hillshade-highlight-color': '#ffffff'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.map.setTerrain({source: 'terrain-dem', exaggeration: 1.25});
|
||||||
|
},
|
||||||
|
removeTerrain() {
|
||||||
|
if(!this.map) return;
|
||||||
|
if(this.map.getTerrain()) this.map.setTerrain(null);
|
||||||
|
if(this.map.getLayer('terrain-hillshade')) this.map.removeLayer('terrain-hillshade');
|
||||||
|
if(this.map.getSource('terrain-dem')) this.map.removeSource('terrain-dem');
|
||||||
|
this.map.setProjection({type: 'globe'});
|
||||||
|
},
|
||||||
|
addBaseMap(asBaseMap) {
|
||||||
|
if(asBaseMap.default_map) this.$emit('update:base-map', asBaseMap.codename);
|
||||||
|
if(this.map.getSource(asBaseMap.codename) && this.map.getLayer(asBaseMap.codename)) return;
|
||||||
|
this.map.addSource(asBaseMap.codename, {
|
||||||
|
type: 'raster',
|
||||||
|
tiles: [asBaseMap.pattern],
|
||||||
|
tileSize: asBaseMap.tile_size
|
||||||
|
});
|
||||||
|
this.map.addLayer({
|
||||||
|
id: asBaseMap.codename,
|
||||||
|
type: 'raster',
|
||||||
|
source: asBaseMap.codename,
|
||||||
|
'layout': {'visibility': asBaseMap.default_map?'visible':'none'},
|
||||||
|
minZoom: asBaseMap.min_zoom,
|
||||||
|
maxZoom: asBaseMap.max_zoom
|
||||||
|
});
|
||||||
|
},
|
||||||
|
removeBaseMap(asBaseMap) {
|
||||||
|
if(this.map.getLayer(asBaseMap.codename)) this.map.removeLayer(asBaseMap.codename);
|
||||||
|
if(this.map.getSource(asBaseMap.codename)) this.map.removeSource(asBaseMap.codename);
|
||||||
|
},
|
||||||
|
addTrack() {
|
||||||
|
if(!this.track) return;
|
||||||
|
|
||||||
|
this.track.features.forEach((oFeature, iFeatureId) => {
|
||||||
|
oFeature.properties.track_id = iFeatureId;
|
||||||
|
});
|
||||||
|
this.map.addSource('track', {
|
||||||
|
'type': 'geojson',
|
||||||
|
'data': this.track
|
||||||
|
});
|
||||||
|
|
||||||
|
//Color mapping
|
||||||
|
let asColorMapping = ['match', ['get', 'type']];
|
||||||
|
for(const [sHikeType, sColor] of Object.entries(this.hikes.colors)) {
|
||||||
|
asColorMapping.push(sHikeType);
|
||||||
|
asColorMapping.push(sColor);
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//Enlarged track (click hit box)
|
||||||
|
this.map.addLayer({
|
||||||
|
'id': 'track-hitbox',
|
||||||
|
'type': 'line',
|
||||||
|
'source': 'track',
|
||||||
|
'paint': {
|
||||||
|
'line-opacity': 0,
|
||||||
|
'line-width': this.hikes.width + BASE_MAP_PADDING
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.map.on('click', 'track-hitbox', this.openTrackPopup);
|
||||||
|
this.map.on('mouseenter', 'track-hitbox', this.onTrackHover);
|
||||||
|
this.map.on('mouseleave', 'track-hitbox', this.onTrackHover);
|
||||||
|
},
|
||||||
|
removeTrack() {
|
||||||
|
//Over clickable track
|
||||||
|
if(this.map.getLayer('track-hitbox')) {
|
||||||
|
this.map.off('click', 'track-hitbox', this.openTrackPopup);
|
||||||
|
this.map.off('mouseenter', 'track-hitbox', this.onTrackHover);
|
||||||
|
this.map.off('mouseleave', 'track-hitbox', this.onTrackHover);
|
||||||
|
this.map.removeLayer('track-hitbox');
|
||||||
|
}
|
||||||
|
|
||||||
|
//Actual track
|
||||||
|
if(this.map.getLayer('track')) this.map.removeLayer('track');
|
||||||
|
|
||||||
|
//Track source
|
||||||
|
if(this.map.getSource('track')) this.map.removeSource('track');
|
||||||
|
},
|
||||||
|
addMarker(oMarker) {
|
||||||
|
const $Marker = document.createElement('div');
|
||||||
|
oMarker.app = createApp(AppIconStack, MARKER_PROPS[oMarker.subtype]);
|
||||||
|
oMarker.app.mount($Marker);
|
||||||
|
|
||||||
|
oMarker.marker = new Marker({element: $Marker, anchor: 'bottom', opacityWhenCovered: oMarker.opacityWhenCovered ?? 0})
|
||||||
|
.setLngLat([oMarker.longitude, oMarker.latitude])
|
||||||
|
.addTo(this.map);
|
||||||
|
|
||||||
|
const $MarkerElement = oMarker.marker.getElement();
|
||||||
|
$MarkerElement.addEventListener('click', (oEvent) => {this.onMarkerClick(oEvent, oMarker);});
|
||||||
|
$MarkerElement.addEventListener('mouseenter', (oEvent) => {this.onMarkerHover(oEvent, oMarker);});
|
||||||
|
$MarkerElement.addEventListener('mouseleave', (oEvent) => {this.onMarkerHover(oEvent, oMarker);});
|
||||||
|
},
|
||||||
|
removeMarker(oMarker) {
|
||||||
|
if(oMarker.app) {
|
||||||
|
oMarker.app.unmount();
|
||||||
|
delete oMarker.app;
|
||||||
|
}
|
||||||
|
if(oMarker.marker) {
|
||||||
|
oMarker.marker.remove();
|
||||||
|
delete oMarker.marker;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addNewMarkers(aoMarkers) { //FIXME Use its own marker update API
|
||||||
|
this.markers.push(...aoMarkers);
|
||||||
|
aoMarkers.forEach(this.addMarker);
|
||||||
|
},
|
||||||
|
onTrackHover(oEvent) {
|
||||||
|
this.map.getCanvas().style.cursor = (oEvent.type == 'mouseenter')?'pointer':'';
|
||||||
|
},
|
||||||
|
onMarkerClick(oEvent, oMarker) {
|
||||||
|
oEvent.preventDefault();
|
||||||
|
oEvent.stopPropagation();
|
||||||
|
switch(oMarker.type) {
|
||||||
|
case 'project':
|
||||||
|
this.hash.items = [oMarker.codename];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.openMarkerPopup(oMarker.id, oMarker.type);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onMarkerHover(oEvent, oMarker) {
|
||||||
|
switch(oMarker.type) {
|
||||||
|
case 'project':
|
||||||
|
if(oEvent.type == 'mouseenter') this.openProjectPopup(oMarker);
|
||||||
|
else this.closePopup();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openProjectPopup(oProject) {
|
||||||
|
this.openPopup({
|
||||||
|
lnglat: [oProject.longitude, oProject.latitude],
|
||||||
|
options: oProject,
|
||||||
|
offset: [0, -1 * this.markerHeight * getStyleProperty('--zoom-scale')]
|
||||||
|
});
|
||||||
|
},
|
||||||
|
findMarkerByMediaId(iMediaId) {
|
||||||
|
return this.markers.find((oMarker) => (oMarker.medias || []).some((oMedia) => oMedia.id_media == iMediaId)) || null;
|
||||||
|
},
|
||||||
|
openMarkerPopup(iMarkerId, sMarkerType) {
|
||||||
|
let oMarker = this.markers.find((oCandidate) => oCandidate.id == iMarkerId && oCandidate.type == sMarkerType);
|
||||||
|
this.openPopup({
|
||||||
|
lnglat: [oMarker.longitude, oMarker.latitude],
|
||||||
|
options: oMarker,
|
||||||
|
//NB: `this.isMobile` here is the injected function itself, not a call to it - always
|
||||||
|
//truthy, so this branch always applies. Pre-existing behavior, kept as-is by this split.
|
||||||
|
offset: [0, -1 * this.markerHeight * (this.isMobile?getStyleProperty('--zoom-scale'):1)]
|
||||||
|
});
|
||||||
|
},
|
||||||
|
openTrackPopup(oEvent) {
|
||||||
|
this.openPopup({
|
||||||
|
lnglat: oEvent.lngLat,
|
||||||
|
options: this.projects.getTrackInfo(oEvent.features[0], this.track, this.lang),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
openPopup({lnglat, options={}, offset=[0, 0]} = {}) {
|
||||||
|
this.closePopup();
|
||||||
|
const $Popup = document.createElement('div');
|
||||||
|
this.popup.element = new Popup({
|
||||||
|
anchor: 'bottom',
|
||||||
|
offset: offset,
|
||||||
|
closeButton: false
|
||||||
|
})
|
||||||
|
.setDOMContent($Popup)
|
||||||
|
.setLngLat(lnglat)
|
||||||
|
.addTo(this.map);
|
||||||
|
|
||||||
|
this.popup.content = createApp(ProjectPopup, {
|
||||||
|
options: options,
|
||||||
|
project: this.project,
|
||||||
|
hikes: this.hikes
|
||||||
|
});
|
||||||
|
this.popup.content
|
||||||
|
.provide('lang', this.lang)
|
||||||
|
.provide('consts', this.consts)
|
||||||
|
.provide('isMobile', this.isMobile)
|
||||||
|
.mount($Popup);
|
||||||
|
},
|
||||||
|
closePopup() {
|
||||||
|
if(this.popup.content) {
|
||||||
|
this.popup.content.unmount();
|
||||||
|
this.popup.content = null;
|
||||||
|
}
|
||||||
|
if(this.popup.element) {
|
||||||
|
this.popup.element.remove();
|
||||||
|
this.popup.element = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setInitialProjectCamera() {
|
||||||
|
let oHashMarker;
|
||||||
|
if(this.hash.items.length == 3) {
|
||||||
|
oHashMarker = this.markers.find((oMarker) => (
|
||||||
|
oMarker.type == this.hash.items[1] &&
|
||||||
|
oMarker.id == this.hash.items[2] &&
|
||||||
|
oMarker.longitude != null &&
|
||||||
|
oMarker.latitude != null
|
||||||
|
)) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let oLastMarker = this.markers.at(-1);
|
||||||
|
|
||||||
|
//Overview map: Center on default project
|
||||||
|
if(!this.project) {
|
||||||
|
//Center on default project
|
||||||
|
const oDefaultProject = this.projects.getDefaultProject();
|
||||||
|
|
||||||
|
//Get Map / Canvas size
|
||||||
|
const $Canvas = this.map.getCanvas();
|
||||||
|
const oMapBounds = this.map.getContainer().getBoundingClientRect();
|
||||||
|
|
||||||
|
//Adapt zoom to see whole planet
|
||||||
|
const iTargetRadius = Math.max(1, Math.min(oMapBounds.width || $Canvas.clientWidth, oMapBounds.height || $Canvas.clientHeight) / 2);
|
||||||
|
const iWorldSize = iTargetRadius * 2 * Math.PI * Math.cos(oDefaultProject.latitude * Math.PI / 180);
|
||||||
|
|
||||||
|
this.map.jumpTo({
|
||||||
|
center: new LngLat(oDefaultProject.longitude, oDefaultProject.latitude),
|
||||||
|
zoom: Math.log2(iWorldSize / 512),
|
||||||
|
pitch: 0,
|
||||||
|
bearing: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//Direct link to marker
|
||||||
|
else if(oHashMarker) {
|
||||||
|
this.map.jumpTo({
|
||||||
|
center: new LngLat(oHashMarker.longitude, oHashMarker.latitude),
|
||||||
|
zoom: 13,
|
||||||
|
pitch: this.initialPitch
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//Blog Mode: Fit to last marker
|
||||||
|
else if(this.project.mode == this.consts.modes.blog && oLastMarker) {
|
||||||
|
this.map.jumpTo({
|
||||||
|
center: new LngLat(oLastMarker.longitude, oLastMarker.latitude),
|
||||||
|
zoom: this.maxZoom,
|
||||||
|
pitch: this.initialPitch,
|
||||||
|
bearing: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//Pre Mode, Histo Mode, Blog Mode without markers or missing direct link marker: Fit to track
|
||||||
|
else {
|
||||||
|
let oBounds = new LngLatBounds();
|
||||||
|
const aoTrackCoordinates = [];
|
||||||
|
for(const iFeatureId in this.track.features) {
|
||||||
|
oBounds = this.track.features[iFeatureId].geometry.coordinates.reduce(
|
||||||
|
(bounds, coord) => {
|
||||||
|
aoTrackCoordinates.push(coord);
|
||||||
|
return bounds.extend(coord);
|
||||||
|
},
|
||||||
|
oBounds
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.map.fitBounds(oBounds, {
|
||||||
|
padding: BASE_MAP_PADDING,
|
||||||
|
animate: false,
|
||||||
|
maxZoom: this.maxZoom,
|
||||||
|
pitch: this.initialPitch,
|
||||||
|
bearing: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fixPitchedCameraCenter(aoTrackCoordinates);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fixPitchedCameraCenter(aoTrackCoordinates) {
|
||||||
|
//Project min/max coords (lat, lng) onto map rectangle corner points (x, y)
|
||||||
|
const oScreenBounds = aoTrackCoordinates.reduce((oBounds, coord) => {
|
||||||
|
const oPoint = this.map.project(coord);
|
||||||
|
return {
|
||||||
|
minX: Math.min(oBounds.minX, oPoint.x),
|
||||||
|
minY: Math.min(oBounds.minY, oPoint.y),
|
||||||
|
maxX: Math.max(oBounds.maxX, oPoint.x),
|
||||||
|
maxY: Math.max(oBounds.maxY, oPoint.y)
|
||||||
|
};
|
||||||
|
}, {
|
||||||
|
minX: Infinity,
|
||||||
|
minY: Infinity,
|
||||||
|
maxX: -Infinity,
|
||||||
|
maxY: -Infinity
|
||||||
|
});
|
||||||
|
|
||||||
|
//Current Rectangle center
|
||||||
|
const oTrackCenter = {
|
||||||
|
x: (oScreenBounds.minX + oScreenBounds.maxX) / 2,
|
||||||
|
y: (oScreenBounds.minY + oScreenBounds.maxY) / 2
|
||||||
|
};
|
||||||
|
|
||||||
|
//Convert back center point (x, y) to coords and Move map to the track center
|
||||||
|
this.map.jumpTo({
|
||||||
|
center: this.map.unproject([
|
||||||
|
oTrackCenter.x,
|
||||||
|
oTrackCenter.y
|
||||||
|
])
|
||||||
|
});
|
||||||
|
},
|
||||||
|
panTo(oLngLat, iZoom, padding, iAnimDuration=500) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if(!this.map) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.map.once('moveend', resolve);
|
||||||
|
this.map.easeTo({
|
||||||
|
center: oLngLat,
|
||||||
|
zoom: iZoom,
|
||||||
|
padding: padding,
|
||||||
|
duration: iAnimDuration
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setPadding(padding, iDuration=0) {
|
||||||
|
if(iDuration > 0) this.map.easeTo({padding, duration: iDuration});
|
||||||
|
else this.map.jumpTo({padding});
|
||||||
|
},
|
||||||
|
isMarkerVisible(oLngLat) {
|
||||||
|
return !!this.map && this.map.getBounds().contains(oLngLat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div id="map"></div>
|
||||||
|
</template>
|
||||||
@@ -4,7 +4,7 @@ export default {
|
|||||||
options: Object
|
options: Object
|
||||||
},
|
},
|
||||||
inject: ['lang']
|
inject: ['lang']
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
title:''
|
title:''
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
inject: ['lang', 'isMobile'],
|
inject: ['lang', 'isMobile'],
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -32,7 +32,7 @@ export default {
|
|||||||
this.$refs.link.click();
|
this.$refs.link.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -72,7 +72,7 @@ export default {
|
|||||||
<span ref="postedon" class="lb-caption-line">
|
<span ref="postedon" class="lb-caption-line">
|
||||||
<projectRelTime
|
<projectRelTime
|
||||||
icon="upload"
|
icon="upload"
|
||||||
titleWrapperName="media.posted_on"
|
titleWrapperLangId="media.posted_on"
|
||||||
:localTime="options.posted_on_formatted_time_local"
|
:localTime="options.posted_on_formatted_time_local"
|
||||||
:siteTime="options.posted_on_formatted_time"
|
:siteTime="options.posted_on_formatted_time"
|
||||||
:offset="options.posted_on_day_offset"
|
:offset="options.posted_on_day_offset"
|
||||||
@@ -81,7 +81,7 @@ export default {
|
|||||||
<span ref="takenon" class="lb-caption-line">
|
<span ref="takenon" class="lb-caption-line">
|
||||||
<projectRelTime
|
<projectRelTime
|
||||||
:icon="options.subtype+'-shot'"
|
:icon="options.subtype+'-shot'"
|
||||||
:titleWrapperName="'media.'+options.subtype+'_taken_on'"
|
:titleWrapperLangId="'media.'+options.subtype+'_taken_on'"
|
||||||
:localTime="options.taken_on_formatted_time_local"
|
:localTime="options.taken_on_formatted_time_local"
|
||||||
:siteTime="options.taken_on_formatted_time"
|
:siteTime="options.taken_on_formatted_time"
|
||||||
:offset="options.taken_on_day_offset"
|
:offset="options.taken_on_day_offset"
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default {
|
|||||||
;
|
;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -98,7 +98,7 @@ export default {
|
|||||||
<div v-if="options.medias" class="section medias">
|
<div v-if="options.medias" class="section medias">
|
||||||
<appIcon v-if="options.type=='message'" icon="media" width="fixed" size="lg" :text="lang.get('media.nearby')" />
|
<appIcon v-if="options.type=='message'" icon="media" width="fixed" size="lg" :text="lang.get('media.nearby')" />
|
||||||
<div class="medias-list">
|
<div class="medias-list">
|
||||||
<projectMediaLink v-for="media in options?.medias" :options="media" :type="'marker'" />
|
<projectMediaLink v-for="media in options?.medias" :key="media.id_media" :options="media" :type="'marker'" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -71,11 +71,7 @@
|
|||||||
relatedMarker() {
|
relatedMarker() {
|
||||||
//Find corresponding marker
|
//Find corresponding marker
|
||||||
if(!this.options.longitude && !this.options.latitude && this.options.type == 'media') {
|
if(!this.options.longitude && !this.options.latitude && this.options.type == 'media') {
|
||||||
return this.project.markers.find((marker) => {
|
return this.map.findMarkerByMediaId(this.options.id_media);
|
||||||
return (marker.medias || []).some((media) => {
|
|
||||||
return media.id_media == this.options.id_media;
|
|
||||||
});
|
|
||||||
}) || null;
|
|
||||||
}
|
}
|
||||||
else if(
|
else if(
|
||||||
['message', 'media'].includes(this.options.type)
|
['message', 'media'].includes(this.options.type)
|
||||||
@@ -147,7 +143,7 @@
|
|||||||
this.feed.checkNewFeed();
|
this.feed.checkNewFeed();
|
||||||
this.sending = false;
|
this.sending = false;
|
||||||
})
|
})
|
||||||
.catch((sDesc) => {
|
.catch(() => {
|
||||||
this.sending = false;
|
this.sending = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -159,6 +155,7 @@
|
|||||||
case 'media':
|
case 'media':
|
||||||
this.$refs.medialink.openMedia();
|
this.$refs.medialink.openMedia();
|
||||||
if(this.relatedMarker) return this.openMarkerPopup();
|
if(this.relatedMarker) return this.openMarkerPopup();
|
||||||
|
else return Promise.resolve();
|
||||||
default:
|
default:
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
@@ -168,7 +165,7 @@
|
|||||||
//Auto-adjust text area height
|
//Auto-adjust text area height
|
||||||
if(this.options.type == 'poster') autosize(this.$refs.post);
|
if(this.options.type == 'poster') autosize(this.$refs.post);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default {
|
|||||||
offset: String,
|
offset: String,
|
||||||
classes: String,
|
classes: String,
|
||||||
icon: String,
|
icon: String,
|
||||||
titleWrapperName: String
|
titleWrapperLangId: String
|
||||||
},
|
},
|
||||||
inject: ['lang'],
|
inject: ['lang'],
|
||||||
computed: {
|
computed: {
|
||||||
@@ -21,13 +21,13 @@ export default {
|
|||||||
this.lang.get('time.user', this.siteTime.slice(-5))
|
this.lang.get('time.user', this.siteTime.slice(-5))
|
||||||
+ ((this.offset != '0')?' ('+this.lang.get('unit.day_short')+this.offset+')':'');
|
+ ((this.offset != '0')?' ('+this.lang.get('unit.day_short')+this.offset+')':'');
|
||||||
|
|
||||||
return (this.titleWrapperName)?
|
return (this.titleWrapperLangId)?
|
||||||
this.lang.get(this.titleWrapperName, bDifferentTimeZone?sTime:this.siteTime)
|
this.lang.get(this.titleWrapperLangId, bDifferentTimeZone?sTime:this.siteTime)
|
||||||
:
|
:
|
||||||
(bDifferentTimeZone?sTime:null);
|
(bDifferentTimeZone?sTime:null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export default {
|
|||||||
return this.$el.getBoundingClientRect().width;
|
return this.$el.getBoundingClientRect().width;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -154,7 +154,7 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="project?.id && !isMobile()" id="legend" class="panel-control panel-control-bottom">
|
<div v-if="project?.id && !isMobile()" id="legend" class="panel-control panel-control-bottom">
|
||||||
<div class="panel-control-elem">
|
<div class="panel-control-elem">
|
||||||
<div v-for="(color, hikeType) in hikes.colors" class="track">
|
<div v-for="(color, hikeType) in hikes.colors" :key="hikeType" class="track">
|
||||||
<span class="line" :style="'background-color:'+color+';'"></span>
|
<span class="line" :style="'background-color:'+color+';'"></span>
|
||||||
<span class="desc">{{ lang.get('track.'+hikeType) }}</span>
|
<span class="desc">{{ lang.get('track.'+hikeType) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+22
-14
@@ -22,7 +22,7 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
if(!this.project.editable) {
|
if(!this.project.editable) {
|
||||||
this.logs = [this.lang.get('upload.mode_archived', [this.project.name])];
|
this.addLog('upload.mode_archived', [this.project.name]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +35,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
addLog(sLangId = 'upload.error', asLangParams = []) {
|
||||||
|
this.logs.push({lang_id: sLangId, lang_params: asLangParams});
|
||||||
|
},
|
||||||
initUploader() {
|
initUploader() {
|
||||||
const endpoint = `${this.consts.process_page}?a=upload`;
|
const endpoint = `${this.consts.process_page}?a=upload`;
|
||||||
|
|
||||||
@@ -66,14 +69,14 @@ export default {
|
|||||||
const uploadedFiles = response?.body?.files || [];
|
const uploadedFiles = response?.body?.files || [];
|
||||||
uploadedFiles.forEach((uploadedFile) => {
|
uploadedFiles.forEach((uploadedFile) => {
|
||||||
const hasError = Object.prototype.hasOwnProperty.call(uploadedFile, 'error');
|
const hasError = Object.prototype.hasOwnProperty.call(uploadedFile, 'error');
|
||||||
this.logs.push(hasError ? uploadedFile.error : this.lang.get('upload.success', [uploadedFile.original_name || uploadedFile.name]));
|
if(hasError) this.addLog(uploadedFile.desc_lang_id, uploadedFile.desc_lang_params);
|
||||||
|
else this.addLog('upload.success', [uploadedFile.original_name || uploadedFile.name]);
|
||||||
if(!hasError) this.files.push({...uploadedFile, content: ''});
|
if(!hasError) this.files.push({...uploadedFile, content: ''});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.uppy.on('upload-error', (file, error, response) => {
|
this.uppy.on('upload-error', (file, error, response) => {
|
||||||
const message = response?.body?.error || error?.message || this.lang.get('upload.error');
|
this.addLog(response?.body?.desc_lang_id || 'upload.error', response?.body?.desc_lang_params || []);
|
||||||
this.logs.push(message);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.uppy.on('complete', () => {
|
this.uppy.on('complete', () => {
|
||||||
@@ -90,32 +93,37 @@ export default {
|
|||||||
id: oFile.id,
|
id: oFile.id,
|
||||||
content: oFile.content
|
content: oFile.content
|
||||||
})
|
})
|
||||||
.then((asData) => {this.logs.push(this.lang.get('media.comment_update', asData.filename));})
|
.then((asData) => {this.addLog('media.comment_update', [asData.filename]);})
|
||||||
.catch((sMsgId) => {this.logs.push(this.lang.get(sMsgId));});
|
.catch((oError) => {this.addLog(oError.desc_lang_id, oError.desc_lang_params);});
|
||||||
},
|
},
|
||||||
addPosition() {
|
addPosition() {
|
||||||
if(navigator.geolocation) {
|
if(navigator.geolocation) {
|
||||||
this.logs.push('Determining position...');
|
this.addLog('upload.position.determining');
|
||||||
navigator.geolocation.getCurrentPosition(
|
navigator.geolocation.getCurrentPosition(
|
||||||
(position) => {
|
(position) => {
|
||||||
this.logs.push('Sending position...');
|
this.addLog('upload.position.sending');
|
||||||
this.api.post('add_position', {
|
this.api.post('add_position', {
|
||||||
'latitude': position.coords.latitude,
|
'latitude': position.coords.latitude,
|
||||||
'longitude': position.coords.longitude,
|
'longitude': position.coords.longitude,
|
||||||
'timestamp': Math.round(position.timestamp / 1000)
|
'timestamp': Math.round(position.timestamp / 1000)
|
||||||
})
|
})
|
||||||
.then((asData) => {this.logs.push(this.lang.get('upload.success', [this.lang.get('upload.position.new')]));})
|
.then(() => {this.addLog('upload.position.success');})
|
||||||
.catch((sMsgId) => {this.logs.push(this.lang.get(sMsgId));});
|
.catch((oError) => {this.addLog(oError.desc_lang_id, oError.desc_lang_params);});
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
this.logs.push(error.message);
|
const asErrorLangIds = {
|
||||||
|
1: 'upload.position.permission_denied',
|
||||||
|
2: 'upload.position.unavailable',
|
||||||
|
3: 'upload.position.timeout'
|
||||||
|
};
|
||||||
|
this.addLog(asErrorLangIds[error.code] || 'upload.position.error');
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else this.logs.push('This browser does not support geolocation');
|
else this.addLog('upload.position.unsupported');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div id="upload">
|
<div id="upload">
|
||||||
@@ -144,7 +152,7 @@ export default {
|
|||||||
<AppButton :icon="'marker'" :text="lang.get('upload.position.new')" @click="addPosition()" />
|
<AppButton :icon="'marker'" :text="lang.get('upload.position.new')" @click="addPosition()" />
|
||||||
</div>
|
</div>
|
||||||
<div class="section logs" v-if="logs.length > 0">
|
<div class="section logs" v-if="logs.length > 0">
|
||||||
<p class="log" v-for="log in logs">{{ log }}.</p>
|
<p class="log" v-for="(log, index) in logs" :key="index">{{ lang.get(log.lang_id, log.lang_params) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+9
-8
@@ -10,8 +10,8 @@ export default class Api {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async get(sAction, asParams = {}) {
|
async get(sAction, asParams = {}) {
|
||||||
const response = await this.request(sAction, asParams, 'GET');
|
const oResponse = await this.request(sAction, asParams, 'GET');
|
||||||
return response.data;
|
return oResponse.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAsset(sAssetPath) {
|
async getAsset(sAssetPath) {
|
||||||
@@ -25,8 +25,8 @@ export default class Api {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async post(sAction, asParams = {}) {
|
async post(sAction, asParams = {}) {
|
||||||
const response = await this.request(sAction, asParams, 'POST');
|
const oResponse = await this.request(sAction, asParams, 'POST');
|
||||||
return response.data;
|
return oResponse.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async request(sAction, asParams = {}, method = 'GET') {
|
async request(sAction, asParams = {}, method = 'GET') {
|
||||||
@@ -59,12 +59,13 @@ export default class Api {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const oResponse = await oRequest.json();
|
const oResponse = await oRequest.json();
|
||||||
oResponse.descKey = this.lang.getLangKey(oResponse.desc);
|
oResponse.desc_lang_text = this.lang.get(oResponse.desc_lang_id, oResponse.desc_lang_params);
|
||||||
oResponse.desc = this.lang.parse(oResponse.desc);
|
|
||||||
|
|
||||||
if(oResponse.result == this.errorCode) {
|
if(oResponse.result == this.errorCode) {
|
||||||
const oError = new Error(oResponse.desc);
|
const oError = new Error(oResponse.desc_lang_text);
|
||||||
oError.descKey = oResponse.descKey;
|
oError.desc_lang_id = oResponse.desc_lang_id;
|
||||||
|
oError.desc_lang_params = oResponse.desc_lang_params;
|
||||||
|
oError.desc_lang_text = oResponse.desc_lang_text;
|
||||||
throw oError;
|
throw oError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-3
@@ -1,8 +1,15 @@
|
|||||||
/* Common Functions */
|
/* Common Functions */
|
||||||
|
|
||||||
|
//All the custom properties this reads (--space, --track-*, --trans-*, --zoom-scale...)
|
||||||
|
//are declared on :root (_common.scss), so there's no need to read them from any
|
||||||
|
//specific component's own element - document.documentElement always has them.
|
||||||
|
export function getStyleProperty(sProperty) {
|
||||||
|
return getComputedStyle(document.documentElement).getPropertyValue(sProperty).trim();
|
||||||
|
}
|
||||||
|
|
||||||
export function copyTextToClipboard(text) {
|
export function copyTextToClipboard(text) {
|
||||||
if(!navigator.clipboard) {
|
if(!navigator.clipboard) {
|
||||||
var textArea = document.createElement('textarea');
|
let textArea = document.createElement('textarea');
|
||||||
textArea.value = text;
|
textArea.value = text;
|
||||||
|
|
||||||
// Avoid scrolling to bottom
|
// Avoid scrolling to bottom
|
||||||
@@ -15,9 +22,9 @@ export function copyTextToClipboard(text) {
|
|||||||
textArea.select();
|
textArea.select();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var successful = document.execCommand('copy');
|
let successful = document.execCommand('copy');
|
||||||
if(!successful) console.error('Fallback: Oops, unable to copy', text);
|
if(!successful) console.error('Fallback: Oops, unable to copy', text);
|
||||||
} catch (err) {
|
} catch(err) {
|
||||||
console.error('Fallback: Oops, unable to copy', err);
|
console.error('Fallback: Oops, unable to copy', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
// Font Awesome Free Solid Icons
|
||||||
import {
|
import {
|
||||||
faArrowsRotate,
|
faArrowsRotate,
|
||||||
faBars,
|
faBars,
|
||||||
faCamera,
|
faCamera,
|
||||||
faCarSide,
|
faCarSide,
|
||||||
faChartArea,
|
|
||||||
faCheck,
|
faCheck,
|
||||||
faChevronLeft,
|
faChevronLeft,
|
||||||
faChevronRight,
|
faChevronRight,
|
||||||
@@ -61,7 +61,11 @@ function customIcon(iconName, width, height, hex, path) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO: Use official icons: https://github.com/visualcrossing/WeatherIcons/tree/main/SVG/2nd%20Set%20-%20Monochrome */
|
// Additional Icons
|
||||||
|
const faCommentPen = customIcon('comment-pen', 512, 512, 'f4ae', 'M256 480c141.4 0 256-107.5 256-240S397.4 0 256 0 0 107.5 0 240c0 54.3 19.2 104.3 51.6 144.5L2.8 476.8c-4.8 9-3.3 20 3.6 27.5s17.8 9.8 27.1 5.8l118.4-50.7C183.7 472.6 218.9 480 256 480zM144.4 334.3l12.3-49.4c2.1-8.4 6.5-16.2 12.6-22.3L290.7 141.3c8.5-8.5 20-13.3 32-13.3 25 0 45.3 20.3 45.3 45.3 0 12-4.8 23.5-13.3 32L233.4 326.6c-6.2 6.2-13.9 10.5-22.3 12.6l-49.4 12.3c-1.1 .3-2.3 .4-3.5 .4-7.9 0-14.2-6.4-14.2-14.2 0-1.2 .1-2.3 .4-3.5z');
|
||||||
|
const findMeSpot = customIcon('find-me-spot', 406, 469, 'e900', 'M85.806 195.8c-1-0.8-1.3-2.3-0.6-3.4 11.1-18.2 56.5-85.8 117.3-85.8 49.6 0 90.4 33.4 110.3 53.3 1.2 1.2 2.9 1.9 4.6 1.9s3.4-0.7 4.6-1.9l16.4-16.3c1-1 1.1-2.5 0.2-3.5-5.1-6.1-15.3-17.2-29.8-28.2-31.7-24.1-67.5-36.3-106.3-36.3-79.4 0-129.8 75.4-142.3 96.5-0.8 1.4-2.6 1.7-3.8 0.7l-55.4-43.6c-1-0.8-1.3-2.2-0.7-3.3 5.9-10.8 23-39.4 48.5-63.7 42.8-40.7 95.1-62.2 151.4-62.2 56 0 109.2 18.9 153.9 54.8 27.3 21.9 45.5 43.2 51.3 50.4 0.8 1 0.7 2.5-0.2 3.5l-12.3 12.2c-1.1 1.1-2.9 1-3.8-0.2-7.3-8.9-26.2-30.5-49.7-49.2-41.1-32.7-88-49.2-139.2-49.2-50.9 0-96.5 18.6-135.4 55.4-20.9 19.7-30.4 34.1-35.6 42.3-0.7 1.1-0.5 2.6 0.6 3.5l16.7 13.2c1.2 0.9 2.6 1.4 4.1 1.4 2.2 0 4.2-1.1 5.4-2.9 7.4-10.7 15.9-20.6 26.8-31.1 34.3-33.1 75.7-50.6 119.9-50.6 92 0 151.2 70.7 165.3 89.4 0.8 1.1 0.7 2.5-0.3 3.4l-49.8 49.3c-1.1 1.1-2.8 1-3.8-0.1-15.9-18.1-63-66.5-111.4-66.5-23.6 0-46.6 11.3-68.4 33.5-7.2 7.3-13.9 15.6-19.9 24.4-0.8 1.1-0.5 2.7 0.6 3.6l93.1 73.2c1.1 0.9 1.3 2.5 0.4 3.7l-10.5 13.3c-0.9 1.1-2.5 1.3-3.7 0.4l-108.5-85.3z M205.91 468.9c-56 0-109.2-18.9-153.9-54.8-27.3-21.9-45.5-43.2-51.3-50.4-0.8-1-0.7-2.5 0.2-3.5l12.3-12.2c1.1-1.1 2.9-1 3.8 0.2 7.3 8.9 26.2 30.5 49.7 49.2 41.1 32.7 88 49.2 139.2 49.2 50.9 0 96.5-18.6 135.4-55.4 20.9-19.7 30.4-34.1 35.6-42.3 0.7-1.1 0.5-2.6-0.6-3.5l-16.7-13.2c-1.2-0.9-2.6-1.4-4.1-1.4-2.2 0-4.2 1.1-5.4 2.9-7.4 10.7-15.9 20.6-26.8 31.1-34.3 33.1-75.7 50.6-119.8 50.6-92 0-151.2-70.7-165.3-89.4-0.8-1.1-0.7-2.5 0.3-3.4l49.8-49.3c1.1-1.1 2.8-1 3.8 0.1 15.9 18.1 63 66.5 111.4 66.5 23.6 0 46.6-11.3 68.4-33.5 7.2-7.3 13.9-15.6 19.9-24.4 0.8-1.1 0.5-2.7-0.6-3.6l-93.1-73.2c-1.1-0.9-1.3-2.5-0.4-3.7l10.5-13.3c0.9-1.1 2.5-1.3 3.7-0.4l108.3 85.2c1 0.8 1.3 2.3 0.6 3.4-11.1 18.2-56.5 85.8-117.3 85.8-49.6 0-90.4-33.4-110.3-53.3-1.2-1.2-2.9-1.9-4.6-1.9s-3.4 0.7-4.6 1.9l-16.4 16.3c-1 1-1.1 2.5-0.2 3.5 5.1 6.1 15.3 17.2 29.8 28.2 31.7 24.1 67.5 36.3 106.3 36.3 79.4 0 129.8-75.4 142.3-96.5 0.8-1.4 2.6-1.7 3.8-0.7l55.4 43.6c1 0.8 1.3 2.2 0.7 3.3-5.9 10.8-23 39.4-48.5 63.7-42.6 40.8-95 62.3-151.3 62.3z');
|
||||||
|
|
||||||
|
// Additional Weather Icons (TODO: Use official icons: https://github.com/visualcrossing/WeatherIcons/tree/main/SVG/2nd%20Set%20-%20Monochrome )
|
||||||
const faCloudBoltMoon = customIcon('cloud-bolt-moon', 640, 512, 'f76d', 'M399.1 48.4c34.7 19.6 59.3 54.8 64.2 96.1 38.8 22 64.9 63.6 64.9 111.4 35.6 .6 63.5-10.3 89.7-31.2 3.5-2.8 6.9-5.8 10.1-8.9 4.1-4 5.3-10.1 3-15.3s-7.7-8.4-13.4-7.9c-4 .3-7.9 .4-11.9 .3-55.5-1.9-99.9-47.5-99.9-103.4 0-36.3 18.7-68.3 47.1-86.8 3.3-2.2 6.8-4.1 10.3-5.9 5.1-2.5 8.1-8 7.4-13.7s-4.9-10.3-10.4-11.6c-10.5-2.4-21.2-3.5-31.9-3.5-56.7 0-105.8 32.8-129.3 80.5zM206.8 416L175.5 520.1c-3.6 11.9 5.3 23.9 17.8 23.9 4.6 0 9-1.7 12.4-4.7L346.9 412.9c3.5-3.1 5.5-7.6 5.5-12.4 0-9.2-7.4-16.6-16.6-16.6l-61.8 0 31.2-104.1c3.6-11.9-5.3-23.9-17.8-23.9-4.6 0-9 1.7-12.4 4.7L133.9 387.1c-3.5 3.1-5.5 7.6-5.5 12.4 0 9.2 7.4 16.6 16.6 16.6l61.8 0zm193.5-80.1c44.2 0 80-35.8 80-80 0-39.3-28.4-72.1-65.8-78.7 1.2-5.6 1.9-11.3 1.9-17.2 0-44.2-35.8-80-80-80-17 0-32.8 5.3-45.8 14.4-16.8-27.8-47.3-46.4-82.2-46.4-53.1 0-96.3 44.5-96 97.3-45.4 7.6-80 47.1-80 94.6 0 50 38.3 91.1 87.2 95.6L243.1 225c12.2-10.9 28-17 44.4-17 44.6 0 76.5 43 63.7 85.7l-12.7 42.2 61.8 0z');
|
const faCloudBoltMoon = customIcon('cloud-bolt-moon', 640, 512, 'f76d', 'M399.1 48.4c34.7 19.6 59.3 54.8 64.2 96.1 38.8 22 64.9 63.6 64.9 111.4 35.6 .6 63.5-10.3 89.7-31.2 3.5-2.8 6.9-5.8 10.1-8.9 4.1-4 5.3-10.1 3-15.3s-7.7-8.4-13.4-7.9c-4 .3-7.9 .4-11.9 .3-55.5-1.9-99.9-47.5-99.9-103.4 0-36.3 18.7-68.3 47.1-86.8 3.3-2.2 6.8-4.1 10.3-5.9 5.1-2.5 8.1-8 7.4-13.7s-4.9-10.3-10.4-11.6c-10.5-2.4-21.2-3.5-31.9-3.5-56.7 0-105.8 32.8-129.3 80.5zM206.8 416L175.5 520.1c-3.6 11.9 5.3 23.9 17.8 23.9 4.6 0 9-1.7 12.4-4.7L346.9 412.9c3.5-3.1 5.5-7.6 5.5-12.4 0-9.2-7.4-16.6-16.6-16.6l-61.8 0 31.2-104.1c3.6-11.9-5.3-23.9-17.8-23.9-4.6 0-9 1.7-12.4 4.7L133.9 387.1c-3.5 3.1-5.5 7.6-5.5 12.4 0 9.2 7.4 16.6 16.6 16.6l61.8 0zm193.5-80.1c44.2 0 80-35.8 80-80 0-39.3-28.4-72.1-65.8-78.7 1.2-5.6 1.9-11.3 1.9-17.2 0-44.2-35.8-80-80-80-17 0-32.8 5.3-45.8 14.4-16.8-27.8-47.3-46.4-82.2-46.4-53.1 0-96.3 44.5-96 97.3-45.4 7.6-80 47.1-80 94.6 0 50 38.3 91.1 87.2 95.6L243.1 225c12.2-10.9 28-17 44.4-17 44.6 0 76.5 43 63.7 85.7l-12.7 42.2 61.8 0z');
|
||||||
const faCloudBoltSun = customIcon('cloud-bolt-sun', 576, 512, 'f76e', 'M248.5-31c-4.2-1.7-8.9-1.3-12.6 1.2L176 9.9 116.2-29.8c-3.7-2.5-8.5-2.9-12.6-1.2s-7.2 5.4-8.1 9.8L81.2 49.2 10.8 63.4c-4.4 .9-8.1 3.9-9.8 8.1S-.2 80.4 2.3 84.2L41.9 144 2.3 203.8c-2.5 3.7-2.9 8.5-1.2 12.6s5.4 7.2 9.8 8.1l69.3 14c.5-47.2 26.5-88.2 64.9-110 7.3-60.8 57.3-108.5 119.1-112.3l-7.6-37.4c-.9-4.4-3.9-8.1-8.1-9.8zM128 239.9c0 44.2 35.8 80 80 80l6.7 0 124-111c12.2-10.9 28-17 44.4-17 44.6 0 76.5 43 63.7 85.7l-12.7 42.2 45.8 0c53 0 96-43 96-96 0-47.6-34.6-87-80-94.6 .4-52.8-42.9-97.3-96-97.3-34.9 0-65.4 18.6-82.2 46.4-13-9.1-28.8-14.4-45.8-14.4-44.2 0-80 35.8-80 80 0 5.9 .6 11.7 1.9 17.2-37.4 6.7-65.8 39.4-65.8 78.7zM302.4 400L271.2 504.1c-3.6 11.9 5.3 23.9 17.8 23.9 4.6 0 9-1.7 12.4-4.7L442.5 396.9c3.5-3.1 5.5-7.6 5.5-12.4 0-9.2-7.4-16.6-16.6-16.6l-61.8 0 31.2-104.1c3.6-11.9-5.3-23.9-17.8-23.9-4.6 0-9 1.7-12.4 4.7L229.5 371.1c-3.5 3.1-5.5 7.6-5.5 12.4 0 9.2 7.4 16.6 16.6 16.6l61.8 0z');
|
const faCloudBoltSun = customIcon('cloud-bolt-sun', 576, 512, 'f76e', 'M248.5-31c-4.2-1.7-8.9-1.3-12.6 1.2L176 9.9 116.2-29.8c-3.7-2.5-8.5-2.9-12.6-1.2s-7.2 5.4-8.1 9.8L81.2 49.2 10.8 63.4c-4.4 .9-8.1 3.9-9.8 8.1S-.2 80.4 2.3 84.2L41.9 144 2.3 203.8c-2.5 3.7-2.9 8.5-1.2 12.6s5.4 7.2 9.8 8.1l69.3 14c.5-47.2 26.5-88.2 64.9-110 7.3-60.8 57.3-108.5 119.1-112.3l-7.6-37.4c-.9-4.4-3.9-8.1-8.1-9.8zM128 239.9c0 44.2 35.8 80 80 80l6.7 0 124-111c12.2-10.9 28-17 44.4-17 44.6 0 76.5 43 63.7 85.7l-12.7 42.2 45.8 0c53 0 96-43 96-96 0-47.6-34.6-87-80-94.6 .4-52.8-42.9-97.3-96-97.3-34.9 0-65.4 18.6-82.2 46.4-13-9.1-28.8-14.4-45.8-14.4-44.2 0-80 35.8-80 80 0 5.9 .6 11.7 1.9 17.2-37.4 6.7-65.8 39.4-65.8 78.7zM302.4 400L271.2 504.1c-3.6 11.9 5.3 23.9 17.8 23.9 4.6 0 9-1.7 12.4-4.7L442.5 396.9c3.5-3.1 5.5-7.6 5.5-12.4 0-9.2-7.4-16.6-16.6-16.6l-61.8 0 31.2-104.1c3.6-11.9-5.3-23.9-17.8-23.9-4.6 0-9 1.7-12.4 4.7L229.5 371.1c-3.5 3.1-5.5 7.6-5.5 12.4 0 9.2 7.4 16.6 16.6 16.6l61.8 0z');
|
||||||
const faCloudFog = customIcon('cloud-fog', 576, 512, 'f74e', 'M32 224c0 53 43 96 96 96l320 0c53 0 96-43 96-96s-43-96-96-96c-.5 0-1.1 0-1.6 0 1.1-5.2 1.6-10.5 1.6-16 0-44.2-35.8-80-80-80-24.3 0-46.1 10.9-60.8 28-18.7-35.7-56.1-60-99.2-60-61.9 0-112 50.1-112 112 0 7.1 .7 14.1 1.9 20.8-38.3 12.6-65.9 48.7-65.9 91.2zM512 392c0-13.3-10.7-24-24-24L24 368c-13.3 0-24 10.7-24 24s10.7 24 24 24l464 0c13.3 0 24-10.7 24-24zM88 464c-13.3 0-24 10.7-24 24s10.7 24 24 24l80 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-80 0zm176 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l288 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-288 0z');
|
const faCloudFog = customIcon('cloud-fog', 576, 512, 'f74e', 'M32 224c0 53 43 96 96 96l320 0c53 0 96-43 96-96s-43-96-96-96c-.5 0-1.1 0-1.6 0 1.1-5.2 1.6-10.5 1.6-16 0-44.2-35.8-80-80-80-24.3 0-46.1 10.9-60.8 28-18.7-35.7-56.1-60-99.2-60-61.9 0-112 50.1-112 112 0 7.1 .7 14.1 1.9 20.8-38.3 12.6-65.9 48.7-65.9 91.2zM512 392c0-13.3-10.7-24-24-24L24 368c-13.3 0-24 10.7-24 24s10.7 24 24 24l464 0c13.3 0 24-10.7 24-24zM88 464c-13.3 0-24 10.7-24 24s10.7 24 24 24l80 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-80 0zm176 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l288 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-288 0z');
|
||||||
@@ -69,10 +73,9 @@ const faCloudHail = customIcon('cloud-hail', 512, 512, 'f739', 'M96 320c-53 0-96
|
|||||||
const faCloudSleet = customIcon('cloud-sleet', 512, 512, 'f741', 'M96 320c-53 0-96-43-96-96 0-42.5 27.6-78.6 65.9-91.2-1.3-6.7-1.9-13.7-1.9-20.8 0-61.9 50.1-112 112-112 43.1 0 80.5 24.3 99.2 60 14.7-17.1 36.5-28 60.8-28 44.2 0 80 35.8 80 80 0 5.5-.6 10.8-1.6 16 .5 0 1.1 0 1.6 0 53 0 96 43 96 96s-43 96-96 96L96 320zm80 48c13.3 0 24 10.7 24 24l0 16 16 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-16 0 0 16c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-16-16 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l16 0 0-16c0-13.3 10.7-24 24-24zm272 24l0 16 16 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-16 0 0 16c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-16-16 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l16 0 0-16c0-13.3 10.7-24 24-24s24 10.7 24 24zM86.8 399.6l-32 96C50.6 508.2 37 515 24.4 510.8S5 493 9.2 480.4l32-96C45.4 371.8 59 365 71.6 369.2S91 387 86.8 399.6zm248 0l-32 96c-4.2 12.6-17.8 19.4-30.4 15.2S253 493 257.2 480.4l32-96c4.2-12.6 17.8-19.4 30.4-15.2S339 387 334.8 399.6z');
|
const faCloudSleet = customIcon('cloud-sleet', 512, 512, 'f741', 'M96 320c-53 0-96-43-96-96 0-42.5 27.6-78.6 65.9-91.2-1.3-6.7-1.9-13.7-1.9-20.8 0-61.9 50.1-112 112-112 43.1 0 80.5 24.3 99.2 60 14.7-17.1 36.5-28 60.8-28 44.2 0 80 35.8 80 80 0 5.5-.6 10.8-1.6 16 .5 0 1.1 0 1.6 0 53 0 96 43 96 96s-43 96-96 96L96 320zm80 48c13.3 0 24 10.7 24 24l0 16 16 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-16 0 0 16c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-16-16 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l16 0 0-16c0-13.3 10.7-24 24-24zm272 24l0 16 16 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-16 0 0 16c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-16-16 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l16 0 0-16c0-13.3 10.7-24 24-24s24 10.7 24 24zM86.8 399.6l-32 96C50.6 508.2 37 515 24.4 510.8S5 493 9.2 480.4l32-96C45.4 371.8 59 365 71.6 369.2S91 387 86.8 399.6zm248 0l-32 96c-4.2 12.6-17.8 19.4-30.4 15.2S253 493 257.2 480.4l32-96c4.2-12.6 17.8-19.4 30.4-15.2S339 387 334.8 399.6z');
|
||||||
const faCloudSnow = customIcon('cloud-snow', 512, 512, 'f742', 'M96 320c-53 0-96-43-96-96 0-42.5 27.6-78.6 65.9-91.2-1.3-6.7-1.9-13.7-1.9-20.8 0-61.9 50.1-112 112-112 43.1 0 80.5 24.3 99.2 60 14.7-17.1 36.5-28 60.8-28 44.2 0 80 35.8 80 80 0 5.5-.6 10.8-1.6 16 .5 0 1.1 0 1.6 0 53 0 96 43 96 96s-43 96-96 96L96 320zm0 72l0 16 16 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-16 0 0 16c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-16-16 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l16 0 0-16c0-13.3 10.7-24 24-24s24 10.7 24 24zm184 32l0 16 16 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-16 0 0 16c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-16-16 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l16 0 0-16c0-13.3 10.7-24 24-24s24 10.7 24 24zm160-56c13.3 0 24 10.7 24 24l0 16 16 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-16 0 0 16c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-16-16 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l16 0 0-16c0-13.3 10.7-24 24-24z');
|
const faCloudSnow = customIcon('cloud-snow', 512, 512, 'f742', 'M96 320c-53 0-96-43-96-96 0-42.5 27.6-78.6 65.9-91.2-1.3-6.7-1.9-13.7-1.9-20.8 0-61.9 50.1-112 112-112 43.1 0 80.5 24.3 99.2 60 14.7-17.1 36.5-28 60.8-28 44.2 0 80 35.8 80 80 0 5.5-.6 10.8-1.6 16 .5 0 1.1 0 1.6 0 53 0 96 43 96 96s-43 96-96 96L96 320zm0 72l0 16 16 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-16 0 0 16c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-16-16 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l16 0 0-16c0-13.3 10.7-24 24-24s24 10.7 24 24zm184 32l0 16 16 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-16 0 0 16c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-16-16 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l16 0 0-16c0-13.3 10.7-24 24-24s24 10.7 24 24zm160-56c13.3 0 24 10.7 24 24l0 16 16 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-16 0 0 16c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-16-16 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l16 0 0-16c0-13.3 10.7-24 24-24z');
|
||||||
const faClouds = customIcon('clouds', 576, 512, 'e47d', 'M112.4 276.4c-5.6 3.5-11 7.4-16 11.6l-.4 0c-53 0-96-43-96-96S43 96 96 96l1.1 0c7.8-54.3 54.4-96 110.9-96 47.9 0 88.8 30.1 104.8 72.4 12-5.4 25.2-8.4 39.2-8.4 53 0 96 43 96 96 0 1.3 0 2.7-.1 4-10.2-2.6-20.9-4-31.9-4-14 0-27.4 2.2-40 6.4-27.9-23.9-64.3-38.4-104-38.4-84.4 0-153.6 65.4-159.6 148.4zM184 480c-48.6 0-88-39.4-88-88 0-40.9 27.8-75.2 65.6-85.1-1-6.1-1.6-12.4-1.6-18.9 0-61.9 50.1-112 112-112 39 0 73.3 19.9 93.3 50.1 13.8-11.3 31.4-18.1 50.7-18.1 44.2 0 80 35.8 80 80 0 .4 0 .9 0 1.3 45.4 7.6 80 47.1 80 94.7 0 53-43 96-96 96l-296 0z');
|
const faClouds = customIcon('clouds', 576, 512, 'e47d', 'M112.4 276.4c-5.6 3.5-11 7.4-16 11.6l-.4 0c-53 0-96-43-96-96S43 96 96 96l1.1 0c7.8-54.3 54.4-96 110.9-96 47.9 0 88.8 30.1 104.8 72.4 12-5.4 25.2-8.4 39.2-8.4 53 0 96 43 96 96 0 1.3 0 2.7-.1 4-10.2-2.6-20.9-4-31.9-4-14 0-27.4 2.2-40 6.4-27.9-23.9-64.3-38.4-104-38.4-84.4 0-153.6 65.4-159.6 148.4zM184 480c-48.6 0-88-39.4-88-88 0-40.9 27.8-75.2 65.6-85.1-1-6.1-1.6-12.4-1.6-18.9 0-61.9 50.1-112 112-112 39 0 73.3 19.9 93.3 50.1 13.8-11.3 31.4-18.1 50.7-18.1 44.2 0 80 35.8 80 80 0 .4 0 .9 0 1.3 45.4 7.6 80 47.1 80 94.7 0 53-43 96-96 96l-296 0z');
|
||||||
const faCommentPen = customIcon('comment-pen', 512, 512, 'f4ae', 'M256 480c141.4 0 256-107.5 256-240S397.4 0 256 0 0 107.5 0 240c0 54.3 19.2 104.3 51.6 144.5L2.8 476.8c-4.8 9-3.3 20 3.6 27.5s17.8 9.8 27.1 5.8l118.4-50.7C183.7 472.6 218.9 480 256 480zM144.4 334.3l12.3-49.4c2.1-8.4 6.5-16.2 12.6-22.3L290.7 141.3c8.5-8.5 20-13.3 32-13.3 25 0 45.3 20.3 45.3 45.3 0 12-4.8 23.5-13.3 32L233.4 326.6c-6.2 6.2-13.9 10.5-22.3 12.6l-49.4 12.3c-1.1 .3-2.3 .4-3.5 .4-7.9 0-14.2-6.4-14.2-14.2 0-1.2 .1-2.3 .4-3.5z');
|
|
||||||
const faMoonStars = customIcon('moon-stars', 512, 512, 'f755', 'M439.8 89.8c1 3.6 4.4 6.2 8.2 6.2s7.1-2.5 8.2-6.2l11-38.6 38.6-11c3.6-1 6.2-4.4 6.2-8.2s-2.5-7.1-6.2-8.2l-38.6-11-11-38.6c-1-3.6-4.4-6.2-8.2-6.2s-7.1 2.5-8.2 6.2l-11 38.6-38.6 11c-3.6 1-6.2 4.4-6.2 8.2s2.5 7.1 6.2 8.2l38.6 11 11 38.6zM224 64C100.3 64 0 164.3 0 288S100.3 512 224 512c60.2 0 114.9-23.8 155.1-62.4 6.4-6.1 8.2-15.7 4.6-23.8s-12-13-20.8-12.3c-4.3 .3-8.6 .5-12.9 .5-88.9 0-161-72.1-161-161 0-63.1 36.3-117.8 89.3-144.2 7.9-4 12.6-12.5 11.5-21.3s-7.6-16-16.2-18C257.6 65.9 241 64 224 64zM355.2 268.8l16.6 58c1.6 5.5 6.6 9.2 12.2 9.2s10.7-3.8 12.2-9.2l16.6-58 58-16.6c5.5-1.6 9.2-6.6 9.2-12.2s-3.8-10.7-9.2-12.2l-58-16.6-16.6-58c-1.6-5.5-6.6-9.2-12.2-9.2s-10.7 3.8-12.2 9.2l-16.6 58-58 16.6c-5.5 1.6-9.2 6.6-9.2 12.2s3.8 10.7 9.2 12.2l58 16.6z');
|
const faMoonStars = customIcon('moon-stars', 512, 512, 'f755', 'M439.8 89.8c1 3.6 4.4 6.2 8.2 6.2s7.1-2.5 8.2-6.2l11-38.6 38.6-11c3.6-1 6.2-4.4 6.2-8.2s-2.5-7.1-6.2-8.2l-38.6-11-11-38.6c-1-3.6-4.4-6.2-8.2-6.2s-7.1 2.5-8.2 6.2l-11 38.6-38.6 11c-3.6 1-6.2 4.4-6.2 8.2s2.5 7.1 6.2 8.2l38.6 11 11 38.6zM224 64C100.3 64 0 164.3 0 288S100.3 512 224 512c60.2 0 114.9-23.8 155.1-62.4 6.4-6.1 8.2-15.7 4.6-23.8s-12-13-20.8-12.3c-4.3 .3-8.6 .5-12.9 .5-88.9 0-161-72.1-161-161 0-63.1 36.3-117.8 89.3-144.2 7.9-4 12.6-12.5 11.5-21.3s-7.6-16-16.2-18C257.6 65.9 241 64 224 64zM355.2 268.8l16.6 58c1.6 5.5 6.6 9.2 12.2 9.2s10.7-3.8 12.2-9.2l16.6-58 58-16.6c5.5-1.6 9.2-6.6 9.2-12.2s-3.8-10.7-9.2-12.2l-58-16.6-16.6-58c-1.6-5.5-6.6-9.2-12.2-9.2s-10.7 3.8-12.2 9.2l-16.6 58-58 16.6c-5.5 1.6-9.2 6.6-9.2 12.2s3.8 10.7 9.2 12.2l58 16.6z');
|
||||||
const findMeSpot = customIcon('find-me-spot', 406, 469, 'e900', 'M85.806 195.8c-1-0.8-1.3-2.3-0.6-3.4 11.1-18.2 56.5-85.8 117.3-85.8 49.6 0 90.4 33.4 110.3 53.3 1.2 1.2 2.9 1.9 4.6 1.9s3.4-0.7 4.6-1.9l16.4-16.3c1-1 1.1-2.5 0.2-3.5-5.1-6.1-15.3-17.2-29.8-28.2-31.7-24.1-67.5-36.3-106.3-36.3-79.4 0-129.8 75.4-142.3 96.5-0.8 1.4-2.6 1.7-3.8 0.7l-55.4-43.6c-1-0.8-1.3-2.2-0.7-3.3 5.9-10.8 23-39.4 48.5-63.7 42.8-40.7 95.1-62.2 151.4-62.2 56 0 109.2 18.9 153.9 54.8 27.3 21.9 45.5 43.2 51.3 50.4 0.8 1 0.7 2.5-0.2 3.5l-12.3 12.2c-1.1 1.1-2.9 1-3.8-0.2-7.3-8.9-26.2-30.5-49.7-49.2-41.1-32.7-88-49.2-139.2-49.2-50.9 0-96.5 18.6-135.4 55.4-20.9 19.7-30.4 34.1-35.6 42.3-0.7 1.1-0.5 2.6 0.6 3.5l16.7 13.2c1.2 0.9 2.6 1.4 4.1 1.4 2.2 0 4.2-1.1 5.4-2.9 7.4-10.7 15.9-20.6 26.8-31.1 34.3-33.1 75.7-50.6 119.9-50.6 92 0 151.2 70.7 165.3 89.4 0.8 1.1 0.7 2.5-0.3 3.4l-49.8 49.3c-1.1 1.1-2.8 1-3.8-0.1-15.9-18.1-63-66.5-111.4-66.5-23.6 0-46.6 11.3-68.4 33.5-7.2 7.3-13.9 15.6-19.9 24.4-0.8 1.1-0.5 2.7 0.6 3.6l93.1 73.2c1.1 0.9 1.3 2.5 0.4 3.7l-10.5 13.3c-0.9 1.1-2.5 1.3-3.7 0.4l-108.5-85.3z M205.91 468.9c-56 0-109.2-18.9-153.9-54.8-27.3-21.9-45.5-43.2-51.3-50.4-0.8-1-0.7-2.5 0.2-3.5l12.3-12.2c1.1-1.1 2.9-1 3.8 0.2 7.3 8.9 26.2 30.5 49.7 49.2 41.1 32.7 88 49.2 139.2 49.2 50.9 0 96.5-18.6 135.4-55.4 20.9-19.7 30.4-34.1 35.6-42.3 0.7-1.1 0.5-2.6-0.6-3.5l-16.7-13.2c-1.2-0.9-2.6-1.4-4.1-1.4-2.2 0-4.2 1.1-5.4 2.9-7.4 10.7-15.9 20.6-26.8 31.1-34.3 33.1-75.7 50.6-119.8 50.6-92 0-151.2-70.7-165.3-89.4-0.8-1.1-0.7-2.5 0.3-3.4l49.8-49.3c1.1-1.1 2.8-1 3.8 0.1 15.9 18.1 63 66.5 111.4 66.5 23.6 0 46.6-11.3 68.4-33.5 7.2-7.3 13.9-15.6 19.9-24.4 0.8-1.1 0.5-2.7-0.6-3.6l-93.1-73.2c-1.1-0.9-1.3-2.5-0.4-3.7l10.5-13.3c0.9-1.1 2.5-1.3 3.7-0.4l108.3 85.2c1 0.8 1.3 2.3 0.6 3.4-11.1 18.2-56.5 85.8-117.3 85.8-49.6 0-90.4-33.4-110.3-53.3-1.2-1.2-2.9-1.9-4.6-1.9s-3.4 0.7-4.6 1.9l-16.4 16.3c-1 1-1.1 2.5-0.2 3.5 5.1 6.1 15.3 17.2 29.8 28.2 31.7 24.1 67.5 36.3 106.3 36.3 79.4 0 129.8-75.4 142.3-96.5 0.8-1.4 2.6-1.7 3.8-0.7l55.4 43.6c1 0.8 1.3 2.2 0.7 3.3-5.9 10.8-23 39.4-48.5 63.7-42.6 40.8-95 62.3-151.3 62.3z');
|
|
||||||
|
|
||||||
|
// Mapping
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
/* Navigation */
|
/* Navigation */
|
||||||
menu: faBars,
|
menu: faBars,
|
||||||
@@ -95,7 +98,6 @@ const ICONS = {
|
|||||||
'main': faPersonHiking,
|
'main': faPersonHiking,
|
||||||
'hitchhiking': faCarSide,
|
'hitchhiking': faCarSide,
|
||||||
layers: faLayerGroup,
|
layers: faLayerGroup,
|
||||||
'elev-chart': faChartArea,
|
|
||||||
distance: faCircleRight,
|
distance: faCircleRight,
|
||||||
'elev-drop': faCircleDown,
|
'elev-drop': faCircleDown,
|
||||||
'elev-gain': faCircleUp,
|
'elev-gain': faCircleUp,
|
||||||
|
|||||||
+6
-15
@@ -5,29 +5,20 @@ export default class Lang {
|
|||||||
this.prefix = prefix;
|
this.prefix = prefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
get(key = '', params = []) {
|
get(sLangId = '', params = []) {
|
||||||
if(key === '') return '';
|
if(sLangId === '') return '';
|
||||||
|
|
||||||
const normalizedParams = Array.isArray(params) ? params : [params];
|
const normalizedParams = Array.isArray(params) ? params : [params];
|
||||||
|
|
||||||
if(Object.prototype.hasOwnProperty.call(this.translations, key)) {
|
if(Object.prototype.hasOwnProperty.call(this.translations, sLangId)) {
|
||||||
let text = this.translations[key];
|
let text = this.translations[sLangId];
|
||||||
normalizedParams.forEach((param, index) => {
|
normalizedParams.forEach((param, index) => {
|
||||||
text = text.replace('$' + index, param);
|
text = text.replace('$' + index, param);
|
||||||
});
|
});
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn('Missing translation:', key);
|
console.warn('Missing translation:', sLangId);
|
||||||
return key;
|
return sLangId;
|
||||||
}
|
|
||||||
|
|
||||||
parse(message = '') {
|
|
||||||
let sLangKey = this.getLangKey(message);
|
|
||||||
return (sLangKey != '')?this.get(sLangKey):message;
|
|
||||||
}
|
|
||||||
|
|
||||||
getLangKey(message = '') {
|
|
||||||
return (this.prefix && typeof message === 'string' && message.startsWith(this.prefix))?message.slice(this.prefix.length):'';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,650 +0,0 @@
|
|||||||
import { icon } from '@fortawesome/fontawesome-svg-core';
|
|
||||||
import { getIcon } from '@scripts/icons';
|
|
||||||
|
|
||||||
export default class Lightbox {
|
|
||||||
constructor(options = {}) {
|
|
||||||
this.album = [];
|
|
||||||
this.currentImageIndex = 0;
|
|
||||||
this.options = {
|
|
||||||
alwaysShowNavOnTouchDevices: false,
|
|
||||||
fadeDuration: 600,
|
|
||||||
imageFadeDuration: 600,
|
|
||||||
positionFromTop: 50,
|
|
||||||
resizeDuration: 700,
|
|
||||||
wrapAround: false,
|
|
||||||
disableScrolling: false,
|
|
||||||
sanitizeTitle: false,
|
|
||||||
hasVideo: true,
|
|
||||||
onMediaChange: () => {},
|
|
||||||
onClosing: () => {}
|
|
||||||
};
|
|
||||||
this.option(options);
|
|
||||||
this.gMouseDownOffsetX = 0;
|
|
||||||
this.gMouseDownOffsetY = 0;
|
|
||||||
this.resizeTimer = null;
|
|
||||||
this.boundOnBodyClick = this.onBodyClick.bind(this);
|
|
||||||
this.boundOnResize = this.sizeOverlay.bind(this);
|
|
||||||
this.boundOnKeyUp = this.keyboardAction.bind(this);
|
|
||||||
this.boundOnWheel = this.onWheel.bind(this);
|
|
||||||
this.boundOnDragStart = this.onDragStart.bind(this);
|
|
||||||
this.boundOnDragMove = this.onDragMove.bind(this);
|
|
||||||
this.boundOnDragEnd = this.onDragEnd.bind(this);
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
option(options = {}) {
|
|
||||||
Object.assign(this.options, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
this.build();
|
|
||||||
this.enable();
|
|
||||||
}, { once: true });
|
|
||||||
} else {
|
|
||||||
this.build();
|
|
||||||
this.enable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enable() {
|
|
||||||
document.body.addEventListener('click', this.boundOnBodyClick);
|
|
||||||
}
|
|
||||||
|
|
||||||
disable() {
|
|
||||||
document.body.removeEventListener('click', this.boundOnBodyClick);
|
|
||||||
}
|
|
||||||
|
|
||||||
onBodyClick(event) {
|
|
||||||
const link = event.target.closest('a[data-lightbox], area[data-lightbox]');
|
|
||||||
if (!link) return;
|
|
||||||
event.preventDefault();
|
|
||||||
this.start(link);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderIcon(name, sClass=null) {
|
|
||||||
return icon(getIcon(name), {classes: ['app-icon', name, sClass]}).html;
|
|
||||||
}
|
|
||||||
|
|
||||||
build() {
|
|
||||||
if (!document.getElementById('lightbox')) {
|
|
||||||
const wrapper = document.createElement('div');
|
|
||||||
wrapper.innerHTML = `
|
|
||||||
<div id="lightboxOverlay" tabindex="-1" class="lightboxOverlay"></div>
|
|
||||||
<div id="lightbox" tabindex="-1" class="lightbox">
|
|
||||||
<div class="lb-outerContainer">
|
|
||||||
<div class="lb-container">
|
|
||||||
<img class="lb-image" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" alt="" />
|
|
||||||
<video class="lb-video" controls autoplay></video>
|
|
||||||
<div class="lb-nav">
|
|
||||||
<div class="lb-prev-area">
|
|
||||||
<a class="lb-prev" aria-label="Previous image" href="" role="button">${this.renderIcon('prev')}</a>
|
|
||||||
</div>
|
|
||||||
<div class="lb-next-area">
|
|
||||||
<a class="lb-next" aria-label="Next image" href="" role="button">${this.renderIcon('next')}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="lb-loader">
|
|
||||||
<a class="lb-cancel" href="#">${this.renderIcon('cancel')}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="lb-dataContainer desktop">
|
|
||||||
<div class="lb-data">
|
|
||||||
<div class="lb-details">
|
|
||||||
<span class="lb-caption"></span>
|
|
||||||
</div>
|
|
||||||
<div class="lb-closeContainer">
|
|
||||||
<a class="lb-close" href="#" role="button">${this.renderIcon('close', 'fa-lg')}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
document.body.append(...wrapper.children);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.overlay = document.getElementById('lightboxOverlay');
|
|
||||||
this.lightbox = document.getElementById('lightbox');
|
|
||||||
this.outerContainer = this.lightbox.querySelector('.lb-outerContainer');
|
|
||||||
this.container = this.lightbox.querySelector('.lb-container');
|
|
||||||
this.image = this.lightbox.querySelector('.lb-image');
|
|
||||||
this.nav = this.lightbox.querySelector('.lb-nav');
|
|
||||||
this.loader = this.lightbox.querySelector('.lb-loader');
|
|
||||||
this.caption = this.lightbox.querySelector('.lb-caption');
|
|
||||||
this.closeButton = this.lightbox.querySelector('.lb-close');
|
|
||||||
this.dataContainer = this.lightbox.querySelector('.lb-dataContainer');
|
|
||||||
this.prev = this.lightbox.querySelector('.lb-prev');
|
|
||||||
this.next = this.lightbox.querySelector('.lb-next');
|
|
||||||
this.video = this.lightbox.querySelector('.lb-video');
|
|
||||||
|
|
||||||
this.setVisible(this.overlay, false);
|
|
||||||
this.setVisible(this.lightbox, false);
|
|
||||||
|
|
||||||
this.containerPadding = this.getBoxMetrics(this.container, 'padding');
|
|
||||||
this.imageBorderWidth = this.getBoxMetrics(this.image, 'border');
|
|
||||||
this.videoBorderWidth = this.getBoxMetrics(this.video, 'border');
|
|
||||||
|
|
||||||
this.overlay.addEventListener('click', () => this.end());
|
|
||||||
this.dataContainer.addEventListener('click', () => this.end());
|
|
||||||
this.lightbox.addEventListener('click', (event) => {
|
|
||||||
if (event.target === this.lightbox) this.end();
|
|
||||||
});
|
|
||||||
this.outerContainer.addEventListener('click', (event) => {
|
|
||||||
if (event.target === this.outerContainer) this.end();
|
|
||||||
event.stopPropagation();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.prev.addEventListener('click', (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
if (this.currentImageIndex === 0) this.changeImage(this.album.length - 1);
|
|
||||||
else this.changeImage(this.currentImageIndex - 1);
|
|
||||||
});
|
|
||||||
this.next.addEventListener('click', (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
if (this.currentImageIndex === this.album.length - 1) this.changeImage(0);
|
|
||||||
else this.changeImage(this.currentImageIndex + 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.loader.addEventListener('click', (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
this.end();
|
|
||||||
});
|
|
||||||
this.closeButton.addEventListener('click', (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
this.end();
|
|
||||||
});
|
|
||||||
this.closeButton.addEventListener('keyup', (event) => {
|
|
||||||
if (event.key === 'Enter' || event.key === ' ') this.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.nav.addEventListener('wheel', this.boundOnWheel, { passive: false });
|
|
||||||
this.nav.addEventListener('mousedown', this.boundOnDragStart);
|
|
||||||
window.addEventListener('mouseup', this.boundOnDragEnd);
|
|
||||||
}
|
|
||||||
|
|
||||||
getBoxMetrics(element, type) {
|
|
||||||
const styles = getComputedStyle(element);
|
|
||||||
return {
|
|
||||||
top: parseInt(styles[`${type}-top-width`], 10) || 0,
|
|
||||||
right: parseInt(styles[`${type}-right-width`], 10) || 0,
|
|
||||||
bottom: parseInt(styles[`${type}-bottom-width`], 10) || 0,
|
|
||||||
left: parseInt(styles[`${type}-left-width`], 10) || 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
start(link) {
|
|
||||||
this.sizeOverlay();
|
|
||||||
this.album = [];
|
|
||||||
let imageNumber = 0;
|
|
||||||
const setName = link.getAttribute('data-lightbox');
|
|
||||||
|
|
||||||
const links = [...document.querySelectorAll(`${link.tagName}[data-lightbox="${CSS.escape(setName)}"]`)];
|
|
||||||
links.forEach((item, index) => {
|
|
||||||
this.addToAlbum(item);
|
|
||||||
if (item === link) imageNumber = index;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.fade(this.overlay, true, this.options.fadeDuration);
|
|
||||||
this.fade(this.lightbox, true, this.options.fadeDuration);
|
|
||||||
|
|
||||||
if (this.options.disableScrolling) document.body.classList.add('lb-disable-scrolling');
|
|
||||||
|
|
||||||
window.addEventListener('resize', this.boundOnResize);
|
|
||||||
this.changeImage(imageNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
addToAlbum(link) {
|
|
||||||
const img = link.querySelector('img');
|
|
||||||
this.album.push({
|
|
||||||
alt: link.getAttribute('data-alt') || '',
|
|
||||||
link: link.getAttribute('href'),
|
|
||||||
title: link.getAttribute('data-title') || link.getAttribute('title') || '',
|
|
||||||
orientation: parseInt(link.getAttribute('data-orientation') || '0', 10),
|
|
||||||
type: link.getAttribute('data-type') || 'image',
|
|
||||||
id: link.getAttribute('data-id'),
|
|
||||||
width: parseInt(img?.getAttribute('width') || '0', 10),
|
|
||||||
height: parseInt(img?.getAttribute('height') || '0', 10),
|
|
||||||
set: link.getAttribute('data-lightbox') || ''
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
hasMediaAfterCurrent() {
|
|
||||||
return this.currentImageIndex < this.album.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshAlbum() {
|
|
||||||
const current = this.album[this.currentImageIndex];
|
|
||||||
if (!current?.set) return;
|
|
||||||
|
|
||||||
const links = [...document.querySelectorAll(`a[data-lightbox="${CSS.escape(current.set)}"], area[data-lightbox="${CSS.escape(current.set)}"]`)];
|
|
||||||
if (!links.length) return;
|
|
||||||
|
|
||||||
const existingKeys = new Set(this.album.map((media) => this.getMediaKey(media)));
|
|
||||||
links.forEach((link) => {
|
|
||||||
const key = this.getLinkMediaKey(link);
|
|
||||||
if (existingKeys.has(key)) return;
|
|
||||||
|
|
||||||
this.addToAlbum(link);
|
|
||||||
existingKeys.add(key);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.updateNav();
|
|
||||||
}
|
|
||||||
|
|
||||||
getMediaKey(media) {
|
|
||||||
return `${media.set}:${media.id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
getLinkMediaKey(link) {
|
|
||||||
return `${link.getAttribute('data-lightbox') || ''}:${link.getAttribute('data-id')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
getMaxSizes(mediaWidth, mediaHeight, mediaType) {
|
|
||||||
let maxWidth = window.innerWidth - this.containerPadding.left - this.containerPadding.right;
|
|
||||||
let maxHeight = window.innerHeight - this.containerPadding.top - this.containerPadding.bottom - this.options.positionFromTop;
|
|
||||||
const border = mediaType === 'image' ? this.imageBorderWidth : this.videoBorderWidth;
|
|
||||||
maxWidth -= border.left + border.right;
|
|
||||||
maxHeight -= border.top + border.bottom;
|
|
||||||
maxHeight -= this.getDataContainerHeight(maxWidth + this.containerPadding.left + this.containerPadding.right + border.left + border.right);
|
|
||||||
|
|
||||||
return {
|
|
||||||
maxWidth: Math.max(maxWidth, 1),
|
|
||||||
maxHeight: Math.max(maxHeight, 1)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getDataContainerHeight(width = null) {
|
|
||||||
if (!this.dataContainer) return 0;
|
|
||||||
|
|
||||||
const currentWidth = this.dataContainer.style.width;
|
|
||||||
if (width !== null) this.dataContainer.style.width = `${width}px`;
|
|
||||||
const height = Math.ceil(this.dataContainer.getBoundingClientRect().height || this.dataContainer.offsetHeight || 0);
|
|
||||||
this.dataContainer.style.width = currentWidth;
|
|
||||||
|
|
||||||
return height;
|
|
||||||
}
|
|
||||||
|
|
||||||
getMediaSize(media, maxWidth, maxHeight) {
|
|
||||||
if (media.width <= maxWidth && media.height <= maxHeight) {
|
|
||||||
return {
|
|
||||||
width: media.width,
|
|
||||||
height: media.height
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const widthRatio = media.width / maxWidth;
|
|
||||||
const heightRatio = media.height / maxHeight;
|
|
||||||
|
|
||||||
if (widthRatio > heightRatio) {
|
|
||||||
return {
|
|
||||||
width: maxWidth,
|
|
||||||
height: Math.round(media.height / widthRatio)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
width: Math.round(media.width / heightRatio),
|
|
||||||
height: maxHeight
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fitSizeWithDataContainer(size, mediaType) {
|
|
||||||
const border = mediaType === 'image' ? this.imageBorderWidth : this.videoBorderWidth;
|
|
||||||
const maxOuterHeight = Math.max(window.innerHeight - this.options.positionFromTop, 1);
|
|
||||||
let fittedSize = size;
|
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
const containerWidth = fittedSize.width + this.containerPadding.left + this.containerPadding.right + border.left + border.right;
|
|
||||||
const containerHeight = fittedSize.height + this.containerPadding.top + this.containerPadding.bottom + border.top + border.bottom;
|
|
||||||
const dataHeight = this.getDataContainerHeight(containerWidth);
|
|
||||||
const overflow = Math.ceil(containerHeight + dataHeight - maxOuterHeight);
|
|
||||||
if (overflow <= 0 || fittedSize.height <= 1) break;
|
|
||||||
|
|
||||||
const height = Math.max(fittedSize.height - overflow, 1);
|
|
||||||
fittedSize = {
|
|
||||||
width: Math.max(Math.round(fittedSize.width * (height / fittedSize.height)), 1),
|
|
||||||
height
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return fittedSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSize(index) {
|
|
||||||
const media = this.album[index];
|
|
||||||
const maxSizes = this.getMaxSizes(media.width, media.height, media.type);
|
|
||||||
const maxWidth = this.options.maxWidth ? Math.min(this.options.maxWidth, maxSizes.maxWidth) : maxSizes.maxWidth;
|
|
||||||
const maxHeight = this.options.maxHeight ? Math.min(this.options.maxHeight, maxSizes.maxHeight) : maxSizes.maxHeight;
|
|
||||||
const size = this.fitSizeWithDataContainer(this.getMediaSize(media, maxWidth, maxHeight), media.type);
|
|
||||||
|
|
||||||
const target = media.type === 'video' ? this.video : this.image;
|
|
||||||
target.width = size.width;
|
|
||||||
target.height = size.height;
|
|
||||||
this.sizeContainer(size.width, size.height, media.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
changeImage(index) {
|
|
||||||
const media = this.album[index];
|
|
||||||
if (!media) return;
|
|
||||||
|
|
||||||
this.updateDetails(media, false);
|
|
||||||
this.hideElements([this.dataContainer]);
|
|
||||||
this.disableKeyboardNav();
|
|
||||||
this.fade(this.overlay, true, this.options.fadeDuration);
|
|
||||||
this.fade(this.loader, true, 200);
|
|
||||||
this.hideElements([this.image, this.video, this.nav, this.prev, this.next]);
|
|
||||||
this.resetImageTransform();
|
|
||||||
this.outerContainer.classList.add('animating');
|
|
||||||
this.container.classList.remove('moveable', 'moving', 'lb-video-nav');
|
|
||||||
this.currentImageIndex = index;
|
|
||||||
|
|
||||||
this.options.onMediaChange(media);
|
|
||||||
|
|
||||||
if (media.type === 'video') {
|
|
||||||
this.image.removeAttribute('src');
|
|
||||||
this.container.classList.add('lb-video-nav');
|
|
||||||
this.video.onloadedmetadata = () => {
|
|
||||||
media.width = this.video.videoWidth;
|
|
||||||
media.height = this.video.videoHeight;
|
|
||||||
this.video.onloadedmetadata = null;
|
|
||||||
this.updateSize(index);
|
|
||||||
};
|
|
||||||
this.video.src = media.link;
|
|
||||||
} else {
|
|
||||||
this.video.pause();
|
|
||||||
this.video.removeAttribute('src');
|
|
||||||
this.image.onload = () => {
|
|
||||||
this.image.alt = media.alt;
|
|
||||||
let width = this.image.naturalWidth;
|
|
||||||
let height = this.image.naturalHeight;
|
|
||||||
if (Math.abs(media.orientation) === 90 && width > height) {
|
|
||||||
const tmp = width;
|
|
||||||
width = height;
|
|
||||||
height = tmp;
|
|
||||||
}
|
|
||||||
media.width = width;
|
|
||||||
media.height = height;
|
|
||||||
this.image.onload = null;
|
|
||||||
this.updateSize(index);
|
|
||||||
};
|
|
||||||
this.image.src = media.link;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sizeOverlay() {
|
|
||||||
if (this.resizeTimer) clearTimeout(this.resizeTimer);
|
|
||||||
if (!this.album.length) return;
|
|
||||||
|
|
||||||
this.resizeTimer = window.setTimeout(() => {
|
|
||||||
const current = this.album[this.currentImageIndex];
|
|
||||||
if (!current) return;
|
|
||||||
if (current.type === 'image') this.changeImage(this.currentImageIndex);
|
|
||||||
else this.updateSize(this.currentImageIndex);
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
sizeContainer(width, height, mediaType = 'image') {
|
|
||||||
const border = mediaType === 'image' ? this.imageBorderWidth : this.videoBorderWidth;
|
|
||||||
const newWidth = width + this.containerPadding.left + this.containerPadding.right + border.left + border.right;
|
|
||||||
const newHeight = height + this.containerPadding.top + this.containerPadding.bottom + border.top + border.bottom;
|
|
||||||
const dataHeight = this.getDataContainerHeight(newWidth);
|
|
||||||
|
|
||||||
this.outerContainer.style.transition = `width ${this.options.resizeDuration}ms, height ${this.options.resizeDuration}ms`;
|
|
||||||
this.outerContainer.style.width = `${newWidth}px`;
|
|
||||||
this.outerContainer.style.height = `${newHeight + dataHeight}px`;
|
|
||||||
this.container.style.height = `${newHeight}px`;
|
|
||||||
|
|
||||||
window.setTimeout(() => {
|
|
||||||
this.overlay.focus();
|
|
||||||
this.showImage();
|
|
||||||
this.outerContainer.style.transition = '';
|
|
||||||
}, this.options.resizeDuration);
|
|
||||||
}
|
|
||||||
|
|
||||||
showImage() {
|
|
||||||
this.fade(this.loader, false, 0);
|
|
||||||
if (this.options.hasVideo && this.album[this.currentImageIndex].type === 'video') this.fade(this.video, true, this.options.imageFadeDuration);
|
|
||||||
else this.fade(this.image, true, this.options.imageFadeDuration);
|
|
||||||
|
|
||||||
this.updateNav();
|
|
||||||
this.updateDetails();
|
|
||||||
this.preloadNeighboringImages();
|
|
||||||
this.enableKeyboardNav();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateNav() {
|
|
||||||
this.setVisible(this.nav, true);
|
|
||||||
this.setVisible(this.prev, false);
|
|
||||||
this.setVisible(this.next, false);
|
|
||||||
|
|
||||||
const alwaysShowNav = ('ontouchstart' in window) && this.options.alwaysShowNavOnTouchDevices;
|
|
||||||
if (this.album.length <= 1) return;
|
|
||||||
|
|
||||||
if (this.options.wrapAround) {
|
|
||||||
this.setVisible(this.prev, true);
|
|
||||||
this.setVisible(this.next, true);
|
|
||||||
} else {
|
|
||||||
if (this.currentImageIndex > 0) this.setVisible(this.prev, true);
|
|
||||||
if (this.currentImageIndex < this.album.length - 1) this.setVisible(this.next, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (alwaysShowNav) {
|
|
||||||
this.prev.style.opacity = '1';
|
|
||||||
this.next.style.opacity = '1';
|
|
||||||
} else {
|
|
||||||
this.prev.style.opacity = '';
|
|
||||||
this.next.style.opacity = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDetails(media = this.album[this.currentImageIndex], show = true) {
|
|
||||||
if (!media) return;
|
|
||||||
|
|
||||||
if (media.title) {
|
|
||||||
if (this.options.sanitizeTitle) this.caption.textContent = media.title;
|
|
||||||
else this.caption.innerHTML = media.title;
|
|
||||||
if (show) this.fade(this.caption, true, 200);
|
|
||||||
else this.setVisible(this.caption, true);
|
|
||||||
} else {
|
|
||||||
this.caption.textContent = '';
|
|
||||||
this.setVisible(this.caption, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (show) {
|
|
||||||
this.fade(this.closeButton, true, 200);
|
|
||||||
this.outerContainer.classList.remove('animating');
|
|
||||||
this.fade(this.dataContainer, true, this.options.resizeDuration);
|
|
||||||
} else {
|
|
||||||
this.setVisible(this.closeButton, true);
|
|
||||||
this.setVisible(this.dataContainer, false);
|
|
||||||
this.dataContainer.style.transition = '';
|
|
||||||
this.dataContainer.style.opacity = '0';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
preloadNeighboringImages() {
|
|
||||||
const next = this.album[this.currentImageIndex + 1];
|
|
||||||
const prev = this.album[this.currentImageIndex - 1];
|
|
||||||
if (next && next.type === 'image') {
|
|
||||||
const preloadNext = new Image();
|
|
||||||
preloadNext.src = next.link;
|
|
||||||
}
|
|
||||||
if (prev && prev.type === 'image') {
|
|
||||||
const preloadPrev = new Image();
|
|
||||||
preloadPrev.src = prev.link;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enableKeyboardNav() {
|
|
||||||
this.disableKeyboardNav();
|
|
||||||
this.lightbox.addEventListener('keyup', this.boundOnKeyUp);
|
|
||||||
this.overlay.addEventListener('keyup', this.boundOnKeyUp);
|
|
||||||
}
|
|
||||||
|
|
||||||
disableKeyboardNav() {
|
|
||||||
this.lightbox?.removeEventListener('keyup', this.boundOnKeyUp);
|
|
||||||
this.overlay?.removeEventListener('keyup', this.boundOnKeyUp);
|
|
||||||
}
|
|
||||||
|
|
||||||
keyboardAction(event) {
|
|
||||||
switch (event.key) {
|
|
||||||
case 'Escape':
|
|
||||||
event.stopPropagation();
|
|
||||||
this.end();
|
|
||||||
break;
|
|
||||||
case 'ArrowLeft':
|
|
||||||
if (this.currentImageIndex !== 0) this.changeImage(this.currentImageIndex - 1);
|
|
||||||
else if (this.options.wrapAround && this.album.length > 1) this.changeImage(this.album.length - 1);
|
|
||||||
break;
|
|
||||||
case 'ArrowRight':
|
|
||||||
if (this.currentImageIndex !== this.album.length - 1) this.changeImage(this.currentImageIndex + 1);
|
|
||||||
else if (this.options.wrapAround && this.album.length > 1) this.changeImage(0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onWheel(event) {
|
|
||||||
const media = this.album[this.currentImageIndex];
|
|
||||||
if (!media || media.type === 'video') return;
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const rect = this.image.getBoundingClientRect();
|
|
||||||
const oldTransform = this.getImageTransform();
|
|
||||||
const oldZoom = oldTransform.scale;
|
|
||||||
const maxZoom = Math.max(media.width / Math.max(this.image.width, 1), media.height / Math.max(this.image.height, 1), 1);
|
|
||||||
const newZoom = Math.min(Math.max(oldZoom + (-Math.sign(event.deltaY) / 10), 1), maxZoom);
|
|
||||||
|
|
||||||
const imageCenterX = rect.left + rect.width / 2 - oldTransform.translateX;
|
|
||||||
const imageCenterY = rect.top + rect.height / 2 - oldTransform.translateY;
|
|
||||||
const cursorX = event.clientX - imageCenterX;
|
|
||||||
const cursorY = event.clientY - imageCenterY;
|
|
||||||
const zoomRatio = newZoom / oldZoom;
|
|
||||||
const transform = this.clampImageTransform({
|
|
||||||
scale: newZoom,
|
|
||||||
translateX: cursorX - zoomRatio * (cursorX - oldTransform.translateX),
|
|
||||||
translateY: cursorY - zoomRatio * (cursorY - oldTransform.translateY)
|
|
||||||
});
|
|
||||||
|
|
||||||
this.container.classList.toggle('moveable', newZoom > 1);
|
|
||||||
this.setImageTransform(transform);
|
|
||||||
}
|
|
||||||
|
|
||||||
onDragStart(event) {
|
|
||||||
const scale = parseFloat(this.image.style.getPropertyValue('--scale') || '1');
|
|
||||||
if (scale <= 1) return;
|
|
||||||
|
|
||||||
this.gMouseDownOffsetX = event.clientX - parseFloat(this.image.style.getPropertyValue('--translate-x') || '0');
|
|
||||||
this.gMouseDownOffsetY = event.clientY - parseFloat(this.image.style.getPropertyValue('--translate-y') || '0');
|
|
||||||
this.container.classList.add('moving');
|
|
||||||
window.addEventListener('mousemove', this.boundOnDragMove);
|
|
||||||
}
|
|
||||||
|
|
||||||
onDragMove(event) {
|
|
||||||
const zoom = parseFloat(this.image.style.getPropertyValue('--scale') || '1');
|
|
||||||
const transform = this.clampImageTransform({
|
|
||||||
scale: zoom,
|
|
||||||
translateX: event.clientX - this.gMouseDownOffsetX,
|
|
||||||
translateY: event.clientY - this.gMouseDownOffsetY
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setImageTransform(transform);
|
|
||||||
}
|
|
||||||
|
|
||||||
onDragEnd() {
|
|
||||||
window.removeEventListener('mousemove', this.boundOnDragMove);
|
|
||||||
this.container?.classList.remove('moving');
|
|
||||||
}
|
|
||||||
|
|
||||||
resetImageTransform() {
|
|
||||||
this.setImageTransform({scale: 1, translateX: 0, translateY: 0});
|
|
||||||
}
|
|
||||||
|
|
||||||
getImageTransform() {
|
|
||||||
return {
|
|
||||||
scale: parseFloat(this.image.style.getPropertyValue('--scale') || '1'),
|
|
||||||
translateX: parseFloat(this.image.style.getPropertyValue('--translate-x') || '0'),
|
|
||||||
translateY: parseFloat(this.image.style.getPropertyValue('--translate-y') || '0')
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
clampImageTransform(transform) {
|
|
||||||
const maxTranslateX = (transform.scale - 1) * this.image.width / 2;
|
|
||||||
const maxTranslateY = (transform.scale - 1) * this.image.height / 2;
|
|
||||||
|
|
||||||
return {
|
|
||||||
scale: transform.scale,
|
|
||||||
translateX: Math.max(Math.min(transform.translateX, maxTranslateX), -maxTranslateX),
|
|
||||||
translateY: Math.max(Math.min(transform.translateY, maxTranslateY), -maxTranslateY)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
setImageTransform(transform) {
|
|
||||||
if (!this.image) return;
|
|
||||||
this.image.style.setProperty('--scale', String(transform.scale));
|
|
||||||
this.image.style.setProperty('--translate-x', `${transform.translateX}px`);
|
|
||||||
this.image.style.setProperty('--translate-y', `${transform.translateY}px`);
|
|
||||||
}
|
|
||||||
|
|
||||||
hideElements(elements) {
|
|
||||||
elements.forEach((element) => {
|
|
||||||
this.setVisible(element, false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setVisible(element, visible) {
|
|
||||||
if (!element) return;
|
|
||||||
element.style.visibility = visible ? 'visible' : 'hidden';
|
|
||||||
element.style.pointerEvents = visible ? '' : 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
fade(element, show, duration, done) {
|
|
||||||
if (!element) return;
|
|
||||||
|
|
||||||
const safeDuration = duration || 0;
|
|
||||||
element.style.transition = `opacity ${safeDuration}ms`;
|
|
||||||
if (show) {
|
|
||||||
this.setVisible(element, true);
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
element.style.opacity = element === this.overlay ? '0.8' : '1';
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
element.style.opacity = '0';
|
|
||||||
element.style.pointerEvents = 'none';
|
|
||||||
window.setTimeout(() => {
|
|
||||||
this.setVisible(element, false);
|
|
||||||
}, safeDuration);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof done === 'function') {
|
|
||||||
window.setTimeout(done, safeDuration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
end(dispose = false) {
|
|
||||||
this.disableKeyboardNav();
|
|
||||||
this.video?.pause();
|
|
||||||
this.video?.removeAttribute('src');
|
|
||||||
this.container?.classList.remove('lb-video-nav', 'moveable', 'moving');
|
|
||||||
window.removeEventListener('resize', this.boundOnResize);
|
|
||||||
window.removeEventListener('mousemove', this.boundOnDragMove);
|
|
||||||
|
|
||||||
if(dispose){
|
|
||||||
this.disable();
|
|
||||||
if(this.resizeTimer) clearTimeout(this.resizeTimer);
|
|
||||||
window.removeEventListener('mouseup', this.boundOnDragEnd);
|
|
||||||
this.lightbox?.remove();
|
|
||||||
this.overlay?.remove();
|
|
||||||
this.album = [];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.fade(this.lightbox, false, this.options.fadeDuration);
|
|
||||||
this.fade(this.overlay, false, this.options.fadeDuration);
|
|
||||||
this.options.onClosing();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.options.disableScrolling) document.body.classList.remove('lb-disable-scrolling');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -75,7 +75,7 @@ export default class Projects {
|
|||||||
iElevDrop += Math.min(iElevDelta, 0);
|
iElevDrop += Math.min(iElevDelta, 0);
|
||||||
iElevGain += Math.max(iElevDelta, 0);
|
iElevGain += Math.max(iElevDelta, 0);
|
||||||
|
|
||||||
let iSpeedCorrecRatio = 0;
|
let iSpeedCorrecRatio;
|
||||||
const iAngle = iElevDelta / iSegDistance;
|
const iAngle = iElevDelta / iSegDistance;
|
||||||
if(iAngle < -1) iSpeedCorrecRatio = 0.5;
|
if(iAngle < -1) iSpeedCorrecRatio = 0.5;
|
||||||
else if(iAngle < -0.2) iSpeedCorrecRatio = 1.25;
|
else if(iAngle < -0.2) iSpeedCorrecRatio = 1.25;
|
||||||
|
|||||||
@@ -105,13 +105,13 @@
|
|||||||
|
|
||||||
&.account {
|
&.account {
|
||||||
.account-login {
|
.account-login {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-wrap: wrap;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: var.$block-spacing;
|
gap: var.$block-spacing;
|
||||||
|
|
||||||
input {
|
input {
|
||||||
flex: 1 1 auto;
|
grid-column: 1 / -1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
@@ -120,8 +120,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input:last-of-type {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
button.manage {
|
button.manage {
|
||||||
flex: 0 0 auto;
|
grid-column: 2;
|
||||||
|
|
||||||
&.loading {
|
&.loading {
|
||||||
background-color: color.$main;
|
background-color: color.$main;
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
/* Site Global CSS */
|
/* Site Global CSS */
|
||||||
@use "sass:meta";
|
|
||||||
@use '@styles/common';
|
@use '@styles/common';
|
||||||
|
|
||||||
/* Modules */
|
/* Modules */
|
||||||
@use '@styles/lightbox';
|
@use '@styles/lightbox';
|
||||||
@import 'simplebar-vue/dist/simplebar.min.css';
|
@use '@styles/vue';
|
||||||
@include meta.load-css('@styles/vue');
|
|
||||||
|
|
||||||
/* Pages Specific CSS */
|
/* Pages Specific CSS */
|
||||||
@include meta.load-css('@styles/page.project');
|
@use '@styles/page.project' as page-project;
|
||||||
@include meta.load-css('@styles/page.upload');
|
@use '@styles/page.upload' as page-upload;
|
||||||
@include meta.load-css('@styles/page.admin');
|
@use '@styles/page.admin' as page-admin;
|
||||||
|
|
||||||
@include meta.load-css('@styles/mobile');
|
@use '@styles/mobile';
|
||||||
|
|
||||||
|
@import 'simplebar-vue/dist/simplebar.min.css';
|
||||||
@@ -16,6 +16,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
publicDir: false,
|
publicDir: false,
|
||||||
plugins: [
|
plugins: [
|
||||||
livetrailPublicAssets(),
|
livetrailPublicAssets(),
|
||||||
|
livetrailLint(isDev),
|
||||||
vue()
|
vue()
|
||||||
],
|
],
|
||||||
build: {
|
build: {
|
||||||
@@ -62,6 +63,37 @@ export default defineConfig(({ mode }) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function livetrailLint(isDev) {
|
||||||
|
return {
|
||||||
|
name: 'livetrail-lint',
|
||||||
|
apply: 'build',
|
||||||
|
//In `--watch` mode Vite re-runs the whole plugin pipeline (including
|
||||||
|
//buildStart) on every rebuild it triggers from a file change, so this
|
||||||
|
//alone gives re-linting on every save with no separate watchChange
|
||||||
|
//wiring needed.
|
||||||
|
async buildStart() {
|
||||||
|
await runLint(isDev);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runLint(isDev) {
|
||||||
|
const { ESLint } = await import('eslint');
|
||||||
|
const eslint = new ESLint({ cwd: ROOT });
|
||||||
|
const results = await eslint.lintFiles(['src']);
|
||||||
|
|
||||||
|
const formatter = await eslint.loadFormatter('stylish');
|
||||||
|
const output = await formatter.format(results);
|
||||||
|
if (output) process.stdout.write(output + '\n');
|
||||||
|
|
||||||
|
//Dev keeps watching regardless - the point is fast feedback, not a gate.
|
||||||
|
//Prod aborts the build so bad code can't ship.
|
||||||
|
const hasErrors = results.some((result) => result.errorCount > 0);
|
||||||
|
if (hasErrors && !isDev) {
|
||||||
|
throw new Error('ESLint found errors in src/ - aborting production build.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function livetrailPublicAssets() {
|
function livetrailPublicAssets() {
|
||||||
return {
|
return {
|
||||||
name: 'livetrail-public-assets',
|
name: 'livetrail-public-assets',
|
||||||
|
|||||||
Reference in New Issue
Block a user