Compare commits
208 Commits
789838dd07
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 085cfd8ba2 | |||
| 14d827ab66 | |||
| aa30431df8 | |||
| a127535b36 | |||
| aa17ea99a2 | |||
| ae24be2c22 | |||
| 0b9f886905 | |||
| 7ecd8094e2 | |||
| 9718713eb4 | |||
| 36a5900118 | |||
| 1eebfc90fa | |||
| 7b2962be15 | |||
| c738fe8d50 | |||
| 76fdc4be43 | |||
| ff3fac2ab9 | |||
| 62f976b6f3 | |||
| 00a06a1ca9 | |||
| 6800256f09 | |||
| 17b998ee60 | |||
| 87a991eaea | |||
| 9ce25e73f0 | |||
| 6cad199431 | |||
| 36f9057a30 | |||
| 05c77f30bd | |||
| 739c593d2a | |||
| 034d02f042 | |||
| c2685a2731 | |||
| 28c6f79fdb | |||
| 77a1c51692 | |||
| 319c288586 | |||
| 980035e3d1 | |||
| 520df5b570 | |||
| fdd0ada815 | |||
| 8092846d6f | |||
| 7b58b65db3 | |||
| d0c33c31a8 | |||
| 313dab26a2 | |||
| 7ead18601c | |||
| c80e8d1c67 | |||
| d4bc73e32c | |||
| 6ee4c8efc7 | |||
| badae8a3a0 | |||
| c783cbe543 | |||
| cf5ae33ba4 | |||
| a3d217bbdd | |||
| 7cad5fbdf9 | |||
| fe8a8034ca | |||
| 138ce6ec8b | |||
| 690fd6d831 | |||
| 24fd224ec6 | |||
| c9ce785f12 | |||
| e0fc62df84 | |||
| 8a590aa2fc | |||
| 3fd68fa938 | |||
| 3ba7b2bfab | |||
| b44d2960f7 | |||
| c5529d5f94 | |||
| f63f5c240e | |||
| 0bb7ae2361 | |||
| 7f74263ba2 | |||
| c43539b640 | |||
| 93a72c628e | |||
| 39ddd1cf95 | |||
| 6f11c827c6 | |||
| b5de606a3e | |||
| 837c4a327b | |||
| 5b365f1eab | |||
| dfa4f3239c | |||
| 1c69ae56ac | |||
| 9adfa18e9b | |||
| a2e7b235fe | |||
| 49f37465bd | |||
| c3835f45c5 | |||
| 17fe2330c6 | |||
| b88fb4ca9d | |||
| daca0a8294 | |||
| e80e3ff3f3 | |||
| 8e17db7a2e | |||
| 238001ae93 | |||
| b7956766e8 | |||
| ca1183d88a | |||
| 4c34994ac7 | |||
| 8385c85820 | |||
| 71e9c1a45a | |||
| 880bbc3d9a | |||
| e293193dd7 | |||
| 821b6b47f3 | |||
| 1852f6640e | |||
| 3c8cdbaad6 | |||
| 415bd9d0cf | |||
| aa1856acb7 | |||
| 321e79c230 | |||
| 614a69103b | |||
| 213bd359fc | |||
| 1cb82838b2 | |||
| 3f1f98f98c | |||
| e6d11f424d | |||
| 7aaaff7dda | |||
| 07a5c3baf9 | |||
| 5e690e5576 | |||
| 12ae225773 | |||
| 54bae3e9c9 | |||
| 141618f2cd | |||
| b759508779 | |||
| a4e0a345d6 | |||
| 86082c513e | |||
| 36aa480205 | |||
| 87286dc8fd | |||
| da46106779 | |||
| 0cc7fc336a | |||
| 95ebc96484 | |||
| 560b22c039 | |||
| 3567f521f8 | |||
| fe2a3d91c0 | |||
| 15c044ac52 | |||
| e5e34676e2 | |||
| c4dd938a56 | |||
| dcb916d442 | |||
| 37bfb42834 | |||
| 13c48a559f | |||
| c39b7705be | |||
| 844c9c0a53 | |||
| f2af936e60 | |||
| dc411cc532 | |||
| b339d6d068 | |||
| 7dc2b28c44 | |||
| ff4bc26381 | |||
| 40565849c5 | |||
| dea14acd29 | |||
| c32998650f | |||
| b2b06180e6 | |||
| 90349365f9 | |||
| 24021bf60f | |||
| 64cacaf16e | |||
| eb0ded0d26 | |||
| 635b3781e3 | |||
| 9e4fbe7ad4 | |||
| ef88e600e3 | |||
| bcc5e9e0cd | |||
| fcbb3d9d14 | |||
| 3416ace4ee | |||
| 932f950ed8 | |||
| c4d05c297c | |||
| 52316d9abb | |||
| cb505d9092 | |||
| f81fbd454e | |||
| 295ff0538a | |||
| fb26000122 | |||
| 824718fad0 | |||
| 77216e6c2f | |||
| 28f95162aa | |||
| 4f3be3342c | |||
| e70d3ddbd3 | |||
| 051503cbed | |||
| b86d5d2cb1 | |||
| e7d4c840c2 | |||
| da39ca6589 | |||
| 14bf9e2fc8 | |||
| 853a9cc36f | |||
| 8ca5fa2d53 | |||
| 975a8039b3 | |||
| 325373b5d7 | |||
| ac9fcbe0ba | |||
| d9bc89b7f6 | |||
| 760f38374f | |||
| b9a4bd6d2d | |||
| ea14a1ef3e | |||
| 457bab2c18 | |||
| 3571f93e41 | |||
| e878b159bf | |||
| db70593852 | |||
| 73b8e6b04f | |||
| a49f73236b | |||
| c0b7ad8000 | |||
| 83bf47287c | |||
| 4ce96e7192 | |||
| 3169b8e83e | |||
| 205855acd8 | |||
| 356d8ccd7e | |||
| 25ff80ad7a | |||
| 0cd509a99d | |||
| 3063f8b904 | |||
| 59dea2917d | |||
| 6e614042d1 | |||
| 8c812f6b0a | |||
| abacab8206 | |||
| 869b084d70 | |||
| cab899e544 | |||
| b6fc305111 | |||
| 30a81b5341 | |||
| 683670f77a | |||
| c2956ac373 | |||
| 7853c6e285 | |||
| f674b0d934 | |||
| d767e335f9 | |||
| 3611f2206f | |||
| 828d32b0ef | |||
| f5d193e42b | |||
| c45a19e6bf | |||
| f86dadfc7d | |||
| 9d676c339b | |||
| 2f3a3f9561 | |||
| 55e40f76a1 | |||
| 4e9fb52318 | |||
| 850d2e7235 | |||
| 97645b3476 | |||
| ab914a391f | |||
| 842e02f4bb |
60
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
name: Deploy Spot
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: spot
|
||||||
|
|
||||||
|
env:
|
||||||
|
COMPOSER_NO_INTERACTION: "1"
|
||||||
|
COMPOSER_HOME: .composer
|
||||||
|
DEPLOY_PATH: /var/www/spot
|
||||||
|
npm_config_cache: .npm-cache
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check runner tools
|
||||||
|
run: |
|
||||||
|
command -v composer
|
||||||
|
command -v npm
|
||||||
|
command -v rsync
|
||||||
|
|
||||||
|
- name: Check deploy path
|
||||||
|
run: |
|
||||||
|
test -d "$DEPLOY_PATH"
|
||||||
|
test -w "$DEPLOY_PATH"
|
||||||
|
|
||||||
|
- name: Validate Composer configuration
|
||||||
|
run: composer validate --no-check-publish
|
||||||
|
|
||||||
|
- name: Install PHP dependencies
|
||||||
|
run: composer install --no-dev --prefer-dist --optimize-autoloader
|
||||||
|
|
||||||
|
- name: Install npm dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Prepare runtime missing mount points
|
||||||
|
run: mkdir -p files resources/geo
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
run: npm run prod
|
||||||
|
|
||||||
|
- name: Deploy to production
|
||||||
|
run: |
|
||||||
|
rsync -azc --no-times --delete \
|
||||||
|
--exclude "/.git/" \
|
||||||
|
--exclude "/.gitea/" \
|
||||||
|
--exclude "/.composer/" \
|
||||||
|
--exclude "/.npm-cache/" \
|
||||||
|
--exclude "/node_modules/" \
|
||||||
|
--exclude "/config/settings.php" \
|
||||||
|
--exclude "/log.html" \
|
||||||
|
--exclude "/files/" \
|
||||||
|
--exclude "/resources/geo/*.geojson" \
|
||||||
|
./ "$DEPLOY_PATH/"
|
||||||
28
.gitignore
vendored
@@ -1,14 +1,16 @@
|
|||||||
/settings.php
|
# App config files
|
||||||
/style/.sass-cache/
|
/config/settings.php
|
||||||
/files/**/*.jpg
|
|
||||||
/files/**/*.JPG
|
|
||||||
/files/**/*.jpeg
|
|
||||||
/files/**/*.JPEG
|
|
||||||
/files/**/*.png
|
|
||||||
/files/**/*.PNG
|
|
||||||
/files/**/*.mov
|
|
||||||
/files/**/*.MOV
|
|
||||||
/geo/*.geojson
|
|
||||||
/spot_cron.sh
|
|
||||||
/vendor/*
|
|
||||||
/log.html
|
/log.html
|
||||||
|
|
||||||
|
# Upload folders
|
||||||
|
/files/
|
||||||
|
/resources/geo/*.geojson
|
||||||
|
|
||||||
|
# Build folder
|
||||||
|
/public/*
|
||||||
|
!/public/index.php
|
||||||
|
|
||||||
|
# Dependencies files
|
||||||
|
/vendor/
|
||||||
|
/node_modules/
|
||||||
|
/composer.dev.lock
|
||||||
|
|||||||
153
build/webpack.config.js
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const SymlinkWebpackPlugin = require('symlink-webpack-plugin');
|
||||||
|
const { VueLoaderPlugin } = require('vue-loader');
|
||||||
|
|
||||||
|
const ROOT = path.resolve(__dirname, '..');
|
||||||
|
const SRC = path.resolve(ROOT, 'src');
|
||||||
|
const PUBLIC = path.resolve(ROOT, 'public');
|
||||||
|
|
||||||
|
module.exports = (env, argv) => {
|
||||||
|
const mode = argv.mode || 'production';
|
||||||
|
const isDev = (mode === 'development');
|
||||||
|
|
||||||
|
return {
|
||||||
|
mode,
|
||||||
|
devtool: isDev ? 'inline-source-map' : false,
|
||||||
|
watch: isDev,
|
||||||
|
entry: {
|
||||||
|
app: path.resolve(SRC, 'app.js')
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: PUBLIC,
|
||||||
|
filename: isDev ? 'assets/[name].js' : 'assets/[name].[contenthash:8].js',
|
||||||
|
chunkFilename: isDev ? 'assets/[name].js' : 'assets/[name].[contenthash:8].js',
|
||||||
|
publicPath: './',
|
||||||
|
clean: isDev ? false : {
|
||||||
|
keep: /^(index\.php|files|geo|assets\/images\/icons)(\/.*)?$/
|
||||||
|
}
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
splitChunks: {
|
||||||
|
chunks: 'all',
|
||||||
|
cacheGroups: {
|
||||||
|
maplibre: {
|
||||||
|
test: /[\\/]node_modules[\\/]maplibre-gl[\\/]/,
|
||||||
|
name: 'maplibre',
|
||||||
|
chunks: 'all',
|
||||||
|
priority: 30,
|
||||||
|
enforce: true
|
||||||
|
},
|
||||||
|
uppy: {
|
||||||
|
test: /[\\/]node_modules[\\/](@uppy|@transloadit|namespace-emitter|nanoid)[\\/]/,
|
||||||
|
name: 'uppy',
|
||||||
|
chunks: 'async',
|
||||||
|
priority: 20,
|
||||||
|
enforce: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
performance: {
|
||||||
|
hints: isDev ? false : 'warning',
|
||||||
|
maxEntrypointSize: 1500 * 1024,
|
||||||
|
maxAssetSize: 1100 * 1024
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [{
|
||||||
|
test: /\.vue$/,
|
||||||
|
loader: 'vue-loader'
|
||||||
|
}, {
|
||||||
|
test: /\.js$/,
|
||||||
|
exclude: file => (/node_modules/.test(file) && !/\.vue\.js/.test(file)),
|
||||||
|
use: {
|
||||||
|
loader: 'babel-loader',
|
||||||
|
options: {
|
||||||
|
presets: ['@babel/preset-env']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
test: /\.s[ac]ss$/i,
|
||||||
|
use: [
|
||||||
|
'vue-style-loader',
|
||||||
|
'css-loader',
|
||||||
|
{
|
||||||
|
loader: 'sass-loader',
|
||||||
|
options: {
|
||||||
|
implementation: require('sass'),
|
||||||
|
sourceMap: isDev
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
test: /\.css$/i,
|
||||||
|
use: ['vue-style-loader', 'css-loader']
|
||||||
|
}, {
|
||||||
|
test: /\.(png|svg|jpg|jpeg|gif|webp)$/i,
|
||||||
|
type: 'asset',
|
||||||
|
parser: {
|
||||||
|
dataUrlCondition: {
|
||||||
|
maxSize: 1 * 1024
|
||||||
|
}
|
||||||
|
},
|
||||||
|
generator: {
|
||||||
|
filename: isDev ? 'assets/images/[name][ext]' : 'assets/images/[name].[contenthash:8][ext]'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new SymlinkWebpackPlugin([
|
||||||
|
{ origin: '../files/', symlink: 'files' },
|
||||||
|
{ origin: '../resources/geo/', symlink: 'geo' },
|
||||||
|
{ origin: '../src/images/icons/', symlink: 'assets/images/icons' }
|
||||||
|
]),
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
__VUE_OPTIONS_API__: 'true',
|
||||||
|
__VUE_PROD_DEVTOOLS__: 'false',
|
||||||
|
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false'
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
apply(compiler) {
|
||||||
|
compiler.hooks.done.tap('EntryPointManifestPlugin', (stats) => {
|
||||||
|
const manifest = {
|
||||||
|
entrypoints: mapChunkGroups(stats.compilation.entrypoints),
|
||||||
|
chunkGroups: mapChunkGroups(stats.compilation.chunkGroups)
|
||||||
|
};
|
||||||
|
|
||||||
|
const manifestPath = path.resolve(PUBLIC, 'assets', 'entrypoints.json');
|
||||||
|
const tmpManifestPath = `${manifestPath}.tmp`;
|
||||||
|
|
||||||
|
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
||||||
|
fs.writeFileSync(tmpManifestPath, JSON.stringify(manifest, null, '\t'));
|
||||||
|
fs.renameSync(tmpManifestPath, manifestPath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new VueLoaderPlugin()
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.vue', '.scss', '...'],
|
||||||
|
alias: {
|
||||||
|
'@components': path.resolve(SRC, 'components'),
|
||||||
|
'@images': path.resolve(SRC, 'images'),
|
||||||
|
'@scripts': path.resolve(SRC, 'scripts'),
|
||||||
|
'@styles': path.resolve(SRC, 'styles')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapChunkGroups(chunkGroups = {}) {
|
||||||
|
const chunkGroupEntries = (chunkGroups instanceof Map)?Array.from(chunkGroups.entries()):Array.from(chunkGroups).map((chunkGroup) => [chunkGroup.name, chunkGroup]);
|
||||||
|
return Object.fromEntries(
|
||||||
|
chunkGroupEntries
|
||||||
|
.filter(([name]) => name)
|
||||||
|
.map(([name, chunkGroup]) => [
|
||||||
|
name,
|
||||||
|
Array.from((typeof chunkGroup.getFiles === 'function')?chunkGroup.getFiles():chunkGroup.assets)
|
||||||
|
.map((asset) => (typeof asset === 'string')?asset:asset.name)
|
||||||
|
.filter((file) => file.endsWith('.js'))
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
5
cli/cron.sh
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd "$(dirname "$0")/../public" || exit 1 #Execute from public folder
|
||||||
|
php -f index.php a=update_project > /dev/null
|
||||||
|
|
||||||
|
#Crontab job: 0 * * * * /path/to/spot/cli/cron.sh > /dev/null
|
||||||
28
composer.dev.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "franzz/spot",
|
||||||
|
"description": "LiveTrail",
|
||||||
|
"type": "project",
|
||||||
|
"license": "GPL-3.0-or-later",
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"type": "path",
|
||||||
|
"url": "../objects",
|
||||||
|
"options": {
|
||||||
|
"symlink": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"franzz/objects": "dev-vue",
|
||||||
|
"phpmailer/phpmailer": "^7.1"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Franzz\\Spot\\": "lib/",
|
||||||
|
"Franzz\\Objects\\": "../objects/inc/"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/settings.php"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,24 @@
|
|||||||
{
|
{
|
||||||
"name": "franzz/spot",
|
"name": "franzz/spot",
|
||||||
"description": "Spotty",
|
"description": "LiveTrail",
|
||||||
"type": "project",
|
"type": "project",
|
||||||
|
"license": "GPL-3.0-or-later",
|
||||||
"repositories": [
|
"repositories": [
|
||||||
{
|
{
|
||||||
"type": "path",
|
"type": "git",
|
||||||
"url": "../objects"
|
"url": "https://git.lutran.fr/franzz/objects"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"franzz/objects": "dev-composer",
|
"franzz/objects": "dev-vue",
|
||||||
"phpmailer/phpmailer": "^6.5"
|
"phpmailer/phpmailer": "^7.1"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Franzz\\Spot\\": "inc/"
|
"Franzz\\Spot\\": "lib/"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"settings.php"
|
"config/settings.php"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
44
composer.lock
generated
@@ -4,15 +4,15 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "164c903fea5bdcfb36cf6ea31ec0c307",
|
"content-hash": "8eb764990f0cb9427030c2bf01390d62",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "franzz/objects",
|
"name": "franzz/objects",
|
||||||
"version": "dev-composer",
|
"version": "dev-vue",
|
||||||
"dist": {
|
"source": {
|
||||||
"type": "path",
|
"type": "git",
|
||||||
"url": "../objects",
|
"url": "https://git.lutran.fr/franzz/objects",
|
||||||
"reference": "e1cf78b992a6f52742d6834f7508c0ef373ac860"
|
"reference": "af7d0f4c86564995f1c8149df98a183d33fab767"
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"autoload": {
|
"autoload": {
|
||||||
@@ -21,22 +21,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"description": "Objects",
|
"description": "Objects",
|
||||||
"transport-options": {
|
"time": "2026-05-29T23:29:34+00:00"
|
||||||
"relative": true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpmailer/phpmailer",
|
"name": "phpmailer/phpmailer",
|
||||||
"version": "v6.8.0",
|
"version": "v7.1.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
||||||
"reference": "df16b615e371d81fb79e506277faea67a1be18f1"
|
"reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/df16b615e371d81fb79e506277faea67a1be18f1",
|
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
|
||||||
"reference": "df16b615e371d81fb79e506277faea67a1be18f1",
|
"reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -46,16 +44,18 @@
|
|||||||
"php": ">=5.5.0"
|
"php": ">=5.5.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.2",
|
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
|
||||||
"doctrine/annotations": "^1.2.6 || ^1.13.3",
|
"doctrine/annotations": "^1.2.6 || ^1.13.3",
|
||||||
"php-parallel-lint/php-console-highlighter": "^1.0.0",
|
"php-parallel-lint/php-console-highlighter": "^1.0.0",
|
||||||
"php-parallel-lint/php-parallel-lint": "^1.3.2",
|
"php-parallel-lint/php-parallel-lint": "^1.3.2",
|
||||||
"phpcompatibility/php-compatibility": "^9.3.5",
|
"phpcompatibility/php-compatibility": "^10.0.0@dev",
|
||||||
"roave/security-advisories": "dev-latest",
|
"squizlabs/php_codesniffer": "^3.13.5",
|
||||||
"squizlabs/php_codesniffer": "^3.7.1",
|
|
||||||
"yoast/phpunit-polyfills": "^1.0.4"
|
"yoast/phpunit-polyfills": "^1.0.4"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
|
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
|
||||||
|
"directorytree/imapengine": "For uploading sent messages via IMAP, see gmail example",
|
||||||
|
"ext-imap": "Needed to support advanced email address parsing according to RFC822",
|
||||||
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
|
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
|
||||||
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
|
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
|
||||||
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
|
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
|
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
|
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
|
||||||
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.8.0"
|
"source": "https://github.com/PHPMailer/PHPMailer/tree/v7.1.1"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2023-03-06T14:43:22+00:00"
|
"time": "2026-05-18T08:06:14+00:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packages-dev": [],
|
"packages-dev": [],
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
},
|
},
|
||||||
"prefer-stable": false,
|
"prefer-stable": false,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": [],
|
"platform": {},
|
||||||
"platform-dev": [],
|
"platform-dev": {},
|
||||||
"plugin-api-version": "2.3.0"
|
"plugin-api-version": "2.9.0"
|
||||||
}
|
}
|
||||||
|
|||||||
16
config/apache-localhost.conf
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<VirtualHost *:80>
|
||||||
|
ServerName localhost
|
||||||
|
ServerAlias maui.local
|
||||||
|
DocumentRoot /var/www/html/
|
||||||
|
|
||||||
|
DirectoryIndex index.php
|
||||||
|
|
||||||
|
# Serve http://localhost/spot/ from the public web root.
|
||||||
|
Alias /spot /var/www/html/spot/public
|
||||||
|
|
||||||
|
<Directory /var/www/html/spot/public>
|
||||||
|
Options FollowSymLinks
|
||||||
|
AllowOverride None
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
</VirtualHost>
|
||||||
2
settings-sample.php → config/settings-sample.php
Executable file → Normal file
@@ -8,7 +8,7 @@ class Settings
|
|||||||
const DB_NAME = 'spot';
|
const DB_NAME = 'spot';
|
||||||
const DB_ENC = 'utf8mb4';
|
const DB_ENC = 'utf8mb4';
|
||||||
const TEXT_ENC = 'UTF-8';
|
const TEXT_ENC = 'UTF-8';
|
||||||
const TIMEZONE = 'Europe/Paris';
|
const TIMEZONE = 'Europe/Zurich';
|
||||||
const MAIL_SERVER = '';
|
const MAIL_SERVER = '';
|
||||||
const MAIL_FROM = '';
|
const MAIL_FROM = '';
|
||||||
const MAIL_USER = '';
|
const MAIL_USER = '';
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
CREATE TABLE `maps` (
|
|
||||||
`id_map` int(10) UNSIGNED auto_increment,
|
|
||||||
`codename` VARCHAR(100),
|
|
||||||
`geo_name` VARCHAR(100),
|
|
||||||
`min_zoom` TINYINT UNSIGNED,
|
|
||||||
`max_zoom` TINYINT UNSIGNED,
|
|
||||||
`attribution` VARCHAR(100),
|
|
||||||
`led` TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (`id_map`));
|
|
||||||
|
|
||||||
CREATE TABLE `mappings` (
|
|
||||||
`id_mapping` int(10) UNSIGNED auto_increment,
|
|
||||||
`id_map` int(10) UNSIGNED,
|
|
||||||
`id_project` int(10) UNSIGNED,
|
|
||||||
`led` TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (`id_mapping`));
|
|
||||||
|
|
||||||
ALTER TABLE mappings ADD INDEX(`id_map`);
|
|
||||||
ALTER TABLE mappings ADD FOREIGN KEY (`id_map`) REFERENCES maps(`id_map`);
|
|
||||||
ALTER TABLE mappings ADD INDEX(`id_project`);
|
|
||||||
ALTER TABLE mappings ADD FOREIGN KEY (`id_project`) REFERENCES projects(`id_project`);
|
|
||||||
|
|
||||||
INSERT INTO maps(codename, geo_name, min_zoom, max_zoom, attribution) VALUES
|
|
||||||
/*1*/('satellite', 'mapbox.satellite-streets', 0, 19, ''),
|
|
||||||
/*2*/('otm', 'opentopomap', 2, 19, ''),
|
|
||||||
/*3*/('ign_france', 'ign.fr', 2, 19, ''),
|
|
||||||
/*4*/('ign_spain', 'ign.es', 1, 20, ''),
|
|
||||||
/*5*/('linz', 'linz', 0, 17, 'Sourced from LINZ. CC BY 4.0'),
|
|
||||||
/*6*/('usgs', 'usgs', 1, 16, ''),
|
|
||||||
/*7*/('natgeo', 'natgeo.pct', 5, 14, '');
|
|
||||||
|
|
||||||
INSERT INTO mappings(id_map, id_project) VALUES
|
|
||||||
(1, NULL),
|
|
||||||
(2, NULL),
|
|
||||||
(3, 2),
|
|
||||||
(4, 2),
|
|
||||||
(5, 1),
|
|
||||||
(6, 3);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE users ADD gravatar LONGTEXT AFTER email;
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
ALTER TABLE medias ADD timezone CHAR(64) AFTER posted_on;
|
|
||||||
ALTER TABLE messages ADD timezone CHAR(64) AFTER site_time;
|
|
||||||
ALTER TABLE posts ADD timezone CHAR(64) AFTER site_time;
|
|
||||||
|
|
||||||
UPDATE messages
|
|
||||||
SET iso_time = DATE_FORMAT(CONVERT_TZ(LEFT(iso_time, 19),'+00:00','+02:00'), '%Y-%m-%dT%T+0200'),
|
|
||||||
led = led
|
|
||||||
WHERE id_feed = 2;
|
|
||||||
|
|
||||||
UPDATE messages
|
|
||||||
INNER JOIN feeds ON feeds.id_feed = messages.id_feed
|
|
||||||
INNER JOIN projects ON projects.id_project = feeds.id_project
|
|
||||||
SET messages.timezone = projects.timezone,
|
|
||||||
messages.led = messages.led;
|
|
||||||
|
|
||||||
UPDATE posts
|
|
||||||
SET timezone = 'Europe/Paris',
|
|
||||||
led = led;
|
|
||||||
|
|
||||||
UPDATE posts
|
|
||||||
INNER JOIN projects ON projects.id_project = posts.id_project
|
|
||||||
SET posts.id_user = 1,
|
|
||||||
posts.timezone = projects.timezone,
|
|
||||||
posts.led = posts.led
|
|
||||||
WHERE posts.name IN ('francois', 'françois','Francois', 'François', 'franzz');
|
|
||||||
|
|
||||||
UPDATE posts
|
|
||||||
SET timezone = 'Pacific/Auckland',
|
|
||||||
led = led
|
|
||||||
WHERE name = 'nz';
|
|
||||||
|
|
||||||
UPDATE posts
|
|
||||||
SET timezone = 'Atlantic/Madeira',
|
|
||||||
led = led
|
|
||||||
WHERE id_post IN (141, 142);
|
|
||||||
|
|
||||||
UPDATE medias
|
|
||||||
INNER JOIN projects ON projects.id_project = medias.id_project
|
|
||||||
SET medias.timezone = projects.timezone,
|
|
||||||
medias.led = medias.led;
|
|
||||||
|
|
||||||
UPDATE medias
|
|
||||||
SET timezone = 'Atlantic/Madeira',
|
|
||||||
taken_on = posted_on,
|
|
||||||
led = led
|
|
||||||
WHERE id_media IN (64, 65);
|
|
||||||
|
|
||||||
ALTER TABLE projects DROP COLUMN timezone;
|
|
||||||
|
|
||||||
UPDATE maps SET attribution = 'OpenTopoMap (CC-BY-SA)' WHERE id_map = 2;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE users ADD clearance TINYINT(1) DEFAULT 0 AFTER active;
|
|
||||||
UPDATE users SET clearance = 9 WHERE email = 'francois.lutran@gmail.com';
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE messages ADD posted_on TIMESTAMP DEFAULT 0 AFTER battery_state;
|
|
||||||
UPDATE messages SET posted_on = led, led = led;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
ALTER TABLE messages ADD weather_icon VARCHAR(30) AFTER posted_on;
|
|
||||||
ALTER TABLE messages ADD weather_cond VARCHAR(30) AFTER weather_icon;
|
|
||||||
ALTER TABLE messages ADD weather_temp DECIMAL(3,1) AFTER weather_cond;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
INSERT INTO maps (codename, geo_name, min_zoom, max_zoom, attribution) VALUES ('outdoors', 'mapbox.outdoors', 0, 19, '');
|
|
||||||
ALTER TABLE maps ADD COLUMN tile_size SMALLINT UNSIGNED DEFAULT 256 AFTER geo_name;
|
|
||||||
UPDATE maps SET tile_size = 512 WHERE geo_name = 'mapbox.outdoors';
|
|
||||||
UPDATE maps SET tile_size = 512 WHERE geo_name = 'mapbox.satellite-streets';
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
ALTER TABLE maps ADD pattern VARCHAR(200) NOT NULL AFTER geo_name;
|
|
||||||
UPDATE maps SET pattern = CONCAT('http://localhost/geo/?a=tile&id=', geo_name, '&z={z}&x={x}&y={y}') WHERE geo_name <> '';
|
|
||||||
|
|
||||||
ALTER TABLE maps ADD token VARCHAR(4096) AFTER pattern;
|
|
||||||
UPDATE maps SET token = '';
|
|
||||||
|
|
||||||
ALTER TABLE maps DROP geo_name;
|
|
||||||
|
|
||||||
INSERT INTO maps (codename, pattern, token, tile_size, min_zoom, max_zoom, attribution)
|
|
||||||
VALUES ('static', 'http://localhost/geo/?a=tile&id=static&z=13&x={x}&y={y}', '', 400, 13, 13, '');
|
|
||||||
|
|
||||||
INSERT INTO maps (codename, pattern, token, tile_size, min_zoom, max_zoom, attribution)
|
|
||||||
VALUES ('static_marker', 'http://localhost/geo/?a=tile&id=static.marker&z=13&x={x}&y={y}&marker=http://localhost/spot/images/footprint_mapbox.png', '', 400, 13, 13, '');
|
|
||||||
|
|
||||||
UPDATE maps SET max_zoom = 17 WHERE codename = 'otm';
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
ALTER TABLE medias ADD width INT AFTER timezone;
|
|
||||||
ALTER TABLE medias ADD height INT AFTER width;
|
|
||||||
|
|
||||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (1).jpg';
|
|
||||||
UPDATE medias SET width = 2448, height = 3264 WHERE filename = 'image (10).jpg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (11).jpg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (12).jpg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (13).jpg';
|
|
||||||
UPDATE medias SET width = 3648, height = 5472 WHERE filename = 'image (14).jpg';
|
|
||||||
UPDATE medias SET width = 3648, height = 5472 WHERE filename = 'image (15).jpg';
|
|
||||||
UPDATE medias SET width = 2448, height = 3264 WHERE filename = 'image (16).jpg';
|
|
||||||
UPDATE medias SET width = 2448, height = 3264 WHERE filename = 'image (17).jpg';
|
|
||||||
UPDATE medias SET width = 2448, height = 3264 WHERE filename = 'image (18).jpg';
|
|
||||||
UPDATE medias SET width = 2448, height = 3264 WHERE filename = 'image (19).jpg';
|
|
||||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (2).jpg';
|
|
||||||
UPDATE medias SET width = 3648, height = 5472 WHERE filename = 'image (20).jpg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (21).jpg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (22).jpg';
|
|
||||||
UPDATE medias SET width = 2448, height = 3264 WHERE filename = 'image (23).jpg';
|
|
||||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (24).jpg';
|
|
||||||
UPDATE medias SET width = 640, height = 1136 WHERE filename = 'image (25).jpg';
|
|
||||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (26).jpg';
|
|
||||||
UPDATE medias SET width = 2448, height = 3264 WHERE filename = 'image (27).jpg';
|
|
||||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (28).jpg';
|
|
||||||
UPDATE medias SET width = 960, height = 1280 WHERE filename = 'image (29).jpg';
|
|
||||||
UPDATE medias SET width = 2448, height = 3264 WHERE filename = 'image (3).jpg';
|
|
||||||
UPDATE medias SET width = 960, height = 1280 WHERE filename = 'image (30).jpg';
|
|
||||||
UPDATE medias SET width = 2448, height = 3264 WHERE filename = 'image (31).jpg';
|
|
||||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (32).jpg';
|
|
||||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (33).jpg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (34).jpg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (35).jpg';
|
|
||||||
UPDATE medias SET width = 3648, height = 5472 WHERE filename = 'image (36).jpg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (37).jpg';
|
|
||||||
UPDATE medias SET width = 3648, height = 5472 WHERE filename = 'image (38).jpg';
|
|
||||||
UPDATE medias SET width = 2048, height = 1365 WHERE filename = 'image (39).jpg';
|
|
||||||
UPDATE medias SET width = 1280, height = 960 WHERE filename = 'image (4).jpg';
|
|
||||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (40).jpg';
|
|
||||||
UPDATE medias SET width = 8192, height = 1856 WHERE filename = 'image (41).jpg';
|
|
||||||
UPDATE medias SET width = 3648, height = 5472 WHERE filename = 'image (42).jpg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (43).jpg';
|
|
||||||
UPDATE medias SET width = 8192, height = 1856 WHERE filename = 'image (44).jpg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (45).jpg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (46).jpg';
|
|
||||||
UPDATE medias SET width = 640, height = 1038 WHERE filename = 'image (47).jpg';
|
|
||||||
UPDATE medias SET width = 3648, height = 5472 WHERE filename = 'image (48).jpg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (49).jpg';
|
|
||||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (5).jpg';
|
|
||||||
UPDATE medias SET width = 3648, height = 5472 WHERE filename = 'image (50).jpg';
|
|
||||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (51).jpg';
|
|
||||||
UPDATE medias SET width = 3648, height = 5472 WHERE filename = 'image (52).jpg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (53).jpg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (54).jpg';
|
|
||||||
UPDATE medias SET width = 3648, height = 5472 WHERE filename = 'image (55).jpg';
|
|
||||||
UPDATE medias SET width = 3648, height = 5472 WHERE filename = 'image (56).jpg';
|
|
||||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (57).jpg';
|
|
||||||
UPDATE medias SET width = 2448, height = 3264 WHERE filename = 'image (58).jpg';
|
|
||||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (59).jpg';
|
|
||||||
UPDATE medias SET width = 2448, height = 3264 WHERE filename = 'image (6).jpg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (60).jpg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (61).jpg';
|
|
||||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (62).jpg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'image (63).jpg';
|
|
||||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'image (7).jpg';
|
|
||||||
UPDATE medias SET width = 2448, height = 3264 WHERE filename = 'image (8).jpg';
|
|
||||||
UPDATE medias SET width = 2448, height = 3264 WHERE filename = 'image (9).jpg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'DSC01187[1].JPG';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'DSC01477.JPG';
|
|
||||||
UPDATE medias SET width = 2726, height = 4089 WHERE filename = 'DSC03114.jpg';
|
|
||||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = 'IMG_6011.MOV';
|
|
||||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = '156E315D-88AD-492C-BE97-9854FED48FF7.MOV';
|
|
||||||
UPDATE medias SET width = 2576, height = 1932 WHERE filename = '4D06AAF9-A244-4AEF-A1B0-A3C439673840.jpeg';
|
|
||||||
UPDATE medias SET width = 1280, height = 720 WHERE filename = '06361CBA-F514-4789-A498-47D681881DDF.MOV';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'B0A15DCC-B5BB-4BE3-ADBA-CBCB51C178F6.jpeg';
|
|
||||||
UPDATE medias SET width = 1280, height = 720 WHERE filename = 'F827F48B-5CC4-4DA7-A80B-FFED47F6EC60.MOV';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'EC407D2D-0ECF-46C7-BC05-73F48D152182.jpeg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = '7961DA2D-288D-4B0B-B4A6-400673E23BA6.jpeg';
|
|
||||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = 'B43B0160-DAB4-433A-9064-918C40744D27.MOV';
|
|
||||||
UPDATE medias SET width = 1536, height = 2134 WHERE filename = 'C0D5CCF6-FE72-424C-A040-96D8C34FBE9E.jpeg';
|
|
||||||
UPDATE medias SET width = 2154, height = 1850 WHERE filename = '0940EF97-4C27-4304-A663-654F0DE5FAB2.jpeg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '7597DEC0-1BD3-4B93-926B-E4D8A4B28E61.jpeg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '3A1F777D-58F6-40BA-8AAE-1EA236581BA3.jpeg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'C6AC86F9-C819-4866-AA71-A31375E4CCC4.jpeg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'B936E022-4D70-4C9E-9EAE-308FB6F91816.jpeg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'C777A1C7-7C3A-4ADC-8A5C-CEE8FB047B79.jpeg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'B76D8452-FCC4-4FCE-8686-2A4B8C7832FF.jpeg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = '313F8490-E885-42F5-BB59-ACFB12250F0F.jpeg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'F0C1EFDB-0A4F-4FC0-A46D-A396D8724054.jpeg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'E9339EEF-A21C-48A6-B6CC-23C3A512DA29.jpeg';
|
|
||||||
UPDATE medias SET width = 1280, height = 720 WHERE filename = '5150FFEF-A715-4F5A-8562-1502BE778B6A.MOV';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'C8CD8B78-3EC9-4700-9A46-478556496239.jpeg';
|
|
||||||
UPDATE medias SET width = 1536, height = 2048 WHERE filename = '3D83F7D3-B746-440F-8DD3-6DADF230AC28.jpeg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'B9121A78-0B21-42E2-8392-A60BCE240EFE.jpeg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '5A3755D0-81CB-43B1-9750-AB34F413D526.jpeg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'B9EFBC4F-4FC4-4D03-9722-9A1FF61F3B8E.jpeg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'F7C6FFFF-BF61-42BF-949B-72CD2CAAEFE2.jpeg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'B0557309-579D-4C1A-863F-CF4898C42E77.jpeg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = '2C75F8E9-C3E0-4FD5-AF2E-CE339545D6BC.jpeg';
|
|
||||||
UPDATE medias SET width = 3891, height = 2917 WHERE filename = 'C6E18BC2-B407-4F2C-82F9-87244A9AD311.jpeg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '0A4B5812-D5CD-4CB0-989B-C95D15526F1A.jpeg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = '1548CCA0-C8C8-4DC6-BD96-ABAA4589E825.jpeg';
|
|
||||||
UPDATE medias SET width = 4685, height = 3116 WHERE filename = '7F80807F-D40F-4714-81AE-3CD7CD3FE8CD.jpeg';
|
|
||||||
UPDATE medias SET width = 5283, height = 3519 WHERE filename = '32F5B460-958D-4A13-92BD-371BBC0DA769.jpeg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'F99EEC87-C5CC-49AD-BCBF-8A14ADADD262.jpeg';
|
|
||||||
UPDATE medias SET width = 8192, height = 1856 WHERE filename = 'C243E61F-2A31-4179-97B4-AD54C02E88D5.jpeg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '85807A81-91CB-4B10-861B-1B3C5A60F066.jpeg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'DE99B389-738F-48CB-BEF1-D448DEB16E7E.jpeg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = '6BBD7291-BDFC-460F-A620-9D2F68AF637F.jpeg';
|
|
||||||
UPDATE medias SET width = 1280, height = 720 WHERE filename = '7DB6662E-353A-4AC5-9331-D9122D956FE7.MOV';
|
|
||||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = '974841E5-AB13-4067-AD5F-3EDEFEAE28E4.MOV';
|
|
||||||
UPDATE medias SET width = 1536, height = 2304 WHERE filename = '09F007C7-53BA-4214-B3AF-DB0E0CDD1994.jpeg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '6B92D63D-EDA5-4787-8AC7-4A0F65DC071F.jpeg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'BAF3BB55-E012-40CD-8D8B-27C4398C1D00.jpeg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '8FA2F4F0-F220-4E98-9BC0-ACB607FC2F8E.jpeg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'C260B1AD-BA4C-4835-B132-A529A9319D4D.jpeg';
|
|
||||||
UPDATE medias SET width = 1536, height = 2304 WHERE filename = '92F8E35B-5FF8-414D-A627-46FF45E6CCCA.jpeg';
|
|
||||||
UPDATE medias SET width = 1280, height = 720 WHERE filename = '33AA17D3-CFD6-426A-B2D5-CD639F53C081.MOV';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '0015713D-18A2-45A7-B26F-3954AC0979D3.jpeg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '223A23C5-71BB-4419-8765-CE51F3E69480.jpeg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '9ED663DD-0257-4211-A993-C86CDAC9066B.jpeg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '1ECF5050-8821-441C-8DF7-E93B24941F8A.jpeg';
|
|
||||||
UPDATE medias SET width = 1536, height = 2304 WHERE filename = '68211D98-D6E7-4D7C-BB82-7E5E30884C7D.jpeg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '61747CFA-859E-4859-852F-3AE2650C7578.jpeg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'F17CD761-DFDC-4E5F-97A7-C16035A8D8EB.jpeg';
|
|
||||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = '7EE0B25A-0BC6-4BF1-9CDF-9E4547CFEAF4.MOV';
|
|
||||||
UPDATE medias SET width = 3503, height = 2625 WHERE filename = '273092DD-F6CE-4C74-8878-A4D94A9C78F9.jpeg';
|
|
||||||
UPDATE medias SET width = 1280, height = 720 WHERE filename = '4853B2CB-5BC4-45FE-AD02-0980953DB59A.MOV';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '789210A7-293D-4119-87EE-1B7CA0ACBE0A.jpeg';
|
|
||||||
UPDATE medias SET width = 2665, height = 1996 WHERE filename = '52D57195-22A2-423B-AA35-05D48B889950.jpeg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'F5644C0F-5244-4574-BBE1-4059794F87E1.jpeg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = '7BB3D57A-D5BE-45D5-8CAC-C3172A89C67A.jpeg';
|
|
||||||
UPDATE medias SET width = 2687, height = 2013 WHERE filename = '8345C480-1274-45DB-A990-40913DD219A7.jpeg';
|
|
||||||
UPDATE medias SET width = 4128, height = 3096 WHERE filename = '28567B3E-A067-41AE-AE69-37DD3C98F820.jpeg';
|
|
||||||
UPDATE medias SET width = 3096, height = 4128 WHERE filename = '20190827_133201_DxO.jpg';
|
|
||||||
UPDATE medias SET width = 3096, height = 4128 WHERE filename = '20190827_133201_DxO (1).jpg';
|
|
||||||
UPDATE medias SET width = 3096, height = 4128 WHERE filename = '20190827_133201_DxO (2).jpg';
|
|
||||||
UPDATE medias SET width = 3096, height = 4128 WHERE filename = '20190827_133201_DxO (3).jpg';
|
|
||||||
UPDATE medias SET width = 2000, height = 3000 WHERE filename = 'Untitled.png';
|
|
||||||
UPDATE medias SET width = 2000, height = 3000 WHERE filename = 'Untitled (1).png';
|
|
||||||
UPDATE medias SET width = 2000, height = 3000 WHERE filename = 'Untitled (2).png';
|
|
||||||
UPDATE medias SET width = 2000, height = 3000 WHERE filename = 'Untitled (3).png';
|
|
||||||
UPDATE medias SET width = 2000, height = 3000 WHERE filename = 'Untitled (4).png';
|
|
||||||
UPDATE medias SET width = 2000, height = 3000 WHERE filename = 'Untitled (5).png';
|
|
||||||
UPDATE medias SET width = 2000, height = 3000 WHERE filename = 'Untitled (6).png';
|
|
||||||
UPDATE medias SET width = 2000, height = 3000 WHERE filename = 'Untitled (7).png';
|
|
||||||
UPDATE medias SET width = 2000, height = 3000 WHERE filename = 'Untitled (8).png';
|
|
||||||
UPDATE medias SET width = 365, height = 600 WHERE filename = 'asunabg.png';
|
|
||||||
UPDATE medias SET width = 365, height = 600 WHERE filename = 'asunabg (1).png';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'iPhone 6S (1).jpeg';
|
|
||||||
UPDATE medias SET width = 3024, height = 4032 WHERE filename = 'IMG_2591.JPG';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'CF0432E7-5221-433D-A681-DBABE131EE99.jpeg';
|
|
||||||
UPDATE medias SET width = 694, height = 628 WHERE filename = 'Capture.PNG';
|
|
||||||
UPDATE medias SET width = 3024, height = 4032 WHERE filename = 'IMG_2232 (1).jpg';
|
|
||||||
UPDATE medias SET width = 1350, height = 900 WHERE filename = 'Photo 2014-06-21 20-17-11 (_MG_1217).jpg';
|
|
||||||
UPDATE medias SET width = 2000, height = 1500 WHERE filename = 'Photo 2015-01-11 20-30-03 (P1020945) (9).jpg';
|
|
||||||
UPDATE medias SET width = 1154, height = 770 WHERE filename = 'Photo 2014-06-21 20-17-11 (_MG_1217)_DxO (3).jpg';
|
|
||||||
UPDATE medias SET width = 3024, height = 4032 WHERE filename = 'IMG_2232 (2).jpg';
|
|
||||||
UPDATE medias SET width = 2320, height = 3088 WHERE filename = '9C0965E1-6A40-4691-8127-35B2F1D6BB58.jpeg';
|
|
||||||
UPDATE medias SET width = 3024, height = 4032 WHERE filename = 'IMG_3077.jpg';
|
|
||||||
UPDATE medias SET width = 3024, height = 4032 WHERE filename = 'AF7A1DBC-E1CC-4FEE-8411-AC25BF69C0B5.jpeg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'F7FC1246-6304-4662-A5F2-230A29728EBC.jpeg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'CA5BA9A6-2A53-430A-A0BF-B8D0C0CDC983.jpeg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = '12E87114-6B4B-4051-945C-9042FBBF5E3E.jpeg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'F97B5058-C7D7-4B25-B727-914A8897CE58.jpeg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'D1E626E6-6F49-4F62-A3D5-B19CBC906F7B.jpeg';
|
|
||||||
UPDATE medias SET width = 3024, height = 4032 WHERE filename = 'IMG_1228_DxO.jpg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'IMG_1268_DxO.jpg';
|
|
||||||
UPDATE medias SET width = 3024, height = 4032 WHERE filename = 'IMG_2232.jpg';
|
|
||||||
UPDATE medias SET width = 2475, height = 2475 WHERE filename = 'IMG_2232_DxO (1).jpg';
|
|
||||||
UPDATE medias SET width = 2475, height = 2475 WHERE filename = 'IMG_2232_DxO.jpg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'IMG_2317.jpg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'IMG_2574 (1).jpg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'IMG_2574 (2).jpg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'IMG_2574.jpg';
|
|
||||||
UPDATE medias SET width = 1344, height = 672 WHERE filename = 'PHOTO-2019-12-24-23-40-26.jpg';
|
|
||||||
UPDATE medias SET width = 1512, height = 2688 WHERE filename = 'PXL_20210405_083835976.jpg';
|
|
||||||
UPDATE medias SET width = 3840, height = 2160 WHERE filename = 'PXL_20210405_091516836.jpg';
|
|
||||||
UPDATE medias SET width = 1350, height = 900 WHERE filename = 'Photo 2014-06-21 20-17-11 (_MG_1217)_DxO (1).jpg';
|
|
||||||
UPDATE medias SET width = 1350, height = 900 WHERE filename = 'Photo 2014-06-21 20-17-11 (_MG_1217)_DxO (2).jpg';
|
|
||||||
UPDATE medias SET width = 1350, height = 900 WHERE filename = 'Photo 2014-06-21 20-17-11 (_MG_1217)_DxO.jpg';
|
|
||||||
UPDATE medias SET width = 2000, height = 1500 WHERE filename = 'Photo 2015-01-11 20-30-03 (P1020945) (1).jpg';
|
|
||||||
UPDATE medias SET width = 2000, height = 1500 WHERE filename = 'Photo 2015-01-11 20-30-03 (P1020945) (2).jpg';
|
|
||||||
UPDATE medias SET width = 2000, height = 1500 WHERE filename = 'Photo 2015-01-11 20-30-03 (P1020945) (3).jpg';
|
|
||||||
UPDATE medias SET width = 2000, height = 1500 WHERE filename = 'Photo 2015-01-11 20-30-03 (P1020945) (4).jpg';
|
|
||||||
UPDATE medias SET width = 2000, height = 1500 WHERE filename = 'Photo 2015-01-11 20-30-03 (P1020945) (5).jpg';
|
|
||||||
UPDATE medias SET width = 2000, height = 1500 WHERE filename = 'Photo 2015-01-11 20-30-03 (P1020945) (6).jpg';
|
|
||||||
UPDATE medias SET width = 2000, height = 1500 WHERE filename = 'Photo 2015-01-11 20-30-03 (P1020945) (7).jpg';
|
|
||||||
UPDATE medias SET width = 2000, height = 1500 WHERE filename = 'Photo 2015-01-11 20-30-03 (P1020945) (8).jpg';
|
|
||||||
UPDATE medias SET width = 2000, height = 1500 WHERE filename = 'Photo 2015-01-11 20-30-03 (P1020945).jpg';
|
|
||||||
UPDATE medias SET width = 1500, height = 2000 WHERE filename = 'Photo 2015-01-19 19-59-34 (P1040055).jpg';
|
|
||||||
UPDATE medias SET width = 3264, height = 2448 WHERE filename = 'TEST.jpg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'image (64).jpg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'image (65).jpg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'image.jpg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = '083932A9-4248-4208-91CC-34DCDC374E7A.jpeg';
|
|
||||||
UPDATE medias SET width = 3024, height = 4032 WHERE filename = '102ED557-69B0-41F2-9193-DF52FEEFD35C.jpeg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = '45289F93-5D82-40DE-A0B1-1871317C56C1.jpeg';
|
|
||||||
UPDATE medias SET width = 1200, height = 1600 WHERE filename = '5F97CB55-46C7-4BB7-9335-BBDFF77F1310.jpeg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = '634716D8-D7E7-4128-B0D2-EEB4437411F3.jpeg';
|
|
||||||
UPDATE medias SET width = 3024, height = 4032 WHERE filename = '6C111386-591F-450D-9794-E812FB6FF036.jpeg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = '6D718B33-020D-460A-9194-D637A2590ABC.jpeg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = '866196A9-5428-4E6C-8C4A-03DB284A9448.jpeg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'A68E6383-095C-4CB2-876B-4EC59DB24419.jpeg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'APPLE (1).jpeg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'APPLE.jpeg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'B9EFBC4F-4FC4-4D03-9722-9A1FF61F3B8E (1).jpeg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'B9EFBC4F-4FC4-4D03-9722-9A1FF61F3B8E (2).jpeg';
|
|
||||||
UPDATE medias SET width = 5472, height = 3648 WHERE filename = 'B9EFBC4F-4FC4-4D03-9722-9A1FF61F3B8E (3).jpeg';
|
|
||||||
UPDATE medias SET width = 1152, height = 1536 WHERE filename = 'BA37E256-C0B2-4DE6-B0E0-659EB2C3411B.jpeg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'D4009C41-7B4C-42D5-9FB4-9CEC7CC1B4B0.jpeg';
|
|
||||||
UPDATE medias SET width = 1200, height = 1600 WHERE filename = 'DDFADE5F-2785-4168-9EAB-D63818566929.jpeg';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = 'iPhone 6S.jpeg';
|
|
||||||
UPDATE medias SET width = 365, height = 600 WHERE filename = 'asunabg (2).png';
|
|
||||||
UPDATE medias SET width = 365, height = 600 WHERE filename = 'asunabg (3).png';
|
|
||||||
UPDATE medias SET width = 365, height = 600 WHERE filename = 'asunabg (4).png';
|
|
||||||
UPDATE medias SET width = 200, height = 120 WHERE filename = 'ffprobeall.png';
|
|
||||||
UPDATE medias SET width = 1280, height = 720 WHERE filename = 'temp_60a6954ea732b.png';
|
|
||||||
UPDATE medias SET width = 1280, height = 720 WHERE filename = 'temp_60a695501d507.png';
|
|
||||||
UPDATE medias SET width = 1080, height = 1920 WHERE filename = 'temp_60a695509009d.png';
|
|
||||||
UPDATE medias SET width = 1080, height = 1920 WHERE filename = 'temp_60a695520a9a0.png';
|
|
||||||
UPDATE medias SET width = 1080, height = 1920 WHERE filename = 'temp_60a695535d689.png';
|
|
||||||
UPDATE medias SET width = 1280, height = 720 WHERE filename = '7DB6662E-353A-4AC5-9331-D9122D956FE7 (1).MOV';
|
|
||||||
UPDATE medias SET width = 1280, height = 720 WHERE filename = '7DB6662E-353A-4AC5-9331-D9122D956FE7 (2).MOV';
|
|
||||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = 'IMG_2584 (1).MOV';
|
|
||||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = 'IMG_2584.MOV';
|
|
||||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = 'IMG_2585 (1).MOV';
|
|
||||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = 'IMG_2585 (2).MOV';
|
|
||||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = 'IMG_2585 (3).MOV';
|
|
||||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = 'IMG_2585 (4).MOV';
|
|
||||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = 'IMG_2585 (5).MOV';
|
|
||||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = 'IMG_2585.MOV';
|
|
||||||
UPDATE medias SET width = 1920, height = 1080 WHERE filename = 'iPhone 6S.MOV';
|
|
||||||
UPDATE medias SET width = 4032, height = 3024 WHERE filename = '1620E5B9-C65D-4252-A495-18D5CAD63C6E.jpeg';
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
ALTER TABLE messages MODIFY ref_msg_id VARCHAR(15) NOT NULL;
|
|
||||||
ALTER TABLE messages ADD display BOOLEAN DEFAULT 1 AFTER weather_temp;
|
|
||||||
|
|
||||||
UPDATE messages SET display = 0 WHERE id_message = 197;
|
|
||||||
UPDATE messages SET display = 0 WHERE id_message = 216;
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
CREATE TABLE `projects` (
|
|
||||||
`id_project` int(10) UNSIGNED auto_increment,
|
|
||||||
`codename` VARCHAR(100),
|
|
||||||
`name` VARCHAR(100),
|
|
||||||
`active_from` DATETIME,
|
|
||||||
`active_to` DATETIME,
|
|
||||||
`geofile` VARCHAR(50),
|
|
||||||
`timezone` VARCHAR(100),
|
|
||||||
`led` TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (`id_project`));
|
|
||||||
|
|
||||||
INSERT INTO projects (name, codename, active_from, active_to, geofile, timezone) VALUES ('Te Araroa', 'te_araroa', '2015-12-29 00:00:00', '2016-03-05 23:59:59', 'te_araroa.geojson', 'Pacific/Auckland');
|
|
||||||
INSERT INTO projects (name, codename, active_from, active_to, geofile, timezone) VALUES ('HRP', 'hrp', '2019-06-01 00:00:00', '2019-09-10 23:59:59', 'hrp.geojson', 'Europe/Paris');
|
|
||||||
|
|
||||||
ALTER TABLE feeds ADD COLUMN id_project int(10) UNSIGNED AFTER id_spot;
|
|
||||||
ALTER TABLE feeds ADD last_update DATETIME AFTER status;
|
|
||||||
ALTER TABLE feeds ADD FOREIGN KEY (`id_project`) REFERENCES projects(`id_project`);
|
|
||||||
UPDATE feeds SET last_update = led;
|
|
||||||
UPDATE feeds SET id_project = 1;
|
|
||||||
|
|
||||||
ALTER TABLE posts ADD COLUMN id_project int(10) UNSIGNED AFTER id_post;
|
|
||||||
ALTER TABLE posts ADD timestamp DATETIME AFTER content;
|
|
||||||
ALTER TABLE posts ADD FOREIGN KEY (`id_project`) REFERENCES projects(`id_project`);
|
|
||||||
UPDATE posts SET timestamp = led;
|
|
||||||
UPDATE posts SET id_project = 1;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
ALTER TABLE medias ADD latitude DECIMAL(7,5) AFTER timezone;
|
|
||||||
ALTER TABLE medias ADD longitude DECIMAL(8,5) AFTER latitude;
|
|
||||||
ALTER TABLE medias ADD altitude SMALLINT AFTER longitude;
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
CREATE TABLE `pictures` (
|
|
||||||
`id_picture` int(10) UNSIGNED auto_increment,
|
|
||||||
`id_project` int(10) UNSIGNED,
|
|
||||||
`filename` VARCHAR(100) NOT NULL,
|
|
||||||
`taken_on` DATETIME,
|
|
||||||
`timestamp` DATETIME,
|
|
||||||
`rotate` SMALLINT,
|
|
||||||
`led` TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (`id_picture`),
|
|
||||||
UNIQUE KEY `uni_file_name` (`filename`)
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE pictures ADD INDEX(`id_project`);
|
|
||||||
ALTER TABLE pictures ADD FOREIGN KEY (`id_project`) REFERENCES projects(`id_project`);
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
ALTER TABLE messages ADD iso_time VARCHAR(24) AFTER longitude;
|
|
||||||
ALTER TABLE messages CHANGE COLUMN timestamp site_time TIMESTAMP DEFAULT 0;
|
|
||||||
ALTER TABLE messages CHANGE COLUMN unix_timestamp unix_time INT;
|
|
||||||
UPDATE messages SET iso_time = CONCAT(REPLACE(CONVERT_TZ(FROM_UNIXTIME(unix_time), @@session.time_zone, 'Pacific/Auckland'), ' ', 'T'), '+1200') WHERE id_feed = 1;
|
|
||||||
|
|
||||||
ALTER TABLE feeds MODIFY last_update TIMESTAMP DEFAULT 0;
|
|
||||||
|
|
||||||
ALTER TABLE projects MODIFY active_from TIMESTAMP DEFAULT 0;
|
|
||||||
ALTER TABLE projects MODIFY active_to TIMESTAMP DEFAULT 0;
|
|
||||||
|
|
||||||
ALTER TABLE posts CHANGE COLUMN timestamp site_time TIMESTAMP DEFAULT 0;
|
|
||||||
|
|
||||||
ALTER TABLE pictures CHANGE COLUMN timestamp posted_on TIMESTAMP DEFAULT 0;
|
|
||||||
UPDATE pictures INNER JOIN projects USING(id_project) SET taken_on = CONVERT_TZ(taken_on, projects.timezone, @@session.time_zone) where id_project = 1;
|
|
||||||
ALTER TABLE pictures MODIFY taken_on TIMESTAMP DEFAULT 0;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
UPDATE projects SET geofile = REPLACE(geofile, '.geojson', '');
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
RENAME TABLE pictures TO medias;
|
|
||||||
ALTER TABLE medias CHANGE COLUMN id_picture id_media INT(10) UNSIGNED NOT NULL auto_increment;
|
|
||||||
ALTER TABLE medias ADD COLUMN type VARCHAR(20) AFTER filename;
|
|
||||||
UPDATE medias SET type = 'image';
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
/* Remove NO_ZERO_DATE mode, checks mode with: SELECT @@SQL_MODE, @@GLOBAL.SQL_MODE; and: SET @@SQL_MODE = REPLACE(@@SQL_MODE, 'NO_ZERO_DATE', ''); */
|
|
||||||
ALTER TABLE medias ADD comment LONGTEXT AFTER rotate;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE projects DROP COLUMN geofile;
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
CREATE TABLE `users` (
|
|
||||||
`id_user` int(10) UNSIGNED auto_increment,
|
|
||||||
`name` VARCHAR(100),
|
|
||||||
`email` VARCHAR(320),
|
|
||||||
`language` VARCHAR(2),
|
|
||||||
`active` BOOLEAN,
|
|
||||||
`led` TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (`id_user`),
|
|
||||||
UNIQUE KEY `uni_email` (`email`)
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE posts ADD COLUMN id_user int(10) UNSIGNED AFTER id_project;
|
|
||||||
ALTER TABLE posts ADD INDEX(`id_user`);
|
|
||||||
ALTER TABLE posts ADD FOREIGN KEY (`id_user`) REFERENCES users(`id_user`);
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
ALTER TABLE users ADD COLUMN timezone char(64) AFTER language;
|
|
||||||
ALTER TABLE users MODIFY COLUMN email VARCHAR(320) NOT NULL;
|
|
||||||
UPDATE users SET timezone = 'Europe/Paris';
|
|
||||||
|
|
||||||
ALTER TABLE projects MODIFY COLUMN timezone char(64);
|
|
||||||
265
gaia/tracks.js
782
gaia/upload.js
@@ -1,782 +0,0 @@
|
|||||||
// ==UserScript==
|
|
||||||
// @name GaiaGps Uploader
|
|
||||||
// @namespace https://greasyfork.org/users/583371
|
|
||||||
// @description Allow the user to upload multiple files at once and more than 1000 waypoints
|
|
||||||
// @grant none
|
|
||||||
// @version 3.1.2
|
|
||||||
// @author Franzz
|
|
||||||
// @license GNU GPLv3
|
|
||||||
// @match https://www.gaiagps.com/map/*
|
|
||||||
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js
|
|
||||||
// ==/UserScript==
|
|
||||||
|
|
||||||
/* jshint esversion: 6 */
|
|
||||||
|
|
||||||
//Ctrl+Alt+Shift+I to open network tab on addon
|
|
||||||
|
|
||||||
/* GPXParser - v3.0.8 - https://github.com/Luuka/GPXParser.js/blob/master/src/GPXParser.js */
|
|
||||||
/* Personnal modifications have been applied, care on upgrade */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GPX file parser
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
let gpxParser = function () {
|
|
||||||
this.xmlSource = "";
|
|
||||||
this.metadata = {};
|
|
||||||
this.waypoints = [];
|
|
||||||
this.tracks = [];
|
|
||||||
this.routes = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a gpx formatted string to a GPXParser Object
|
|
||||||
*
|
|
||||||
* @param {string} gpxstring - A GPX formatted String
|
|
||||||
*
|
|
||||||
* @return {gpxParser} A GPXParser object
|
|
||||||
*/
|
|
||||||
gpxParser.prototype.parse = function (gpxstring) {
|
|
||||||
let keepThis = this;
|
|
||||||
|
|
||||||
let domParser = new window.DOMParser();
|
|
||||||
this.xmlSource = domParser.parseFromString(gpxstring, 'text/xml');
|
|
||||||
|
|
||||||
let metadata = this.xmlSource.querySelector('metadata');
|
|
||||||
if(metadata != null){
|
|
||||||
this.metadata.name = this.getElementValue(metadata, "name");
|
|
||||||
this.metadata.desc = this.getElementValue(metadata, "desc");
|
|
||||||
this.metadata.time = this.getElementValue(metadata, "time");
|
|
||||||
|
|
||||||
let author = {};
|
|
||||||
let authorElem = metadata.querySelector('author');
|
|
||||||
if(authorElem != null){
|
|
||||||
author.name = this.getElementValue(authorElem, "name");
|
|
||||||
author.email = {};
|
|
||||||
let emailElem = authorElem.querySelector('email');
|
|
||||||
if(emailElem != null){
|
|
||||||
author.email.id = emailElem.getAttribute("id");
|
|
||||||
author.email.domain = emailElem.getAttribute("domain");
|
|
||||||
}
|
|
||||||
|
|
||||||
let link = {};
|
|
||||||
let linkElem = authorElem.querySelector('link');
|
|
||||||
if(linkElem != null){
|
|
||||||
link.href = linkElem.getAttribute('href');
|
|
||||||
link.text = this.getElementValue(linkElem, "text");
|
|
||||||
link.type = this.getElementValue(linkElem, "type");
|
|
||||||
}
|
|
||||||
author.link = link;
|
|
||||||
}
|
|
||||||
this.metadata.author = author;
|
|
||||||
|
|
||||||
let link = {};
|
|
||||||
let linkElem = this.queryDirectSelector(metadata, 'link');
|
|
||||||
if(linkElem != null){
|
|
||||||
link.href = linkElem.getAttribute('href');
|
|
||||||
link.text = this.getElementValue(linkElem, "text");
|
|
||||||
link.type = this.getElementValue(linkElem, "type");
|
|
||||||
this.metadata.link = link;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var wpts = [].slice.call(this.xmlSource.querySelectorAll('wpt'));
|
|
||||||
for (let idx in wpts){
|
|
||||||
var wpt = wpts[idx];
|
|
||||||
let pt = {};
|
|
||||||
pt.name = keepThis.getElementValue(wpt, "name");
|
|
||||||
pt.lat = parseFloat(wpt.getAttribute("lat"));
|
|
||||||
pt.lon = parseFloat(wpt.getAttribute("lon"));
|
|
||||||
//let floatValue = parseFloat(keepThis.getElementValue(wpt, "ele"));
|
|
||||||
//pt.ele = isNaN(floatValue) ? null : floatValue;
|
|
||||||
pt.ele = parseFloat(keepThis.getElementValue(wpt, "ele")) || null;
|
|
||||||
pt.cmt = keepThis.getElementValue(wpt, "cmt");
|
|
||||||
pt.desc = keepThis.getElementValue(wpt, "desc");
|
|
||||||
pt.sym = keepThis.getElementValue(wpt, "sym");
|
|
||||||
|
|
||||||
//let time = keepThis.getElementValue(wpt, "time");
|
|
||||||
//pt.time = time == null ? null : new Date(time);
|
|
||||||
pt.time = (keepThis.getElementValue(wpt, "time") || keepThis.metadata.time) || null;
|
|
||||||
|
|
||||||
keepThis.waypoints.push(pt);
|
|
||||||
}
|
|
||||||
|
|
||||||
var rtes = [].slice.call(this.xmlSource.querySelectorAll('rte'));
|
|
||||||
for (let idx in rtes){
|
|
||||||
let rte = rtes[idx];
|
|
||||||
let route = {};
|
|
||||||
route.name = keepThis.getElementValue(rte, "name");
|
|
||||||
route.cmt = keepThis.getElementValue(rte, "cmt");
|
|
||||||
route.desc = keepThis.getElementValue(rte, "desc");
|
|
||||||
route.src = keepThis.getElementValue(rte, "src");
|
|
||||||
route.number= keepThis.getElementValue(rte, "number");
|
|
||||||
|
|
||||||
let type = keepThis.queryDirectSelector(rte, "type");
|
|
||||||
route.type = type != null ? type.innerHTML : null;
|
|
||||||
|
|
||||||
let link = {};
|
|
||||||
let linkElem= rte.querySelector('link');
|
|
||||||
if(linkElem != null){
|
|
||||||
link.href = linkElem.getAttribute('href');
|
|
||||||
link.text = keepThis.getElementValue(linkElem, "text");
|
|
||||||
link.type = keepThis.getElementValue(linkElem, "type");
|
|
||||||
}
|
|
||||||
route.link = link;
|
|
||||||
|
|
||||||
let routepoints = [];
|
|
||||||
var rtepts = [].slice.call(rte.querySelectorAll('rtept'));
|
|
||||||
|
|
||||||
for (let idxIn in rtepts){
|
|
||||||
let rtept = rtepts[idxIn];
|
|
||||||
let pt = {};
|
|
||||||
pt.lat = parseFloat(rtept.getAttribute("lat"));
|
|
||||||
pt.lon = parseFloat(rtept.getAttribute("lon"));
|
|
||||||
//let floatValue = parseFloat(keepThis.getElementValue(rtept, "ele"));
|
|
||||||
//pt.ele = isNaN(floatValue) ? null : floatValue;
|
|
||||||
pt.ele = parseFloat(keepThis.getElementValue(rtept, "ele")) || null;
|
|
||||||
|
|
||||||
//let time = keepThis.getElementValue(rtept, "time");
|
|
||||||
//pt.time = time == null ? null : new Date(time);
|
|
||||||
pt.time = (keepThis.getElementValue(rtept, "time") || keepThis.metadata.time) || null;
|
|
||||||
|
|
||||||
routepoints.push(pt);
|
|
||||||
}
|
|
||||||
|
|
||||||
//route.distance = keepThis.calculDistance(routepoints);
|
|
||||||
//route.elevation = keepThis.calcElevation(routepoints);
|
|
||||||
//route.slopes = keepThis.calculSlope(routepoints, route.distance.cumul);
|
|
||||||
route.points = routepoints;
|
|
||||||
|
|
||||||
keepThis.routes.push(route);
|
|
||||||
}
|
|
||||||
|
|
||||||
var trks = [].slice.call(this.xmlSource.querySelectorAll('trk'));
|
|
||||||
for (let idx in trks){
|
|
||||||
let trk = trks[idx];
|
|
||||||
let track = {};
|
|
||||||
|
|
||||||
track.name = keepThis.getElementValue(trk, "name");
|
|
||||||
track.cmt = keepThis.getElementValue(trk, "cmt");
|
|
||||||
track.desc = keepThis.getElementValue(trk, "desc");
|
|
||||||
track.src = keepThis.getElementValue(trk, "src");
|
|
||||||
track.number= keepThis.getElementValue(trk, "number");
|
|
||||||
track.color = keepThis.getElementValue(trk, "DisplayColor");
|
|
||||||
track.time = keepThis.metadata.time;
|
|
||||||
|
|
||||||
let type = keepThis.queryDirectSelector(trk, "type");
|
|
||||||
track.type = type != null ? type.innerHTML : null;
|
|
||||||
|
|
||||||
let link = {};
|
|
||||||
let linkElem= trk.querySelector('link');
|
|
||||||
if(linkElem != null){
|
|
||||||
link.href = linkElem.getAttribute('href');
|
|
||||||
link.text = keepThis.getElementValue(linkElem, "text");
|
|
||||||
link.type = keepThis.getElementValue(linkElem, "type");
|
|
||||||
}
|
|
||||||
track.link = link;
|
|
||||||
|
|
||||||
let trackpoints = [];
|
|
||||||
let trkpts = [].slice.call(trk.querySelectorAll('trkpt'));
|
|
||||||
for (let idxIn in trkpts){
|
|
||||||
var trkpt = trkpts[idxIn];
|
|
||||||
let pt = {};
|
|
||||||
pt.lat = parseFloat(trkpt.getAttribute("lat"));
|
|
||||||
pt.lon = parseFloat(trkpt.getAttribute("lon"));
|
|
||||||
//let floatValue = parseFloat(keepThis.getElementValue(trkpt, "ele"));
|
|
||||||
//pt.ele = isNaN(floatValue) ? null : floatValue;
|
|
||||||
pt.ele = parseFloat(keepThis.getElementValue(trkpt, "ele")) || null;
|
|
||||||
|
|
||||||
//let time = keepThis.getElementValue(trkpt, "time");
|
|
||||||
//pt.time = time == null ? null : new Date(time);
|
|
||||||
pt.time = (keepThis.getElementValue(trkpt, "time") || keepThis.metadata.time) || null;
|
|
||||||
|
|
||||||
trackpoints.push(pt);
|
|
||||||
}
|
|
||||||
//track.distance = keepThis.calculDistance(trackpoints);
|
|
||||||
//track.elevation = keepThis.calcElevation(trackpoints);
|
|
||||||
//track.slopes = keepThis.calculSlope(trackpoints, track.distance.cumul);
|
|
||||||
track.points = trackpoints;
|
|
||||||
|
|
||||||
keepThis.tracks.push(track);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get value from a XML DOM element
|
|
||||||
*
|
|
||||||
* @param {Element} parent - Parent DOM Element
|
|
||||||
* @param {string} needle - Name of the searched element
|
|
||||||
*
|
|
||||||
* @return {} The element value
|
|
||||||
*/
|
|
||||||
gpxParser.prototype.getElementValue = function(parent, needle){
|
|
||||||
let elem = parent.querySelector(needle);
|
|
||||||
if(elem != null){
|
|
||||||
//Get value (in case of CDATA)
|
|
||||||
let sValue = (elem.innerHTML != undefined && elem.innerHTML.substring(0, 8) != '<![CDATA') ? elem.innerHTML : elem.childNodes[0].data;
|
|
||||||
|
|
||||||
//If decoded HTML, re-encode
|
|
||||||
if(sValue.substr(0, 4)== '<') sValue = $('<div>').html(sValue).text();
|
|
||||||
|
|
||||||
//Strip HTML tags & trim value
|
|
||||||
return $('<div>').html(sValue).text().trim();
|
|
||||||
}
|
|
||||||
return elem;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search the value of a direct child XML DOM element
|
|
||||||
*
|
|
||||||
* @param {Element} parent - Parent DOM Element
|
|
||||||
* @param {string} needle - Name of the searched element
|
|
||||||
*
|
|
||||||
* @return {} The element value
|
|
||||||
*/
|
|
||||||
gpxParser.prototype.queryDirectSelector = function(parent, needle) {
|
|
||||||
|
|
||||||
let elements = parent.querySelectorAll(needle);
|
|
||||||
let finalElem = elements[0];
|
|
||||||
|
|
||||||
if(elements.length > 1) {
|
|
||||||
let directChilds = parent.childNodes;
|
|
||||||
|
|
||||||
for(idx in directChilds) {
|
|
||||||
elem = directChilds[idx];
|
|
||||||
if(elem.tagName === needle) {
|
|
||||||
finalElem = elem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return finalElem;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
class Gaia {
|
|
||||||
static get URL() { return 'https://www.gaiagps.com'; }
|
|
||||||
static get API() { return Gaia.URL+'/api/objects'; }
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
this.asFiles = [];
|
|
||||||
this.aoWaypoints = [];
|
|
||||||
this.aoTracks = [];
|
|
||||||
this.asFolders = {};
|
|
||||||
this.progress = {current:0, total:0};
|
|
||||||
}
|
|
||||||
|
|
||||||
setLayout() {
|
|
||||||
this.reset();
|
|
||||||
|
|
||||||
/* FIXME: adapts on GaiaGPS upgrade */
|
|
||||||
let $InputButton = $('a[href="https://help.gaiagps.com/hc/en-us/articles/360052763513"]').parent().find('button');
|
|
||||||
|
|
||||||
//If the button is found (=displayed)
|
|
||||||
if($InputButton.length > 0) {
|
|
||||||
this.$InputBox = $InputButton.parent();
|
|
||||||
|
|
||||||
//Add Feedback box
|
|
||||||
this.$Feedback = ($('#ggu-feedback').length > 0)?$('#ggu-feedback'):($('<div>', {
|
|
||||||
'id':'ggu-feedback',
|
|
||||||
'style':'width:calc(100% + 64px); height:400px; margin:1em 0 0 -32px; display:inline-block; text-align:left; overflow:auto;'
|
|
||||||
}).insertAfter($InputButton));
|
|
||||||
|
|
||||||
/*
|
|
||||||
//Add Folder Name Input next to button
|
|
||||||
this.$InputName = ($('#ggu-inputname').length > 0)?$('#ggu-inputname'):($('<input>', {
|
|
||||||
'type':'text',
|
|
||||||
'id': 'ggu-inputname',
|
|
||||||
'name':'ggu-inputname',
|
|
||||||
'placeholder':'Folder Name',
|
|
||||||
'style': 'border-width:2px; border-color:rgba(0, 0, 0, 0.1); border-radius:4px; font-family:Inter,Helvetica Neue,Helvetica,Arial; font-size:15px; padding:8px 12px; margin-bottom:12px;'
|
|
||||||
}).val('PCT').prependTo(this.$InputBox));
|
|
||||||
*/
|
|
||||||
|
|
||||||
//Reset File Input DOM Element
|
|
||||||
let $InputFile = $('input[type=file]');
|
|
||||||
this.$InputFile = $InputFile.clone()
|
|
||||||
.insertAfter($InputFile)
|
|
||||||
.attr('multiple', 'multiple')
|
|
||||||
.attr('name', 'files[]')
|
|
||||||
.change(() => { this.readInputFiles(); });
|
|
||||||
$InputFile.remove();
|
|
||||||
|
|
||||||
//Reset button
|
|
||||||
this.$InputButton = $InputButton.clone()
|
|
||||||
.insertAfter($InputButton)
|
|
||||||
.click(() => {this.$InputFile.click();});
|
|
||||||
$InputButton.remove();
|
|
||||||
|
|
||||||
//Clear all upload notifications
|
|
||||||
this.resetNotif();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
feedback(sType, sMsg) {
|
|
||||||
let sColor = 'black';
|
|
||||||
let sIcon = '';
|
|
||||||
switch(sType) {
|
|
||||||
case 'error': sColor = 'red'; sIcon = '\u274C'; break;
|
|
||||||
case 'warning': sColor = 'orange'; sIcon = '\u26A0'; break;
|
|
||||||
case 'info': sColor = '#2D5E38'; sIcon = '\u2713'; break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sFormattedMsg = sIcon+' '+sMsg+(sMsg.slice(-1)=='.'?'':'.');
|
|
||||||
console.log(sFormattedMsg);
|
|
||||||
|
|
||||||
this.$Feedback.append($('<p>', {'style': 'color: '+sColor+';'}).text(sFormattedMsg));
|
|
||||||
this.$Feedback.scrollTop(this.$Feedback.prop("scrollHeight"));
|
|
||||||
}
|
|
||||||
|
|
||||||
incProgress() {
|
|
||||||
if(!this.progress.current) {
|
|
||||||
this.progress.$Done = $('<div>', {'style':'overflow:hidden; background:#2D5E38; color: white; text-align:right;'});
|
|
||||||
this.progress.$Left = $('<div>', {'style':'overflow:hidden; background:white; color: #2D5E38; text-align:left;'});
|
|
||||||
this.progress.$Box = $('<div>', {'id':'ggu-progress', 'style':'margin-top:1em;'})
|
|
||||||
.append($('<div>', {'style':'display:flex; width:calc(100% + 64px); margin-left:-32px; border-radius:3px; border: 1px solid #2D5E38; box-sizing: border-box;'})
|
|
||||||
.append(this.progress.$Done)
|
|
||||||
.append(this.progress.$Left)
|
|
||||||
);
|
|
||||||
this.$Feedback.before(this.progress.$Box);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.progress.current++;
|
|
||||||
|
|
||||||
if(this.progress.current < this.progress.total) {
|
|
||||||
let iRatio = Math.round(this.progress.current / this.progress.total * 100);
|
|
||||||
let sTextDone = '', sTextLeft = '';
|
|
||||||
if(iRatio < 50) {
|
|
||||||
sTextDone = '';
|
|
||||||
sTextLeft = ' '+iRatio+'% ('+this.progress.current+' / '+this.progress.total+')';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
sTextDone = '('+this.progress.current+' / '+this.progress.total+') '+iRatio+'% ';
|
|
||||||
sTextLeft = '';
|
|
||||||
}
|
|
||||||
this.progress.$Done.css('flex', iRatio+' 1 0%').html(sTextDone);
|
|
||||||
this.progress.$Left.css('flex', (100 - iRatio)+' 1 0%').html(sTextLeft);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.progress.$Box.remove();
|
|
||||||
this.progress = {current:0, total:0};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Marking all upload notifications as read
|
|
||||||
resetNotif() {
|
|
||||||
this.feedback('info', 'Marking all upload notifications as read');
|
|
||||||
$.get(Gaia.URL+'/social/notifications/popup/').done((asNotifs) => {
|
|
||||||
for(var i in asNotifs) {
|
|
||||||
if(!asNotifs[i].isViewed && asNotifs[i].html.indexOf('has completed') != -1) {
|
|
||||||
$.post(Gaia.URL+'/social/notifications/'+asNotifs[i].id+'/markviewed/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//Parse files from input (multiple)
|
|
||||||
readInputFiles() {
|
|
||||||
if (!window.File || !window.FileReader || !window.FileList || !window.Blob) {
|
|
||||||
this.feedback('error', 'File APIs are not fully supported in this browser');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else this.feedback('info', 'Parsing input files');
|
|
||||||
|
|
||||||
//Get Folder Name (text input)
|
|
||||||
let aoReaders = [];
|
|
||||||
let iCount = 0;
|
|
||||||
this.asFiles = this.$InputFile.prop('files');
|
|
||||||
for(var i=0 ; i < this.asFiles.length ; i++) {
|
|
||||||
aoReaders[i] = new FileReader();
|
|
||||||
aoReaders[i].onload = ((oFileReader) => {
|
|
||||||
return (asResult) => {
|
|
||||||
iCount++;
|
|
||||||
this.feedback('info', 'Reading file "'+oFileReader.name+'" ('+iCount+'/'+this.asFiles.length+')');
|
|
||||||
this.parseFile(oFileReader.name, asResult.target.result);
|
|
||||||
this.asFolders[oFileReader.name] = {};
|
|
||||||
|
|
||||||
if(iCount == this.asFiles.length) {
|
|
||||||
this.progress.total = this.aoTracks.length + this.aoWaypoints.length + this.asFiles.length; //extra action per file: Create folder
|
|
||||||
this.createFolders();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})(this.asFiles[i]);
|
|
||||||
aoReaders[i].readAsText(this.asFiles[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Parse GPX files to consolidate tracks & waypoints
|
|
||||||
parseFile(sFileName, oContent) {
|
|
||||||
this.feedback('info', 'Parsing file "'+sFileName+'"');
|
|
||||||
|
|
||||||
var oGPX = new gpxParser();
|
|
||||||
oGPX.parse(oContent);
|
|
||||||
|
|
||||||
//Waypoints
|
|
||||||
for(var w in oGPX.waypoints) {
|
|
||||||
oGPX.waypoints[w].filename = sFileName;
|
|
||||||
var sWaypointName = oGPX.waypoints[w].name;
|
|
||||||
/* if(sWaypointName.indexOf('Milestone - ') != -1 && sWaypointName.substr(-2) != '00') { //1 milestone every 100 miles
|
|
||||||
this.feedback('info', 'Ignoring milestone waypoint "'+sWaypointName+'"');
|
|
||||||
}
|
|
||||||
else */this.aoWaypoints.push(oGPX.waypoints[w]);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Tracks
|
|
||||||
for(var t in oGPX.tracks) {
|
|
||||||
oGPX.tracks[t].filename = sFileName;
|
|
||||||
this.aoTracks.push(oGPX.tracks[t]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Delete existing folder with same name & recreating it
|
|
||||||
createFolders() {
|
|
||||||
let iCount = 0;
|
|
||||||
$.each(this.asFolders, (sFileName, asFolder) => {
|
|
||||||
|
|
||||||
//Folder Name
|
|
||||||
let sFolderName = sFileName.replace(/\.[^\.]+$/, '');
|
|
||||||
|
|
||||||
this.feedback('info', 'Looking for existing folder "'+sFolderName+'"...');
|
|
||||||
$.get(Gaia.API+'/folder/?search='+sFolderName).done((asFolders) => {
|
|
||||||
if(asFolders != '' && !$.isEmptyObject(asFolders)) {
|
|
||||||
for(var f in asFolders) {
|
|
||||||
this.feedback('info', 'Deleting "'+asFolders[f].title+'" folder');
|
|
||||||
$.ajax({
|
|
||||||
url: Gaia.API+'/folder/'+asFolders[f].id+'/',
|
|
||||||
type: 'DELETE'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else this.feedback('info', 'No folder named "'+sFolderName+'" found');
|
|
||||||
|
|
||||||
this.feedback('info', 'Creating folder "'+sFolderName+'"');
|
|
||||||
let sTime = (new Date()).toISOString();
|
|
||||||
$.post({
|
|
||||||
url: Gaia.API+'/folder/',
|
|
||||||
contentType: 'application/json',
|
|
||||||
data: JSON.stringify({
|
|
||||||
title: sFolderName,
|
|
||||||
imported: true
|
|
||||||
})
|
|
||||||
}).done((asFolder) => {
|
|
||||||
this.feedback('info', 'Folder "'+asFolder.properties.name+'" created');
|
|
||||||
this.asFolders[sFileName] = asFolder;
|
|
||||||
this.incProgress();
|
|
||||||
|
|
||||||
iCount++;
|
|
||||||
if(iCount == Object.keys(this.asFolders).length) this.uploadTrack();
|
|
||||||
}).fail(() => {
|
|
||||||
this.feedback('error', 'Folder "'+sFolderName+'" could not be created');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//Build & Upload Track File
|
|
||||||
uploadTrack(iIndex) {
|
|
||||||
iIndex = iIndex || 0;
|
|
||||||
|
|
||||||
if(iIndex == 0) this.feedback('info', 'Uploading tracks...');
|
|
||||||
|
|
||||||
let aoTrack = this.aoTracks[iIndex];
|
|
||||||
this.feedback('info', 'Uploading track "'+aoTrack.name+'"');
|
|
||||||
|
|
||||||
let sColor = '#4ABD32';
|
|
||||||
switch(aoTrack.color) {
|
|
||||||
|
|
||||||
//Personal Colors
|
|
||||||
case 'DarkBlue': sColor = '#2D3FC7'; break;
|
|
||||||
case 'Magenta': sColor = '#B60DC3'; break;
|
|
||||||
|
|
||||||
//Garmin Colors
|
|
||||||
case 'Black': sColor = '#000000'; break;
|
|
||||||
case 'DarkRed': sColor = '#F90553'; break;
|
|
||||||
case 'DarkGreen': sColor = '#009B89'; break;
|
|
||||||
case 'DarkYellow': sColor = '#DCEE0E'; break;
|
|
||||||
//case 'DarkBlue': sColor = '#5E23CA'; break;
|
|
||||||
case 'DarkMagenta': sColor = '#B60DC3'; break;
|
|
||||||
case 'DarkCyan': sColor = '#00ACF8'; break;
|
|
||||||
case 'LightGray': sColor = '#A4A4A4'; break;
|
|
||||||
case 'DarkGray': sColor = '#577B8E'; break;
|
|
||||||
case 'Red': sColor = '#F90553'; break;
|
|
||||||
case 'Green': sColor = '#36C03B'; break;
|
|
||||||
case 'Yellow': sColor = '#FFF011'; break;
|
|
||||||
case 'Blue': sColor = '#2D3FC7'; break;
|
|
||||||
//case 'Magenta': sColor = '#B60DC3'; break;
|
|
||||||
case 'Cyan': sColor = '#00C3DD'; break;
|
|
||||||
case 'White': sColor = '#FFFFFF'; break;
|
|
||||||
case 'Transparent': sColor = '#784D3E'; break;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Add track points
|
|
||||||
let aoCoords = [];
|
|
||||||
for(var p in aoTrack.points) {
|
|
||||||
let pt = aoTrack.points[p];
|
|
||||||
aoCoords.push([pt.lon, pt.lat, pt.ele, 0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Convert to geojson
|
|
||||||
let sPostedData = JSON.stringify({
|
|
||||||
geometry: {
|
|
||||||
coordinates: [aoCoords],
|
|
||||||
type: 'MultiLineString'
|
|
||||||
},
|
|
||||||
properties: {
|
|
||||||
archived: false,
|
|
||||||
color: sColor,
|
|
||||||
//distance: 168405.62350073704,
|
|
||||||
filename: aoTrack.filename,
|
|
||||||
hexcolor: sColor,
|
|
||||||
isLatestImport: true,
|
|
||||||
isLocallyCreated: true,
|
|
||||||
isPublicTrack: false,
|
|
||||||
isValid: true,
|
|
||||||
localId: 'track'+(iIndex + 1),
|
|
||||||
notes: aoTrack.desc,
|
|
||||||
parent_folder_id: this.asFolders[aoTrack.filename].id,
|
|
||||||
routing_mode: null,
|
|
||||||
time_created: aoTrack.time || (new Date()).toISOString(),
|
|
||||||
title: aoTrack.name,
|
|
||||||
type: 'track',
|
|
||||||
writable: true
|
|
||||||
},
|
|
||||||
type: 'Feature'
|
|
||||||
});
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
$.post({
|
|
||||||
url: Gaia.API+'/track/',
|
|
||||||
contentType: 'application/json',
|
|
||||||
data: sPostedData,
|
|
||||||
trackName: aoTrack.name
|
|
||||||
}).done(function(asTrack) {
|
|
||||||
self.aoTracks[iIndex] = asTrack;
|
|
||||||
self.confirmTrack(iIndex, asTrack, this.data);
|
|
||||||
}).fail(function() {
|
|
||||||
self.feedback('error', 'Track "'+this.trackName+'" upload failed (stage 1). Retrying...');
|
|
||||||
self.uploadTrack(iIndex);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
confirmTrack(iIndex, asTrack, sPostedData) {
|
|
||||||
iIndex = iIndex || 0;
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
$.ajax({
|
|
||||||
url: Gaia.API+'/track/'+asTrack.features[0].id+'/',
|
|
||||||
type: 'PUT',
|
|
||||||
contentType: 'application/json',
|
|
||||||
data: sPostedData,
|
|
||||||
trackName: asTrack.features[0].properties.title
|
|
||||||
}).done(function() {
|
|
||||||
self.feedback('info', 'Track "'+this.trackName+'" uploaded');
|
|
||||||
self.incProgress();
|
|
||||||
iIndex++;
|
|
||||||
if(iIndex < self.aoTracks.length) self.uploadTrack(iIndex);
|
|
||||||
else {
|
|
||||||
self.feedback('info', 'All tracks uploaded');
|
|
||||||
self.uploadWayPoints();
|
|
||||||
}
|
|
||||||
}).fail(function() {
|
|
||||||
self.feedback('error', 'Track "'+this.trackName+'" upload failed (stage 2). Retrying...');
|
|
||||||
self.confirmTrack(iIndex, asTrack, sPostedData);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
//Wait for file to be processed by Gaia
|
|
||||||
checkNotif() {
|
|
||||||
this.feedback('info', 'Waiting for Gaia to process consolidated track');
|
|
||||||
$.get(Gaia.URL+'/social/notifications/popup/').done((asNotifs) => {
|
|
||||||
for(var i in asNotifs) {
|
|
||||||
if(!asNotifs[i].isViewed && asNotifs[i].html.indexOf('has completed') != -1) {
|
|
||||||
this.feedback('info', 'Notification '+asNotifs[i].id+' found. Marking as read');
|
|
||||||
var $Notif = $('<span>').html(asNotifs[i].html);
|
|
||||||
this.sFolderId = $Notif.find('a').attr('href').split('/')[3];
|
|
||||||
$.post(Gaia.URL+'/social/notifications/'+asNotifs[i].id+'/markviewed/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.sFolderId != '') {
|
|
||||||
this.setTracksColor();
|
|
||||||
this.uploadWayPoints();
|
|
||||||
}
|
|
||||||
else setTimeout((() => {this.checkNotif();}), 1000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
uploadWayPoints(iIndex) {
|
|
||||||
iIndex = iIndex || 0;
|
|
||||||
|
|
||||||
//Upload waypoints
|
|
||||||
var sWaypointName = this.aoWaypoints[iIndex].name;
|
|
||||||
var aoWaypoint = this.aoWaypoints[iIndex];
|
|
||||||
|
|
||||||
this.feedback('info', 'Uploading waypoint '+(iIndex + 1)+'/'+this.aoWaypoints.length+' ('+aoWaypoint.name+')');
|
|
||||||
var asPost = {
|
|
||||||
geometry: {
|
|
||||||
coordinates: [
|
|
||||||
aoWaypoint.lon,
|
|
||||||
aoWaypoint.lat,
|
|
||||||
aoWaypoint.ele
|
|
||||||
],
|
|
||||||
type: 'Point'
|
|
||||||
},
|
|
||||||
properties: {
|
|
||||||
archived: false,
|
|
||||||
filename: aoWaypoint.filename,
|
|
||||||
icon: Gaia.getIconName(aoWaypoint.sym),
|
|
||||||
isLatestImport: true,
|
|
||||||
isLocallyCreated: true,
|
|
||||||
isValid: true,
|
|
||||||
localId: iIndex+'',
|
|
||||||
notes: aoWaypoint.desc,
|
|
||||||
parent_folder_id: this.asFolders[aoWaypoint.filename].id,
|
|
||||||
time_created: aoWaypoint.time || (new Date()).toISOString(),
|
|
||||||
title: aoWaypoint.name,
|
|
||||||
type: 'waypoint',
|
|
||||||
writable: true
|
|
||||||
},
|
|
||||||
type: 'Feature'
|
|
||||||
};
|
|
||||||
|
|
||||||
let sData = JSON.stringify(asPost);
|
|
||||||
var self = this;
|
|
||||||
$.post({
|
|
||||||
url: Gaia.API+'/waypoint/',
|
|
||||||
contentType: 'application/json',
|
|
||||||
data: sData
|
|
||||||
}).done(function(asWaypoint) {
|
|
||||||
self.aoWaypoints[iIndex] = asWaypoint;
|
|
||||||
self.confirmWayPoint(iIndex, asWaypoint, this.data);
|
|
||||||
}).fail(function(){
|
|
||||||
self.feedback('error', 'Failed to upload waypoint #'+(iIndex + 1)+' "'+sWaypointName+'" (Stage 1). Trying again...');
|
|
||||||
self.uploadWayPoints(iIndex);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
confirmWayPoint(iIndex, asWaypoint, sPostedData) {
|
|
||||||
$.ajax({
|
|
||||||
url: Gaia.API+'/waypoint/'+asWaypoint.properties.id+'/',
|
|
||||||
type: 'PUT',
|
|
||||||
contentType: 'application/json',
|
|
||||||
data: sPostedData
|
|
||||||
}).done(() => {
|
|
||||||
iIndex++;
|
|
||||||
this.incProgress();
|
|
||||||
if(iIndex < this.aoWaypoints.length) this.uploadWayPoints(iIndex);
|
|
||||||
//else this.assignElementsToFolders();
|
|
||||||
else this.feedback('info', 'Done');
|
|
||||||
}).fail(() => {
|
|
||||||
this.feedback('error', 'Failed to upload waypoint #'+(iIndex + 1)+' "'+asWaypoint.properties.title+'" (Stage 2). Trying again...');
|
|
||||||
this.confirmWayPoint(iIndex, asWaypoint, sPostedData);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
assignElementsToFolders(iIndex) {
|
|
||||||
iIndex = iIndex || 0;
|
|
||||||
let asFolders = Object.keys(this.asFolders).map(key => this.asFolders[key]);
|
|
||||||
let asFolder = asFolders[iIndex];
|
|
||||||
|
|
||||||
this.feedback('info', 'Assigning elements of folder "'+asFolder.properties.name+'"');
|
|
||||||
|
|
||||||
//Folder metadata
|
|
||||||
let asData = {
|
|
||||||
cover_photo_id: asFolder.properties.cover_photo_id,
|
|
||||||
id: asFolder.id,
|
|
||||||
name: asFolder.properties.name,
|
|
||||||
notes: asFolder.properties.notes,
|
|
||||||
time_created: asFolder.properties.time_created,
|
|
||||||
updated_date: asFolder.properties.updated_date
|
|
||||||
}
|
|
||||||
|
|
||||||
//Assign waypoints to folder
|
|
||||||
asData.waypoints = [];
|
|
||||||
for(var w in this.aoWaypoints) {
|
|
||||||
if(this.aoWaypoints[w].parent_folder_id = asFolder.id) asData.waypoints.push(this.aoWaypoints[w].properties.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Assign tracks to folder
|
|
||||||
asData.tracks = [];
|
|
||||||
for(var t in this.aoTracks) {
|
|
||||||
if(this.aoTracks[t].parent_folder_id = asFolder.id) asData.tracks.push(this.aoTracks[t].features[0].id);
|
|
||||||
}
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
url: Gaia.API+'/folder/'+asFolder.id+'/',
|
|
||||||
type: 'PUT',
|
|
||||||
contentType: 'application/json',
|
|
||||||
data: JSON.stringify(asData)
|
|
||||||
}).done(() => {
|
|
||||||
iIndex++;
|
|
||||||
this.incProgress();
|
|
||||||
this.feedback('info', 'Tracks & waypoints assigned to folder "'+asFolder.properties.name+'"');
|
|
||||||
if(iIndex < asFolders.length) this.assignElementsToFolders(iIndex);
|
|
||||||
else this.feedback('info', 'Done');
|
|
||||||
}).fail(() => {
|
|
||||||
this.feedback('warning', 'Failed to assign waypoints & tracks to folder "'+asFolder.properties.name+'". Trying again...');
|
|
||||||
this.assignElementsToFolders(iIndex);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
static getIconName(sGarminName) {
|
|
||||||
var asMapping = {
|
|
||||||
'Bridge': 'bridge',
|
|
||||||
'Campground': 'campsite-24',
|
|
||||||
'Car': 'car-24',
|
|
||||||
'Cemetery': 'cemetery-24',
|
|
||||||
'Church': 'ghost-town',
|
|
||||||
'City (Capitol)': 'city-24',
|
|
||||||
'Convenience Store': 'market',
|
|
||||||
'Drinking Water': 'potable-water',
|
|
||||||
'Flag, Blue': 'blue-pin-down',
|
|
||||||
'Flag, Green': 'green-pin',
|
|
||||||
'Flag, Red': 'red-pin-down',
|
|
||||||
'Forest': 'forest',
|
|
||||||
'Ground Transportation': 'car-24',
|
|
||||||
'Lodging': 'lodging-24',
|
|
||||||
'Park': 'park-24',
|
|
||||||
'Pharmacy': 'hospital-24',
|
|
||||||
'Picnic Area': 'picnic',
|
|
||||||
'Post Office': 'resupply',
|
|
||||||
'Powerline': 'petroglyph',
|
|
||||||
'Residence': 'building-24',
|
|
||||||
'Restaurant': 'restaurant-24',
|
|
||||||
'Restroom': 'toilets-24',
|
|
||||||
'Shopping Center': 'market',
|
|
||||||
'Ski Resort': 'skiing-24',
|
|
||||||
'Summit': 'peak',
|
|
||||||
'Toll Booth': 'ranger-station',
|
|
||||||
'Trail Head': 'known-route',
|
|
||||||
'Truck': 'car-24',
|
|
||||||
'Water Source': 'water-24'
|
|
||||||
};
|
|
||||||
return (sGarminName in asMapping)?asMapping[sGarminName]:'red-pin-down';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Loading GaiaGps Uploader '+GM_info.script.version);
|
|
||||||
|
|
||||||
let oGaia = new Gaia();
|
|
||||||
|
|
||||||
MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
|
|
||||||
|
|
||||||
let observer = new MutationObserver((mutations, observer) => {
|
|
||||||
|
|
||||||
/* FIXME: adapts on GaiaGPS upgrade */
|
|
||||||
let $Import = $('div[aria-label="Import Data"]');
|
|
||||||
if($Import.length > 0) {
|
|
||||||
observer.disconnect();
|
|
||||||
$Import.parent('li').on('click', () => { setTimeout(() => { oGaia.setLayout(); }, 500)});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(document, { subtree: true, attributes: true});
|
|
||||||
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<browserconfig>
|
|
||||||
<msapplication>
|
|
||||||
<tile>
|
|
||||||
<square150x150logo src="/images/icons/mstile-150x150.png?v=GvmqYyKwbb"/>
|
|
||||||
<TileColor>#00a300</TileColor>
|
|
||||||
</tile>
|
|
||||||
</msapplication>
|
|
||||||
</browserconfig>
|
|
||||||
|
Before Width: | Height: | Size: 821 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
@@ -1,32 +0,0 @@
|
|||||||
<?xml version="1.0" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
|
||||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
|
||||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
|
|
||||||
preserveAspectRatio="xMidYMid meet">
|
|
||||||
<metadata>
|
|
||||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
|
||||||
</metadata>
|
|
||||||
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
|
|
||||||
fill="#000000" stroke="none">
|
|
||||||
<path d="M5041 5784 c366 -384 572 -817 630 -1323 17 -156 7 -495 -20 -641
|
|
||||||
-78 -415 -250 -717 -1091 -1915 -108 -154 -338 -482 -510 -730 -172 -247 -328
|
|
||||||
-469 -346 -492 -21 -25 -56 -52 -91 -68 -48 -23 -66 -27 -127 -23 -46 2 -86
|
|
||||||
11 -113 24 l-42 21 36 -44 c47 -58 122 -116 191 -150 50 -25 68 -28 152 -28
|
|
||||||
88 0 100 2 163 34 80 40 167 121 254 236 102 137 967 1375 1158 1660 429 639
|
|
||||||
578 959 636 1365 20 140 17 485 -5 630 -37 237 -101 440 -205 655 -162 331
|
|
||||||
-404 620 -696 830 -33 24 -28 16 26 -41z"/>
|
|
||||||
<path d="M2750 5673 c-63 -22 -143 -75 -197 -131 -192 -196 -318 -616 -298
|
|
||||||
-990 9 -174 36 -314 118 -607 l34 -120 316 -3 317 -2 0 62 c1 81 22 199 49
|
|
||||||
265 11 29 42 87 69 130 143 230 186 366 185 588 -1 420 -140 736 -356 809 -60
|
|
||||||
20 -177 20 -237 -1z"/>
|
|
||||||
<path d="M4017 5056 c-174 -62 -301 -280 -348 -591 -19 -129 -17 -337 4 -425
|
|
||||||
32 -136 85 -257 159 -368 81 -119 127 -268 128 -410 l0 -62 317 2 316 3 34
|
|
||||||
120 c106 374 136 590 115 816 -22 225 -66 391 -153 566 -48 97 -74 138 -131
|
|
||||||
198 -39 42 -90 87 -115 102 -99 58 -241 79 -326 49z"/>
|
|
||||||
<path d="M2412 3548 c3 -109 7 -129 30 -173 120 -232 445 -231 559 0 28 58 32
|
|
||||||
76 37 180 l5 115 -318 0 -317 0 4 -122z"/>
|
|
||||||
<path d="M3960 2962 c0 -234 106 -368 300 -380 98 -5 168 20 233 83 75 75 91
|
|
||||||
119 95 263 l4 122 -316 0 -316 0 0 -88z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Spotty",
|
|
||||||
"short_name": "Spotty",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "/images/icons/android-chrome-192x192.png?v=GvmqYyKwbb",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "any maskable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/images/icons/android-chrome-512x512.png?v=GvmqYyKwbb",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "any maskable"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"theme_color": "#ffffff",
|
|
||||||
"background_color": "#ffffff",
|
|
||||||
"display": "browser",
|
|
||||||
"orientation": "portrait"
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 8.5 KiB |
BIN
images/ogp.png
|
Before Width: | Height: | Size: 261 KiB |
@@ -1,142 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
version="1.1"
|
|
||||||
id="svg4"
|
|
||||||
sodipodi:docname="spot.svg"
|
|
||||||
width="512"
|
|
||||||
height="512"
|
|
||||||
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
|
|
||||||
inkscape:export-filename="C:\Users\francois\Downloads\footprint_mapbox.png"
|
|
||||||
inkscape:export-xdpi="96"
|
|
||||||
inkscape:export-ydpi="96">
|
|
||||||
<metadata
|
|
||||||
id="metadata10">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs8">
|
|
||||||
<linearGradient
|
|
||||||
id="linearGradient4520"
|
|
||||||
osb:paint="solid">
|
|
||||||
<stop
|
|
||||||
style="stop-color:#6dff58;stop-opacity:1;"
|
|
||||||
offset="0"
|
|
||||||
id="stop4518" />
|
|
||||||
</linearGradient>
|
|
||||||
<filter
|
|
||||||
style="color-interpolation-filters:sRGB"
|
|
||||||
inkscape:label="Drop Shadow"
|
|
||||||
id="filter4696">
|
|
||||||
<feFlood
|
|
||||||
flood-opacity="0.498039"
|
|
||||||
flood-color="rgb(0,0,0)"
|
|
||||||
result="flood"
|
|
||||||
id="feFlood4686" />
|
|
||||||
<feComposite
|
|
||||||
in="flood"
|
|
||||||
in2="SourceGraphic"
|
|
||||||
operator="in"
|
|
||||||
result="composite1"
|
|
||||||
id="feComposite4688" />
|
|
||||||
<feGaussianBlur
|
|
||||||
in="composite1"
|
|
||||||
stdDeviation="3"
|
|
||||||
result="blur"
|
|
||||||
id="feGaussianBlur4690" />
|
|
||||||
<feOffset
|
|
||||||
dx="3"
|
|
||||||
dy="3"
|
|
||||||
result="offset"
|
|
||||||
id="feOffset4692" />
|
|
||||||
<feComposite
|
|
||||||
in="SourceGraphic"
|
|
||||||
in2="offset"
|
|
||||||
operator="over"
|
|
||||||
result="composite2"
|
|
||||||
id="feComposite4694" />
|
|
||||||
</filter>
|
|
||||||
<filter
|
|
||||||
style="color-interpolation-filters:sRGB"
|
|
||||||
inkscape:label="Drop Shadow"
|
|
||||||
id="filter5223">
|
|
||||||
<feFlood
|
|
||||||
flood-opacity="0.498039"
|
|
||||||
flood-color="rgb(0,0,0)"
|
|
||||||
result="flood"
|
|
||||||
id="feFlood5213" />
|
|
||||||
<feComposite
|
|
||||||
in="flood"
|
|
||||||
in2="SourceGraphic"
|
|
||||||
operator="in"
|
|
||||||
result="composite1"
|
|
||||||
id="feComposite5215" />
|
|
||||||
<feGaussianBlur
|
|
||||||
in="composite1"
|
|
||||||
stdDeviation="1.2"
|
|
||||||
result="blur"
|
|
||||||
id="feGaussianBlur5217" />
|
|
||||||
<feOffset
|
|
||||||
dx="1.2"
|
|
||||||
dy="1.2"
|
|
||||||
result="offset"
|
|
||||||
id="feOffset5219" />
|
|
||||||
<feComposite
|
|
||||||
in="SourceGraphic"
|
|
||||||
in2="offset"
|
|
||||||
operator="over"
|
|
||||||
result="composite2"
|
|
||||||
id="feComposite5221" />
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
<sodipodi:namedview
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1"
|
|
||||||
objecttolerance="10"
|
|
||||||
gridtolerance="10"
|
|
||||||
guidetolerance="10"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:window-width="1920"
|
|
||||||
inkscape:window-height="1017"
|
|
||||||
id="namedview6"
|
|
||||||
showgrid="false"
|
|
||||||
inkscape:snap-grids="true"
|
|
||||||
inkscape:zoom="1.3037281"
|
|
||||||
inkscape:cx="248.69883"
|
|
||||||
inkscape:cy="256.06919"
|
|
||||||
inkscape:window-x="-8"
|
|
||||||
inkscape:window-y="-8"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="svg4"
|
|
||||||
units="px" />
|
|
||||||
<path
|
|
||||||
id="path4698"
|
|
||||||
d="M 14.766749,479.35437 C 5.685625,466.18943 4,464.83831 4,460 c 0,-6.62744 5.372562,-12 11.999999,-12 6.627437,0 11.999998,5.37256 11.999998,12 0,4.83831 -1.685625,6.18943 -10.766748,19.35437 -0.595937,0.86087 -1.870625,0.86081 -2.4665,0 z"
|
|
||||||
inkscape:connector-curvature="0"
|
|
||||||
style="fill:#6dff58;fill-opacity:1;stroke:none;stroke-width:0.06249999;stroke-opacity:1;filter:url(#filter5223)"
|
|
||||||
inkscape:label="marker"
|
|
||||||
transform="matrix(13.333333,0,0,13.333333,42.666662,-5930.6664)" />
|
|
||||||
<path
|
|
||||||
id="shoes"
|
|
||||||
d="m 221.87402,255.01429 v -11.375 h -45.49995 v 11.375 c 0,12.56578 10.18419,22.74983 22.74997,22.74983 12.56579,0 22.74998,-10.18405 22.74998,-22.74983 z m 90.99975,68.24979 c 12.56579,0 22.74982,-10.18419 22.74982,-22.74998 v -11.37499 h -45.4998 v 11.37499 c 0,12.56579 10.18419,22.74998 22.74998,22.74998 z M 267.37383,203.30789 c 0,12.40941 4.66373,27.07248 11.37498,37.22819 5.82257,8.81203 11.37498,15.8254 11.37498,37.22804 h 45.4998 l 5.67685,-20.45001 c 2.58791,-9.31681 4.66375,-18.84333 5.34988,-28.54756 0.8216,-11.6203 0.23791,-23.31873 -2.20387,-34.68656 -8.36069,-38.91663 -26.87344,-52.81544 -42.94766,-52.81544 -22.74998,0 -34.12496,29.92325 -34.12496,62.04334 z m -99.81881,-54.72772 c -2.44205,11.36784 -3.02861,23.06626 -2.20402,34.68644 0.68639,9.70437 2.7621,19.23086 5.34624,28.54769 l 5.67683,20.45002 h 45.49995 c 0,-21.39914 5.55243,-28.41615 11.37498,-37.22818 6.71124,-10.15572 11.37499,-24.81865 11.37499,-37.22807 0,-32.12008 -11.37499,-62.043305 -34.12495,-62.043305 -16.07422,0 -34.58699,13.898815 -42.94402,52.815405 z"
|
|
||||||
inkscape:connector-curvature="0"
|
|
||||||
style="fill:#326526;fill-opacity:1;stroke-width:0.35546762" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 5.2 KiB |
@@ -1,17 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 23.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<svg width="406" height="469" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 406 469" style="enable-background:new 0 0 406 469;">
|
|
||||||
<style type="text/css">.st0{fill:#F18A00;}</style>
|
|
||||||
<g id="Layer_3"/>
|
|
||||||
<g id="Layer_8"/>
|
|
||||||
<g id="Layer_9"/>
|
|
||||||
<g id="Layer_7"/>
|
|
||||||
<g id="Layer_6"/>
|
|
||||||
<g id="Layer_4"/>
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<path class="st0" d="m85.80618,195.80013c-1,-0.8 -1.3,-2.3 -0.6,-3.4c11.1,-18.2 56.5,-85.8 117.3,-85.8c49.6,0 90.4,33.4 110.3,53.3c1.2,1.2 2.9,1.9 4.6,1.9c1.7,0 3.4,-0.7 4.6,-1.9l16.4,-16.3c1,-1 1.1,-2.5 0.2,-3.5c-5.1,-6.1 -15.3,-17.2 -29.8,-28.2c-31.7,-24.1 -67.5,-36.3 -106.3,-36.3c-79.4,0 -129.8,75.4 -142.3,96.5c-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.3c5.9,-10.8 23,-39.4 48.5,-63.7c42.8,-40.7 95.1,-62.2 151.4,-62.2c56,0 109.2,18.9 153.9,54.8c27.3,21.9 45.5,43.2 51.3,50.4c0.8,1 0.7,2.5 -0.2,3.5l-12.3,12.2c-1.1,1.1 -2.9,1 -3.8,-0.2c-7.3,-8.9 -26.2,-30.5 -49.7,-49.2c-41.1,-32.7 -88,-49.2 -139.2,-49.2c-50.9,0 -96.5,18.6 -135.4,55.4c-20.9,19.7 -30.4,34.1 -35.6,42.3c-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.4c2.2,0 4.2,-1.1 5.4,-2.9c7.4,-10.7 15.9,-20.6 26.8,-31.1c34.3,-33.1 75.7,-50.6 119.9,-50.6c92,0 151.2,70.7 165.3,89.4c0.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.1c-15.9,-18.1 -63,-66.5 -111.4,-66.5c-23.6,0 -46.6,11.3 -68.4,33.5c-7.2,7.3 -13.9,15.6 -19.9,24.4c-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" />
|
|
||||||
<path class="st0" d="m205.90618,468.90013c-56,0 -109.2,-18.9 -153.9,-54.8c-27.3,-21.9 -45.5,-43.2 -51.3,-50.4c-0.8,-1 -0.7,-2.5 0.2,-3.5l12.3,-12.2c1.1,-1.1 2.9,-1 3.8,0.2c7.3,8.9 26.2,30.5 49.7,49.2c41.1,32.7 88,49.2 139.2,49.2c50.9,0 96.5,-18.6 135.4,-55.4c20.9,-19.7 30.4,-34.1 35.6,-42.3c0.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.4c-2.2,0 -4.2,1.1 -5.4,2.9c-7.4,10.7 -15.9,20.6 -26.8,31.1c-34.3,33.1 -75.7,50.6 -119.8,50.6c-92,0 -151.2,-70.7 -165.3,-89.4c-0.8,-1.1 -0.7,-2.5 0.3,-3.4l49.8,-49.3c1.1,-1.1 2.8,-1 3.8,0.1c15.9,18.1 63,66.5 111.4,66.5c23.6,0 46.6,-11.3 68.4,-33.5c7.2,-7.3 13.9,-15.6 19.9,-24.4c0.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.4c-11.1,18.2 -56.5,85.8 -117.3,85.8c-49.6,0 -90.4,-33.4 -110.3,-53.3c-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.5c5.1,6.1 15.3,17.2 29.8,28.2c31.7,24.1 67.5,36.3 106.3,36.3c79.4,0 129.8,-75.4 142.3,-96.5c0.8,-1.4 2.6,-1.7 3.8,-0.7l55.4,43.6c1,0.8 1.3,2.2 0.7,3.3c-5.9,10.8 -23,39.4 -48.5,63.7c-42.6,40.8 -95,62.3 -151.3,62.3z" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.7 KiB |
45
inc/Map.php
@@ -1,45 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Franzz\Spot;
|
|
||||||
use Franzz\Objects\PhpObject;
|
|
||||||
use Franzz\Objects\Db;
|
|
||||||
use \Settings;
|
|
||||||
|
|
||||||
class Map extends PhpObject {
|
|
||||||
|
|
||||||
const MAP_TABLE = 'maps';
|
|
||||||
const MAPPING_TABLE = 'mappings';
|
|
||||||
|
|
||||||
private Db $oDb;
|
|
||||||
|
|
||||||
private $asMaps;
|
|
||||||
|
|
||||||
public function __construct(Db &$oDb) {
|
|
||||||
parent::__construct(__CLASS__);
|
|
||||||
$this->oDb = &$oDb;
|
|
||||||
$this->setMaps();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function setMaps() {
|
|
||||||
$asMaps = $this->oDb->selectRows(array('from'=>self::MAP_TABLE));
|
|
||||||
foreach($asMaps as $asMap) $this->asMaps[$asMap['codename']] = $asMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getProjectMaps($iProjectId) {
|
|
||||||
$asMappings = $this->oDb->getArrayQuery("SELECT id_map FROM mappings WHERE id_project = ".$iProjectId." OR id_project IS NULL", true);
|
|
||||||
return array_filter($this->asMaps, function($asMap) use($asMappings) {return in_array($asMap['id_map'], $asMappings);});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getMapUrl($sCodeName, $asParams) {
|
|
||||||
$asParams['token'] = $this->asMaps[$sCodeName]['token'];
|
|
||||||
return self::populateParams($this->asMaps[$sCodeName]['pattern'], $asParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function populateParams($sUrl, $asParams) {
|
|
||||||
foreach($asParams as $sParam=>$sValue) {
|
|
||||||
$sUrl = str_replace('{'.$sParam.'}', $sValue, $sUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $sUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
112
index.php
@@ -1,112 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/* Requests Handler */
|
|
||||||
|
|
||||||
//Start buffering
|
|
||||||
ob_start();
|
|
||||||
|
|
||||||
$oLoader = require __DIR__.'/vendor/autoload.php';
|
|
||||||
|
|
||||||
use Franzz\Objects\ToolBox;
|
|
||||||
use Franzz\Objects\Main;
|
|
||||||
use Franzz\Spot\Spot;
|
|
||||||
use Franzz\Spot\User;
|
|
||||||
|
|
||||||
ToolBox::fixGlobalVars($argv ?? array());
|
|
||||||
|
|
||||||
//Available variables
|
|
||||||
$sAction = $_REQUEST['a'] ?? '';
|
|
||||||
$sTimezone = $_REQUEST['t'] ?? '';
|
|
||||||
$sName = $_GET['name'] ?? '';
|
|
||||||
$sContent = $_GET['content'] ?? '';
|
|
||||||
$iProjectId = $_REQUEST['id_project'] ?? 0 ;
|
|
||||||
$sField = $_REQUEST['field'] ?? '';
|
|
||||||
$oValue = $_REQUEST['value'] ?? '';
|
|
||||||
$iId = $_REQUEST['id'] ?? 0 ;
|
|
||||||
$sType = $_REQUEST['type'] ?? '';
|
|
||||||
$sEmail = $_REQUEST['email'] ?? '';
|
|
||||||
$sLat = $_REQUEST['latitude'] ?? '';
|
|
||||||
$sLng = $_REQUEST['longitude'] ?? '';
|
|
||||||
$iTimestamp = $_REQUEST['timestamp'] ?? 0;
|
|
||||||
|
|
||||||
//Initiate class
|
|
||||||
$oSpot = new Spot(__FILE__, $sTimezone);
|
|
||||||
$oSpot->setProjectId($iProjectId);
|
|
||||||
|
|
||||||
$sResult = '';
|
|
||||||
if($sAction!='')
|
|
||||||
{
|
|
||||||
switch($sAction)
|
|
||||||
{
|
|
||||||
case 'markers':
|
|
||||||
$sResult = $oSpot->getMarkers();
|
|
||||||
break;
|
|
||||||
case 'next_feed':
|
|
||||||
$sResult = $oSpot->getNextFeed($iId);
|
|
||||||
break;
|
|
||||||
case 'new_feed':
|
|
||||||
$sResult = $oSpot->getNewFeed($iId);
|
|
||||||
break;
|
|
||||||
case 'add_post':
|
|
||||||
$sResult = $oSpot->addPost($sName, $sContent);
|
|
||||||
break;
|
|
||||||
case 'subscribe':
|
|
||||||
$sResult = $oSpot->subscribe($sEmail, $sName);
|
|
||||||
break;
|
|
||||||
case 'unsubscribe':
|
|
||||||
$sResult = $oSpot->unsubscribe();
|
|
||||||
break;
|
|
||||||
case 'unsubscribe_email':
|
|
||||||
$sResult = $oSpot->unsubscribeFromEmail($iId);
|
|
||||||
break;
|
|
||||||
case 'update_project':
|
|
||||||
$sResult = $oSpot->updateProject();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if($oSpot->checkUserClearance(User::CLEARANCE_ADMIN))
|
|
||||||
{
|
|
||||||
switch($sAction)
|
|
||||||
{
|
|
||||||
case 'upload':
|
|
||||||
$sResult = $oSpot->upload();
|
|
||||||
break;
|
|
||||||
case 'add_comment':
|
|
||||||
$sResult = $oSpot->addComment($iId, $sContent);
|
|
||||||
break;
|
|
||||||
case 'add_position':
|
|
||||||
$sResult = $oSpot->addPosition($sLat, $sLng, $iTimestamp);
|
|
||||||
break;
|
|
||||||
case 'admin_new':
|
|
||||||
$sResult = $oSpot->createProject();
|
|
||||||
break;
|
|
||||||
case 'admin_get':
|
|
||||||
$sResult = $oSpot->getAdminSettings();
|
|
||||||
break;
|
|
||||||
case 'admin_set':
|
|
||||||
$sResult = $oSpot->setAdminSettings($sType, $iId, $sField, $oValue);
|
|
||||||
break;
|
|
||||||
case 'admin_del':
|
|
||||||
$sResult = $oSpot->delAdminSettings($sType, $iId);
|
|
||||||
break;
|
|
||||||
case 'generate_cron':
|
|
||||||
$sResult = $oSpot->genCronFile();
|
|
||||||
break;
|
|
||||||
case 'sql':
|
|
||||||
$sResult = $oSpot->getDbBuildScript();
|
|
||||||
break;
|
|
||||||
case 'build_geojson':
|
|
||||||
$sResult = $oSpot->buildGeoJSON($sName);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$sResult = Main::getJsonResult(false, Main::NOT_FOUND);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else $sResult = Main::getJsonResult(false, Main::NOT_FOUND);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else $sResult = $oSpot->getAppMainPage();
|
|
||||||
|
|
||||||
$sDebug = ob_get_clean();
|
|
||||||
if(Settings::DEBUG && $sDebug!='') $oSpot->addUncaughtError($sDebug);
|
|
||||||
|
|
||||||
echo $sResult;
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
locale = en_NZ
|
|
||||||
page_og_desc = Keep contact with François when he is off hiking
|
|
||||||
error_commit_db = Issue committing to DB
|
|
||||||
unknown_field = Field "$0" is unknown
|
|
||||||
|
|
||||||
nav_back = Back
|
|
||||||
|
|
||||||
admin = Admin Panel
|
|
||||||
admin_config = Config
|
|
||||||
admin_upload = Upload
|
|
||||||
save = Save
|
|
||||||
admin_save_success = Saved
|
|
||||||
|
|
||||||
track_main = Main track
|
|
||||||
track_off-track = Off-track
|
|
||||||
track_hitchhiking = Hitchhiking
|
|
||||||
track_download = Download GPX Track
|
|
||||||
|
|
||||||
upload_title = Picture & Video Uploads
|
|
||||||
upload_mode_archived= Project "$0" is archived. No upload allowed
|
|
||||||
upload_success = $0 uploaded successfully
|
|
||||||
upload_media_exist = Picture $0 already exists
|
|
||||||
|
|
||||||
post_message = Message
|
|
||||||
post_name = Name
|
|
||||||
post_new_message = New message
|
|
||||||
and = and
|
|
||||||
counter = #$0
|
|
||||||
send = Send
|
|
||||||
|
|
||||||
maps = Base Maps
|
|
||||||
map_satellite = Satellite
|
|
||||||
map_otm = Open Topo Map
|
|
||||||
map_ign_france = IGN (France)
|
|
||||||
map_ign_spain = IGN (Spain)
|
|
||||||
map_linz = LINZ
|
|
||||||
map_usgs = USGS
|
|
||||||
map_natgeo = National Geographic
|
|
||||||
map_outdoors = Mapbox Outdoors
|
|
||||||
|
|
||||||
image = Picture
|
|
||||||
images = Pictures
|
|
||||||
image_taken = taken on $0
|
|
||||||
video = Video
|
|
||||||
video_taken = shot on $0
|
|
||||||
add_on = added on $0
|
|
||||||
click_watch = Click to watch video
|
|
||||||
click_zoom = Click to zoom
|
|
||||||
media_count = Media $0 / $1
|
|
||||||
media_no_id = Missing Media ID in request
|
|
||||||
media_comment_update= Comment of media "$0" updated
|
|
||||||
see_on_google = See on Google Maps
|
|
||||||
copy_to_clipboard = Copy direct link to clipboard
|
|
||||||
link_copied = Link copied!
|
|
||||||
|
|
||||||
city_time = $0 Time
|
|
||||||
local_time = $0 Local Time
|
|
||||||
your_time = $0 Your Time
|
|
||||||
date_time = $0 at $1
|
|
||||||
time_zone = Time Zone
|
|
||||||
|
|
||||||
id_project = Project ID
|
|
||||||
project = Project
|
|
||||||
projects = Projects
|
|
||||||
new_project = New Project
|
|
||||||
update_project = Update Project
|
|
||||||
hikes = Hikes
|
|
||||||
mode = Mode
|
|
||||||
mode_previz = Project in preparation
|
|
||||||
mode_blog = Active Project
|
|
||||||
mode_histo = Archived Project
|
|
||||||
code_name = Code name
|
|
||||||
start = Start
|
|
||||||
end = End
|
|
||||||
feeds = Feeds
|
|
||||||
id_feed = Feed ID
|
|
||||||
ref_feed_id = Ref. Feed ID
|
|
||||||
id_spot = Spot ID
|
|
||||||
name = Name
|
|
||||||
status = Status
|
|
||||||
last_update = Last Spot Check
|
|
||||||
ref_spot_id = Ref. Spot ID
|
|
||||||
model = Model
|
|
||||||
delete = Delete
|
|
||||||
id_user = User ID
|
|
||||||
user_name = User Name
|
|
||||||
active_users = Active Users
|
|
||||||
language = Language
|
|
||||||
clearance = Clearance
|
|
||||||
toolbox = Toolbox
|
|
||||||
|
|
||||||
unit_day = day
|
|
||||||
unit_days = days
|
|
||||||
unit_hour = h
|
|
||||||
|
|
||||||
newsletter = Keep in touch!
|
|
||||||
nl_email_placeholder= my@email.com
|
|
||||||
nl_invalid_email = It doesn't look like an email
|
|
||||||
nl_subscribed_desc = You're all set. We'll send you updates as soon as we get them
|
|
||||||
nl_unsubscribed_desc= Write down your email address and we'll send you François' position as soon as we get it :)
|
|
||||||
nl_email_exists = This email is already subscribed. You can unsubscribe by clicking on the button above.
|
|
||||||
nl_subscribe = Subscribe
|
|
||||||
nl_subscribed = Thanks! You'll receive a confirmation email shortly
|
|
||||||
nl_unsubscribe = Unsubscribe
|
|
||||||
nl_unsubscribed = Done. No more junk mail from us
|
|
||||||
nl_unknown_email = Unknown email address
|
|
||||||
|
|
||||||
email_unsubscribe = PS: Changed your mind?
|
|
||||||
email_unsub_btn = Unsubscribe
|
|
||||||
|
|
||||||
email_conf_subject = Successful Registration
|
|
||||||
conf_preheader = Thanks for keeping in touch!
|
|
||||||
conf_thanks_sub = You're all set!
|
|
||||||
conf_body_para_1 = Thank you for checking in on my wanderings :). I'll make sure to keep you posted on my progress along the trail.
|
|
||||||
conf_body_para_2 = I usually check-in once a day, plus sometimes on special events, like successful peak ascents. I am using a GPS-based device (PLB) which does not require phone reception to work. Thus the messages should be pretty frequent, but, being awestruck by the beauty of nature, I could also just forget to send a signal once in a while. So do not worry if you don't receive anything for a couple of days.
|
|
||||||
conf_body_para_3 = If I've posted some pictures recently, you should also get them in the same email.
|
|
||||||
conf_body_conclusion= See you down the road!
|
|
||||||
conf_signature = --François
|
|
||||||
|
|
||||||
email_update_subject= Spotted!
|
|
||||||
update_preheader = New position received
|
|
||||||
update_title = Message
|
|
||||||
update_latest_news = Latest news:
|
|
||||||
|
|
||||||
distance = Distance
|
|
||||||
elevation = Elevation
|
|
||||||
segment_length = Segment length
|
|
||||||
type = Track Type
|
|
||||||
legend = Legend
|
|
||||||
|
|
||||||
credits_project = Spotty Project
|
|
||||||
credits_git = Git Repository
|
|
||||||
credits_license = under GPLv3 license
|
|
||||||
|
|
||||||
weather_type_1 = Blowing Or Drifting Snow
|
|
||||||
weather_type_2 = Drizzle
|
|
||||||
weather_type_3 = Heavy Drizzle
|
|
||||||
weather_type_4 = Light Drizzle
|
|
||||||
weather_type_5 = Heavy Drizzle/Rain
|
|
||||||
weather_type_6 = Light Drizzle/Rain
|
|
||||||
weather_type_7 = Duststorm
|
|
||||||
weather_type_8 = Fog
|
|
||||||
weather_type_9 = Freezing Drizzle/Freezing Rain
|
|
||||||
weather_type_10 = Heavy Freezing Drizzle/Freezing Rain
|
|
||||||
weather_type_11 = Light Freezing Drizzle/Freezing Rain
|
|
||||||
weather_type_12 = Freezing Fog
|
|
||||||
weather_type_13 = Heavy Freezing Rain
|
|
||||||
weather_type_14 = Light Freezing Rain
|
|
||||||
weather_type_15 = Funnel Cloud/Tornado
|
|
||||||
weather_type_16 = Hail Showers
|
|
||||||
weather_type_17 = Ice
|
|
||||||
weather_type_18 = Lightning Without Thunder
|
|
||||||
weather_type_19 = Mist
|
|
||||||
weather_type_20 = Precipitation In Vicinity
|
|
||||||
weather_type_21 = Rain
|
|
||||||
weather_type_22 = Heavy Rain And Snow
|
|
||||||
weather_type_23 = Light Rain And Snow
|
|
||||||
weather_type_24 = Rain Showers
|
|
||||||
weather_type_25 = Heavy Rain
|
|
||||||
weather_type_26 = Light Rain
|
|
||||||
weather_type_27 = Sky Coverage Decreasing
|
|
||||||
weather_type_28 = Sky Coverage Increasing
|
|
||||||
weather_type_29 = Sky Unchanged
|
|
||||||
weather_type_30 = Smoke Or Haze
|
|
||||||
weather_type_31 = Snow
|
|
||||||
weather_type_32 = Snow And Rain Showers
|
|
||||||
weather_type_33 = Snow Showers
|
|
||||||
weather_type_34 = Heavy Snow
|
|
||||||
weather_type_35 = Light Snow
|
|
||||||
weather_type_36 = Squalls
|
|
||||||
weather_type_37 = Thunderstorm
|
|
||||||
weather_type_38 = Thunderstorm Without Precipitation
|
|
||||||
weather_type_39 = Diamond Dust
|
|
||||||
weather_type_40 = Hail
|
|
||||||
weather_type_41 = Overcast
|
|
||||||
weather_type_42 = Partially cloudy
|
|
||||||
weather_type_43 = Clear
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
locale = es_ES
|
|
||||||
page_og_desc = Mantente en contacto con François durante sus aventuras a la montaña
|
|
||||||
error_commit_db = Error SQL
|
|
||||||
unknown_field = Campo "$0" desconocido
|
|
||||||
|
|
||||||
nav_back = Atrás
|
|
||||||
|
|
||||||
admin = Administración
|
|
||||||
admin_config = Configuración
|
|
||||||
admin_upload = Cargar
|
|
||||||
save = Guardar
|
|
||||||
admin_save_success = Guardado
|
|
||||||
|
|
||||||
track_main = Camino principal
|
|
||||||
track_off-track = Variante
|
|
||||||
track_hitchhiking = Autostop
|
|
||||||
track_download = Descarga la ruta GPX
|
|
||||||
|
|
||||||
upload_title = Cargar fotos y videos
|
|
||||||
upload_mode_archived= El proyecto "$0" esta archivado. No se puede cargar
|
|
||||||
upload_success = $0 ha sido subido
|
|
||||||
upload_media_exist = La imagen $0 ya existe
|
|
||||||
|
|
||||||
post_message = Mensaje
|
|
||||||
post_name = Nombre
|
|
||||||
post_new_message = Mensaje nuevo
|
|
||||||
and = y
|
|
||||||
counter = No. $0
|
|
||||||
send = Enviar
|
|
||||||
|
|
||||||
maps = Mapas de base
|
|
||||||
map_satellite = Satélite
|
|
||||||
map_otm = Open Topo Map
|
|
||||||
map_ign_france = IGN (Francia)
|
|
||||||
map_ign_spain = IGN (España)
|
|
||||||
map_linz = LINZ
|
|
||||||
map_usgs = USGS
|
|
||||||
map_natgeo = National Geographic
|
|
||||||
map_outdoors = Mapbox Topo
|
|
||||||
|
|
||||||
image = Foto
|
|
||||||
images = Fotos
|
|
||||||
image_taken = Foto tomada el $0
|
|
||||||
video = Video
|
|
||||||
video_taken = Video filmado el $0
|
|
||||||
add_on = Agregado el $0
|
|
||||||
click_watch = Haz clic para ver el video
|
|
||||||
click_zoom = Haz clic para ampliar
|
|
||||||
media_count = Media $0 de $1
|
|
||||||
media_no_id = Falta el ID del sujeto
|
|
||||||
media_comment_update= Comentario "$0" actualizado
|
|
||||||
see_on_google = Ver la posición en Google Maps
|
|
||||||
copy_to_clipboard = Copiar el enlace
|
|
||||||
link_copied = ¡Enlace copiado!
|
|
||||||
|
|
||||||
city_time = Hora de $0
|
|
||||||
local_time = $0 hora local
|
|
||||||
your_time = $0 en tu zona horaria
|
|
||||||
date_time = $0 a la $1
|
|
||||||
time_zone = Huso horario
|
|
||||||
|
|
||||||
id_project = Proyecto ID
|
|
||||||
project = Proyecto
|
|
||||||
projects = Proyectos
|
|
||||||
new_project = Nuevo proyecto
|
|
||||||
update_project = Actualizar el proyecto
|
|
||||||
hikes = Senderos
|
|
||||||
mode = Modo
|
|
||||||
mode_previz = Proyecto en preparación
|
|
||||||
mode_blog = Proyecto activo
|
|
||||||
mode_histo = Proyecto archivado
|
|
||||||
code_name = Nombre clave
|
|
||||||
start = Inicio
|
|
||||||
end = Fin
|
|
||||||
feeds = Feeds
|
|
||||||
id_feed = ID Feed
|
|
||||||
ref_feed_id = ID Feed ref.
|
|
||||||
id_spot = ID Spot
|
|
||||||
name = Descripción
|
|
||||||
status = Estado
|
|
||||||
last_update = Última actualización de Spot
|
|
||||||
ref_spot_id = ID Spot ref.
|
|
||||||
model = Modelo
|
|
||||||
delete = Borrar
|
|
||||||
id_user = ID del usuario
|
|
||||||
user_name = Nombre
|
|
||||||
active_users = Usuarios activos
|
|
||||||
language = Idioma
|
|
||||||
clearance = Nivel de autorización
|
|
||||||
toolbox = Herramientas
|
|
||||||
|
|
||||||
unit_day = Día
|
|
||||||
unit_days = Días
|
|
||||||
unit_hour = h
|
|
||||||
|
|
||||||
newsletter = Mantenerse en contacto
|
|
||||||
nl_email_placeholder= nombre@email.com
|
|
||||||
nl_invalid_email = Esto no parece una dirección de correo electrónico
|
|
||||||
nl_subscribed_desc = Todo esta listo. Te enviaremos noticias frescas en cuanto las recibamos. Prometido...
|
|
||||||
nl_unsubscribed_desc= Anade tu dirección de correo electrónico y te enviaremos la posicion actualizada de François tan pronto como la recibamos :)
|
|
||||||
nl_email_exists = Esta dirección de correo electrónico ya está registrada. Puedes darte de baja haciendo clic en el botón de arriba.
|
|
||||||
nl_subscribe = Suscribir
|
|
||||||
nl_subscribed = ¡Gracias! Recibirás un correo electrónico de confirmación
|
|
||||||
nl_unsubscribe = Desinscribirse
|
|
||||||
nl_unsubscribed = Está hecho. ¡No más spam!
|
|
||||||
nl_unknown_email = Dirección de email desconocida
|
|
||||||
|
|
||||||
email_unsubscribe = PD: ¿Demasiados correos electrónicos?
|
|
||||||
email_unsub_btn = Desinscribirse
|
|
||||||
|
|
||||||
email_conf_subject = Confirmación
|
|
||||||
conf_preheader = ¡Gracias por mantenerte en contacto!
|
|
||||||
conf_thanks_sub = ¡Hecho!
|
|
||||||
conf_body_para_1 = Os agradezco mucho que sigais mi proyecto, y os intereseis de la evolucion. Os prometo que os mantendré informados sobre mi progreso.
|
|
||||||
conf_body_para_2 = Normalmente envío un mensaje una vez al día. Cuando voy a lugares guays, envío uno extra (cimas, ese tipo de cosas). Estoy usando una dispositivo GPS para enviar la señal, por lo que no necesito una red telefónica para que funcione. Sin embargo, puede haber ocasiones en las que presione el botón. Por lo tanto, no se preocupe si no recibe mensajes durante uno o dos días.
|
|
||||||
conf_body_para_3 = Cuando añada fotos en la página, también deberás encontrarlas en este correo electrónico.
|
|
||||||
conf_body_conclusion= ¡Nos vemos en el camino!
|
|
||||||
conf_signature = --François
|
|
||||||
|
|
||||||
email_update_subject= Nueva posición recibida
|
|
||||||
update_preheader = ¡Nueva posición!
|
|
||||||
update_title = Mensaje
|
|
||||||
update_latest_news = Últimas noticias:
|
|
||||||
|
|
||||||
distance = Distancia
|
|
||||||
elevation = Elevación
|
|
||||||
segment_length = Tamaño del segmento
|
|
||||||
type = Tipo de sendero
|
|
||||||
legend = Leyenda
|
|
||||||
|
|
||||||
credits_project = Proyecto Spotty
|
|
||||||
credits_git = Repositorio de Git
|
|
||||||
credits_license = bajo licencia GPLv3
|
|
||||||
|
|
||||||
weather_type_1 Nieve que sopla o a la deriva
|
|
||||||
weather_type_2 Llovizna
|
|
||||||
weather_type_3 Llovizna fuerte
|
|
||||||
weather_type_4 Llovizna ligera
|
|
||||||
weather_type_5 Fuerte llovizna / lluvia
|
|
||||||
weather_type_6 Llovizna ligera / Lluvia
|
|
||||||
weather_type_7 Tormenta de arena
|
|
||||||
weather_type_8 Niebla
|
|
||||||
weather_type_9 Llovizna helada / Lluvia helada
|
|
||||||
weather_type_10 Fuerte llovizna helada / lluvia helada
|
|
||||||
weather_type_11 Llovizna helada ligera / lluvia helada
|
|
||||||
weather_type_12 Niebla helada
|
|
||||||
weather_type_13 Lluvia helada intensa
|
|
||||||
weather_type_14 Lluvia helada ligera
|
|
||||||
weather_type_15 Nube de embudo / Tornado
|
|
||||||
weather_type_16 Lluvias de granizo
|
|
||||||
weather_type_17 Hielo
|
|
||||||
weather_type_18 Rayo sin trueno
|
|
||||||
weather_type_19 Niebla
|
|
||||||
weather_type_20 Precipitación en las proximidades
|
|
||||||
weather_type_21 Lluvia
|
|
||||||
weather_type_22 Fuertes lluvias y nieve
|
|
||||||
weather_type_23 Lluvia ligera y nieve
|
|
||||||
weather_type_24 Lluvias
|
|
||||||
weather_type_25 Lluvia Pesada
|
|
||||||
weather_type_26 Lluvia ligera
|
|
||||||
weather_type_27 Disminución de la cobertura del cielo
|
|
||||||
weather_type_28 Aumento de la cobertura del cielo
|
|
||||||
weather_type_29 Cielo sin cambios
|
|
||||||
weather_type_30 Humo o neblina
|
|
||||||
weather_type_31 Nieve
|
|
||||||
weather_type_32 Lluvias y nieve
|
|
||||||
weather_type_33 Duchas de nieve
|
|
||||||
weather_type_34 Fuertes nevadas
|
|
||||||
weather_type_35 Nieve ligera
|
|
||||||
weather_type_36 Chubascos
|
|
||||||
weather_type_37 Tormenta
|
|
||||||
weather_type_38 Tormenta sin precipitaciones
|
|
||||||
weather_type_39 Polvo de diamante
|
|
||||||
weather_type_40 Granizo
|
|
||||||
weather_type_41 Nublado
|
|
||||||
weather_type_42 Parcialmente nublado
|
|
||||||
weather_type_43 Claro
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
locale = fr_CH
|
|
||||||
page_og_desc = Gardez le contact avec François lorsqu'il part sur les chemins
|
|
||||||
error_commit_db = Error lors de la requête SQL
|
|
||||||
unknown_field = Champ "$0" inconnu
|
|
||||||
|
|
||||||
nav_back = Retour
|
|
||||||
|
|
||||||
admin = Administration
|
|
||||||
admin_config = Paramètres
|
|
||||||
admin_upload = Uploader
|
|
||||||
save = Sauvegarder
|
|
||||||
admin_save_success = Sauvegardé
|
|
||||||
|
|
||||||
track_main = Trajet principal
|
|
||||||
track_off-track = Variante
|
|
||||||
track_hitchhiking = Hors rando
|
|
||||||
track_download = Télécharger la trace GPX
|
|
||||||
|
|
||||||
upload_title = Uploader photos & vidéos
|
|
||||||
upload_mode_archived= Le projet "$0" a été archivé. Aucun upload possible
|
|
||||||
upload_success = $0 a été uploadé
|
|
||||||
upload_media_exist = l'image $0 existe déjà
|
|
||||||
|
|
||||||
post_message = Message
|
|
||||||
post_name = Nom
|
|
||||||
post_new_message = Nouveau message
|
|
||||||
and = et
|
|
||||||
counter = N°$0
|
|
||||||
send = Envoyer
|
|
||||||
|
|
||||||
maps = Fonds de carte
|
|
||||||
map_satellite = Satellite
|
|
||||||
map_otm = Open Topo Map
|
|
||||||
map_ign_france = IGN (France)
|
|
||||||
map_ign_spain = IGN (Espagne)
|
|
||||||
map_linz = LINZ
|
|
||||||
map_usgs = USGS
|
|
||||||
map_natgeo = National Geographic
|
|
||||||
map_outdoors = Mapbox Topo
|
|
||||||
|
|
||||||
image = Photo
|
|
||||||
images = Photos
|
|
||||||
image_taken = prise le $0
|
|
||||||
video = Vidéo
|
|
||||||
video_taken = filmée le $0
|
|
||||||
add_on = ajoutée le $0
|
|
||||||
click_watch = Click pour voir la vidéo
|
|
||||||
click_zoom = Click pour zoomer
|
|
||||||
media_count = Média $0 sur $1
|
|
||||||
media_no_id = ID du média manquant
|
|
||||||
media_comment_update= Commentaire du media "$0" mis-à-jour
|
|
||||||
see_on_google = Voir la position sur Google Maps
|
|
||||||
copy_to_clipboard = Copie le lien dans le presse-papier
|
|
||||||
link_copied = Lien copié !
|
|
||||||
|
|
||||||
city_time = heure de $0
|
|
||||||
local_time = $0 heure locale
|
|
||||||
your_time = $0 dans votre fuseau horaire
|
|
||||||
date_time = $0 à $1
|
|
||||||
time_zone = Fuseau horaire
|
|
||||||
|
|
||||||
id_project = ID projet
|
|
||||||
project = Projet
|
|
||||||
projects = Projets
|
|
||||||
new_project = Nouveau projet
|
|
||||||
update_project = Mettre à jour le projet
|
|
||||||
hikes = Randonnées
|
|
||||||
mode = Mode
|
|
||||||
mode_previz = Projet en cours de préparation
|
|
||||||
mode_blog = Projet actif
|
|
||||||
mode_histo = Projet archivé
|
|
||||||
code_name = Nom de code
|
|
||||||
start = Départ
|
|
||||||
end = Arrivée
|
|
||||||
feeds = Feeds
|
|
||||||
id_feed = ID Feed
|
|
||||||
ref_feed_id = ID Feed ref.
|
|
||||||
id_spot = ID Spot
|
|
||||||
name = Description
|
|
||||||
status = Statut
|
|
||||||
last_update = Dernière vérification Spot
|
|
||||||
ref_spot_id = ID Spot ref.
|
|
||||||
model = Modèle
|
|
||||||
delete = Supprimer
|
|
||||||
id_user = ID Utilisateur
|
|
||||||
user_name = Nom
|
|
||||||
active_users = Utilisateurs actifs
|
|
||||||
language = Langue
|
|
||||||
clearance = Niveau d'autorisation
|
|
||||||
toolbox = Boite à outils
|
|
||||||
|
|
||||||
unit_day = jour
|
|
||||||
unit_days = jours
|
|
||||||
unit_hour = h
|
|
||||||
|
|
||||||
newsletter = Rester en contact
|
|
||||||
nl_email_placeholder= mon@email.com
|
|
||||||
nl_invalid_email = Ceci ne ressemble pas à une adresse email
|
|
||||||
nl_subscribed_desc = C'est tout bon. On t'envoie des nouvelles fraiches dès qu'on les reçoit. Parole de scout.
|
|
||||||
nl_unsubscribed_desc= Ajoute ton adresse email et on t'enverra la nouvelle position de François dès qu'on la reçoit :)
|
|
||||||
nl_email_exists = Cette adresse email est déjà enregistrée. Vous pouvez vous désinscrire en cliquant sur le bouton ci-dessus.
|
|
||||||
nl_subscribe = S'abonner
|
|
||||||
nl_subscribed = Merci ! Tu vas recevoir un email de confirmation très bientôt
|
|
||||||
nl_unsubscribe = Se désinscrire
|
|
||||||
nl_unsubscribed = C'est fait. Fini le spam!
|
|
||||||
nl_unknown_email = Adresse email inconnue
|
|
||||||
|
|
||||||
email_unsubscribe = PS: Trop d'emails ?
|
|
||||||
email_unsub_btn = Se désinscrire
|
|
||||||
|
|
||||||
email_conf_subject = Confirmation
|
|
||||||
conf_preheader = Merci de rester en contact !
|
|
||||||
conf_thanks_sub = C'est tout bon !
|
|
||||||
conf_body_para_1 = C'est gentil de venir voir où j'en suis. Promis, je vous tiendrais au courant de mon avancée.
|
|
||||||
conf_body_para_2 = En général, j'envoie un message une fois par jour. Lorsque je passe à des endroits sympas, j'en envoie un supplémentaire (ascension de sommets, ce genre de choses). J'utilise une balise GPS pour envoyer le signal, je n'ai donc pas besoin de réseau téléphonique pour que cela fonctionne. Cependant, il peut m'arriver d'appuyer sur le bouton. Donc pas de raison de s'inquiéter si vous ne recevez pas de messages pendant une journée ou deux.
|
|
||||||
conf_body_para_3 = Si j'ai ajouté des photos sur le site récemment, vous devriez aussi les retrouver dans cet email.
|
|
||||||
conf_body_conclusion= A bientôt sur les chemins !
|
|
||||||
conf_signature = --François
|
|
||||||
|
|
||||||
email_update_subject= Nouvelle position reçue
|
|
||||||
update_preheader = Nouvelle position !
|
|
||||||
update_title = Message
|
|
||||||
update_latest_news = Dernières nouvelles :
|
|
||||||
|
|
||||||
distance = Distance
|
|
||||||
elevation = Dénivelé
|
|
||||||
segment_length = Taille du segment
|
|
||||||
type = Type de rando
|
|
||||||
legend = Légende
|
|
||||||
|
|
||||||
credits_project = Projet Spotty
|
|
||||||
credits_git = Dépôt Git
|
|
||||||
credits_license = sous licence GPLv3
|
|
||||||
|
|
||||||
weather_type_1 = Poudrerie ou neige à la dérive
|
|
||||||
weather_type_2 = Bruine
|
|
||||||
weather_type_3 = Bruine lourde
|
|
||||||
weather_type_4 = Bruine légère
|
|
||||||
weather_type_5 = Forte bruine / pluie
|
|
||||||
weather_type_6 = Légère bruine / pluie
|
|
||||||
weather_type_7 = Tempête de poussière
|
|
||||||
weather_type_8 = Brouillard
|
|
||||||
weather_type_9 = Bruine verglaçante / Pluie verglaçante
|
|
||||||
weather_type_10 = Forte bruine verglaçante / pluie verglaçante
|
|
||||||
weather_type_11 = Légère bruine verglaçante / pluie verglaçante
|
|
||||||
weather_type_12 = Brouillard verglaçant
|
|
||||||
weather_type_13 = Forte pluie verglaçante
|
|
||||||
weather_type_14 = Légère pluie verglaçante
|
|
||||||
weather_type_15 = Nuage d'entonnoir / Tornade
|
|
||||||
weather_type_16 = Douches de grêle
|
|
||||||
weather_type_17 = La glace
|
|
||||||
weather_type_18 = Foudre sans tonnerre
|
|
||||||
weather_type_19 = Brouillard
|
|
||||||
weather_type_20 = Précipitations à proximité
|
|
||||||
weather_type_21 = Pluie
|
|
||||||
weather_type_22 = Forte pluie et neige
|
|
||||||
weather_type_23 = Légère pluie et neige
|
|
||||||
weather_type_24 = Averses de pluie
|
|
||||||
weather_type_25 = Forte pluie
|
|
||||||
weather_type_26 = Pluie légère
|
|
||||||
weather_type_27 = Couverture du ciel en baisse
|
|
||||||
weather_type_28 = Augmentation de la couverture du ciel
|
|
||||||
weather_type_29 = Ciel inchangé
|
|
||||||
weather_type_30 = Fumée ou brume
|
|
||||||
weather_type_31 = Neige
|
|
||||||
weather_type_32 = Averses de neige et de pluie
|
|
||||||
weather_type_33 = Douches de neige
|
|
||||||
weather_type_34 = Beaucoup de neige
|
|
||||||
weather_type_35 = Neige légère
|
|
||||||
weather_type_36 = Grains
|
|
||||||
weather_type_37 = Orage
|
|
||||||
weather_type_38 = Orage sans précipitations
|
|
||||||
weather_type_39 = La poussière de diamant
|
|
||||||
weather_type_40 = Saluer
|
|
||||||
weather_type_41 = Couvert
|
|
||||||
weather_type_42 = Partiellement nuageux
|
|
||||||
weather_type_43 = Clair
|
|
||||||
168
lib/Controller.php
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Franzz\Spot;
|
||||||
|
|
||||||
|
use Franzz\Objects\PhpObject;
|
||||||
|
use Franzz\Objects\ToolBox;
|
||||||
|
|
||||||
|
//TODO Keep only local specificities and move bulk to Franzz\Objects\Controller
|
||||||
|
class Controller extends PhpObject
|
||||||
|
{
|
||||||
|
const MUTATING_ACTIONS = array(
|
||||||
|
'add_post',
|
||||||
|
'subscribe',
|
||||||
|
'unsubscribe',
|
||||||
|
'update_project',
|
||||||
|
'upload',
|
||||||
|
'add_comment',
|
||||||
|
'add_position',
|
||||||
|
'admin_set',
|
||||||
|
'admin_create',
|
||||||
|
'admin_delete',
|
||||||
|
'build_geojson'
|
||||||
|
);
|
||||||
|
|
||||||
|
private Spot $oSpot;
|
||||||
|
private array $asReq;
|
||||||
|
private string $sCsrfToken = '';
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct(__CLASS__);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setReqVal(string $sKey, $oValue, string $sValidation=''): void
|
||||||
|
{
|
||||||
|
$this->asReq[$sKey] = $this->validateValue($sValidation, $oValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle($sProcessPage, array $argv = array()): string
|
||||||
|
{
|
||||||
|
//Start buffering so warnings/notices can be collected
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
//Parse variables
|
||||||
|
$asReq = ToolBox::getRequest($argv);
|
||||||
|
$this->asReq = array();
|
||||||
|
$sAction = $asReq['a'] ?? '';
|
||||||
|
$this->setReqVal('t', $asReq['t'] ?? '');
|
||||||
|
$this->setReqVal('name', $asReq['name'] ?? '');
|
||||||
|
$this->setReqVal('content', $asReq['content'] ?? '');
|
||||||
|
$this->setReqVal('id_project', $asReq['id_project'] ?? 0, 'positiveInt');
|
||||||
|
$this->setReqVal('id', $asReq['id'] ?? 0);
|
||||||
|
$this->setReqVal('id_entity', $asReq['id'] ?? 0, 'positiveInt');
|
||||||
|
$this->setReqVal('field', $asReq['field'] ?? '');
|
||||||
|
$this->setReqVal('value', $asReq['value'] ?? '');
|
||||||
|
$this->setReqVal('type', $asReq['type'] ?? '');
|
||||||
|
$this->setReqVal('email', $asReq['email'] ?? '');
|
||||||
|
$this->setReqVal('latitude', $asReq['latitude'] ?? '');
|
||||||
|
$this->setReqVal('longitude', $asReq['longitude'] ?? '');
|
||||||
|
$this->setReqVal('timestamp', $asReq['timestamp'] ?? 0, 'positiveInt');
|
||||||
|
$this->setReqVal('csrf_token', $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ($_POST['csrf_token'] ?? ''));
|
||||||
|
|
||||||
|
//Create Spot Instance
|
||||||
|
$this->oSpot = new Spot($sProcessPage, $this->asReq['t']);
|
||||||
|
$this->oSpot->setProjectId($this->asReq['id_project']);
|
||||||
|
|
||||||
|
//Validate CSRF & dispatch
|
||||||
|
if(!$this->validateMutationRequest($sAction)) $sResult = Spot::getJsonResult(false, Spot::UNAUTHORIZED);
|
||||||
|
elseif($sAction == '') $sResult = $this->oSpot->getAppMainPage($this->getCsrfToken());
|
||||||
|
else $sResult = $this->dispatch($sAction);
|
||||||
|
|
||||||
|
//Clean errors
|
||||||
|
$sDebug = ob_get_clean();
|
||||||
|
if($sDebug != '') $this->oSpot->addUncaughtError($sDebug);
|
||||||
|
|
||||||
|
return $sResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateMutationRequest(string $sAction): bool
|
||||||
|
{
|
||||||
|
return
|
||||||
|
PHP_SAPI === 'cli'
|
||||||
|
||
|
||||||
|
!in_array($sAction, self::MUTATING_ACTIONS, true)
|
||||||
|
||
|
||||||
|
($_SERVER['REQUEST_METHOD'] ?? '') === 'POST' && $this->checkCsrfToken($this->asReq['csrf_token'])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getCsrfToken(): string
|
||||||
|
{
|
||||||
|
if($this->sCsrfToken === '') $this->initCsrfToken();
|
||||||
|
return $this->sCsrfToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setCsrfToken(): void
|
||||||
|
{
|
||||||
|
if(empty($_SESSION['csrf_token'])) $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
|
$this->sCsrfToken = $_SESSION['csrf_token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function initCsrfToken(): void
|
||||||
|
{
|
||||||
|
if(PHP_SAPI === 'cli') return;
|
||||||
|
|
||||||
|
$bCloseSession = false;
|
||||||
|
if(session_status() !== PHP_SESSION_ACTIVE) {
|
||||||
|
$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_start();
|
||||||
|
$bCloseSession = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->setCsrfToken();
|
||||||
|
if($bCloseSession) session_write_close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkCsrfToken(string $sClientToken): bool
|
||||||
|
{
|
||||||
|
$sServerToken = $this->getCsrfToken();
|
||||||
|
return PHP_SAPI === 'cli' || ($sServerToken !== '' && is_string($sClientToken) && hash_equals($sServerToken, $sClientToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function dispatch(string $sAction): string
|
||||||
|
{
|
||||||
|
return match($sAction) {
|
||||||
|
'markers' => $this->oSpot->getMarkers(),
|
||||||
|
'last_update' => $this->oSpot->getLastUpdate(),
|
||||||
|
'geojson' => $this->oSpot->getProjectGeoJson(),
|
||||||
|
'next_feed' => $this->oSpot->getNextFeed($this->asReq['id']),
|
||||||
|
'new_feed' => $this->oSpot->getNewFeed($this->asReq['id']),
|
||||||
|
'add_post' => $this->oSpot->addPost($this->asReq['name'], $this->asReq['content']),
|
||||||
|
'subscribe' => $this->oSpot->subscribe($this->asReq['email'], $this->asReq['name']),
|
||||||
|
'unsubscribe' => $this->oSpot->unsubscribe(),
|
||||||
|
'unsubscribe_email' => $this->oSpot->unsubscribeFromEmail($this->asReq['id_entity']),
|
||||||
|
'update_project' => $this->oSpot->updateProject(),
|
||||||
|
default => $this->dispatchAdmin($sAction)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function dispatchAdmin(string $sAction): string
|
||||||
|
{
|
||||||
|
if(!$this->oSpot->checkUserClearance(User::CLEARANCE_ADMIN)) {
|
||||||
|
return Spot::getJsonResult(false, Spot::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
return match($sAction) {
|
||||||
|
'upload' => $this->oSpot->upload(),
|
||||||
|
'add_comment' => $this->oSpot->addComment($this->asReq['id_entity'], $this->asReq['content']),
|
||||||
|
'add_position' => $this->oSpot->addPosition($this->asReq['latitude'], $this->asReq['longitude'], $this->asReq['timestamp']),
|
||||||
|
'admin_get' => $this->oSpot->getAdminSettings(),
|
||||||
|
'admin_set' => $this->oSpot->setAdminSettings($this->asReq['type'], $this->asReq['id_entity'], $this->asReq['field'], $this->asReq['value']),
|
||||||
|
'admin_create' => $this->oSpot->createAdminSettings($this->asReq['type']),
|
||||||
|
'admin_delete' => $this->oSpot->deleteAdminSettings($this->asReq['type'], $this->asReq['id_entity']),
|
||||||
|
'sql' => $this->oSpot->getDbBuildScript(),
|
||||||
|
'build_geojson' => $this->oSpot->buildGeoJSON($this->asReq['name']),
|
||||||
|
default => Spot::getJsonResult(false, Spot::NOT_FOUND)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function validateValue(string $sValidation, $oValue=0)
|
||||||
|
{
|
||||||
|
return match($sValidation) {
|
||||||
|
'' => $oValue,
|
||||||
|
'positiveInt' => filter_var($oValue, FILTER_VALIDATE_INT, array('options' => array('default' => 0, 'min_range' => 0)))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
45
lib/Converter.php
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Franzz\Spot;
|
||||||
|
use Franzz\Objects\PhpObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GPX to GeoJSON Converter
|
||||||
|
*
|
||||||
|
* To convert a gpx file:
|
||||||
|
* 1. Add file <file_name>.gpx to geo/ folder
|
||||||
|
* 2. Assign file to project: UPDATE projects SET codename = '<file_name>' WHERE id_project = <id_project>;
|
||||||
|
* 3. Load any page
|
||||||
|
*
|
||||||
|
* To force gpx rebuild:
|
||||||
|
* ?a=build_geojson&name=<file_name>
|
||||||
|
*/
|
||||||
|
class Converter extends PhpObject {
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
parent::__construct(__CLASS__);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function convertToGeoJson($sCodeName) {
|
||||||
|
$oGpx = new Gpx($sCodeName);
|
||||||
|
$oGeoJson = new GeoJson($sCodeName);
|
||||||
|
|
||||||
|
$oGeoJson->buildTracks($oGpx->getTracks());
|
||||||
|
if($oGeoJson->isSimplicationRequired()) $oGeoJson->buildTracks($oGpx->getTracks(), true);
|
||||||
|
$oGeoJson->sortOffTracks();
|
||||||
|
$oGeoJson->saveFile();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'logs' => $oGpx->getLog().'<br />'.$oGeoJson->getLog(),
|
||||||
|
'center' => $oGeoJson->getCenter()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isGeoJsonValid($sCodeName) {
|
||||||
|
$sGpxFilePath = Gpx::getBackendFilePath($sCodeName);
|
||||||
|
$sGeoJsonFilePath = GeoJson::getBackendFilePath($sCodeName);
|
||||||
|
|
||||||
|
//No need to generate if gpx is missing
|
||||||
|
return !file_exists($sGpxFilePath) || file_exists($sGeoJsonFilePath) && filemtime($sGeoJsonFilePath) >= filemtime($sGpxFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,9 +57,10 @@ class Email extends PhpObject {
|
|||||||
$oPHPMailer->Password = Settings::MAIL_PASS; //SMTP password
|
$oPHPMailer->Password = Settings::MAIL_PASS; //SMTP password
|
||||||
$oPHPMailer->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; //Enable TLS encryption; `PHPMailer::ENCRYPTION_SMTPS` encouraged
|
$oPHPMailer->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; //Enable TLS encryption; `PHPMailer::ENCRYPTION_SMTPS` encouraged
|
||||||
$oPHPMailer->Port = 587; //TCP port to connect to, use 465 for `PHPMailer::ENCRYPTION_SMTPS` above
|
$oPHPMailer->Port = 587; //TCP port to connect to, use 465 for `PHPMailer::ENCRYPTION_SMTPS` above
|
||||||
$oPHPMailer->setFrom(Settings::MAIL_FROM, 'Spotty');
|
$oPHPMailer->setFrom(Settings::MAIL_FROM, Spot::PROJECT_NAME);
|
||||||
$oPHPMailer->addReplyTo(Settings::MAIL_FROM, 'Spotty');
|
$oPHPMailer->addReplyTo(Settings::MAIL_FROM, Spot::PROJECT_NAME);
|
||||||
|
|
||||||
|
$bSuccess = true;
|
||||||
foreach($this->asDests as $asDest) {
|
foreach($this->asDests as $asDest) {
|
||||||
//Message
|
//Message
|
||||||
$this->oTemplate->setLanguage($asDest['language'], Spot::DEFAULT_LANG);
|
$this->oTemplate->setLanguage($asDest['language'], Spot::DEFAULT_LANG);
|
||||||
@@ -72,7 +73,7 @@ class Email extends PhpObject {
|
|||||||
$oPHPMailer->addCustomHeader('List-Unsubscribe-Post','List-Unsubscribe=One-Click');
|
$oPHPMailer->addCustomHeader('List-Unsubscribe-Post','List-Unsubscribe=One-Click');
|
||||||
|
|
||||||
//Email Content
|
//Email Content
|
||||||
$this->oTemplate->setTag('timezone', 'lang:city_time', self::getTimeZoneCity($asDest['timezone']));
|
$this->oTemplate->setTag('timezone', 'lang:time.city', self::getTimeZoneCity($asDest['timezone']));
|
||||||
$sHtmlMessage = $this->oTemplate->getMask();
|
$sHtmlMessage = $this->oTemplate->getMask();
|
||||||
$sPlainMessage = strip_tags(str_replace('<br />', "\n", $sHtmlMessage));
|
$sPlainMessage = strip_tags(str_replace('<br />', "\n", $sHtmlMessage));
|
||||||
|
|
||||||
@@ -86,11 +87,10 @@ class Email extends PhpObject {
|
|||||||
|
|
||||||
//Content
|
//Content
|
||||||
$oPHPMailer->isHTML(true);
|
$oPHPMailer->isHTML(true);
|
||||||
$oPHPMailer->Subject = $this->oTemplate->getTranslator()->getTranslation($this->sTemplateName.'_subject');
|
$oPHPMailer->Subject = $this->oTemplate->getTranslator()->getTranslation($this->sTemplateName.'.subject');
|
||||||
$oPHPMailer->Body = $sHtmlMessage;
|
$oPHPMailer->Body = $sHtmlMessage;
|
||||||
$oPHPMailer->AltBody = $sPlainMessage;
|
$oPHPMailer->AltBody = $sPlainMessage;
|
||||||
|
|
||||||
$bSuccess = true;
|
|
||||||
try {
|
try {
|
||||||
$bSuccess = $bSuccess && $oPHPMailer->send();
|
$bSuccess = $bSuccess && $oPHPMailer->send();
|
||||||
}
|
}
|
||||||
35
lib/Geo.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Franzz\Spot;
|
||||||
|
use Franzz\Objects\PhpObject;
|
||||||
|
use \Settings;
|
||||||
|
|
||||||
|
abstract class Geo extends PhpObject {
|
||||||
|
protected const EXT = '';
|
||||||
|
|
||||||
|
const GEO_FOLDER = 'geo';
|
||||||
|
const OPT_SIMPLE = 'simplification';
|
||||||
|
|
||||||
|
protected array $asTracks;
|
||||||
|
protected string $sFilePath;
|
||||||
|
|
||||||
|
public function __construct(string $sCodeName) {
|
||||||
|
parent::__construct(get_class($this), Settings::DEBUG, PhpObject::MODE_HTML);
|
||||||
|
$this->sFilePath = self::getBackEndFilePath($sCodeName);
|
||||||
|
$this->asTracks = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Access from backend
|
||||||
|
public static function getBackendFilePath(string $sCodeName) {
|
||||||
|
return '../resources/'.self::GEO_FOLDER.'/'.$sCodeName.static::EXT;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Access from frontend (/public/geo is a symlink of /resources/geo)
|
||||||
|
public static function getFrontendFilePath(string $sCodeName) {
|
||||||
|
return self::GEO_FOLDER.'/'.$sCodeName.static::EXT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLog() {
|
||||||
|
return $this->getCleanMessageStack(PhpObject::NOTICE_TAB);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,120 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Franzz\Spot;
|
namespace Franzz\Spot;
|
||||||
use Franzz\Objects\PhpObject;
|
|
||||||
use Franzz\Objects\ToolBox;
|
|
||||||
use \Settings;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GPX to GeoJSON Converter
|
|
||||||
*
|
|
||||||
* To convert a gpx file:
|
|
||||||
* 1. Add file <file_name>.gpx to geo/ folder
|
|
||||||
* 2. Assign file to project: UPDATE projects SET codename = '<file_name>' WHERE id_project = <id_project>;
|
|
||||||
* 3. Load any page
|
|
||||||
*
|
|
||||||
* To force gpx rebuild:
|
|
||||||
* ?a=build_geojson&name=<file_name>
|
|
||||||
*/
|
|
||||||
class Converter extends PhpObject {
|
|
||||||
|
|
||||||
public function __construct() {
|
|
||||||
parent::__construct(__CLASS__);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function convertToGeoJson($sCodeName) {
|
|
||||||
$oGpx = new Gpx($sCodeName);
|
|
||||||
$oGeoJson = new GeoJson($sCodeName);
|
|
||||||
|
|
||||||
$oGeoJson->buildTracks($oGpx->getTracks());
|
|
||||||
if($oGeoJson->isSimplicationRequired()) $oGeoJson->buildTracks($oGpx->getTracks(), true);
|
|
||||||
$oGeoJson->sortOffTracks();
|
|
||||||
$oGeoJson->saveFile();
|
|
||||||
|
|
||||||
return $oGpx->getLog().'<br />'.$oGeoJson->getLog();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function isGeoJsonValid($sCodeName) {
|
|
||||||
$bResult = false;
|
|
||||||
$sGpxFilePath = Geo::getFilePath($sCodeName, Gpx::EXT);
|
|
||||||
$sGeoJsonFilePath = Geo::getFilePath($sCodeName, GeoJson::EXT);
|
|
||||||
|
|
||||||
//No need to generate if gpx is missing
|
|
||||||
if(!file_exists($sGpxFilePath) || file_exists($sGeoJsonFilePath) && filemtime($sGeoJsonFilePath) > filemtime(Geo::getFilePath($sCodeName, Gpx::EXT))) $bResult = true;
|
|
||||||
return $bResult;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Geo extends PhpObject {
|
|
||||||
|
|
||||||
const GEO_FOLDER = 'geo/';
|
|
||||||
const OPT_SIMPLE = 'simplification';
|
|
||||||
|
|
||||||
protected $asTracks;
|
|
||||||
protected $sFilePath;
|
|
||||||
|
|
||||||
public function __construct($sCodeName) {
|
|
||||||
parent::__construct(get_class($this), Settings::DEBUG, PhpObject::MODE_HTML);
|
|
||||||
$this->sFilePath = self::getFilePath($sCodeName, static::EXT);
|
|
||||||
$this->asTracks = array();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getFilePath($sCodeName, $sExt) {
|
|
||||||
return self::GEO_FOLDER.$sCodeName.$sExt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getLog() {
|
|
||||||
return $this->getCleanMessageStack(PhpObject::NOTICE_TAB);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Gpx extends Geo {
|
|
||||||
|
|
||||||
const EXT = '.gpx';
|
|
||||||
|
|
||||||
public function __construct($sCodeName) {
|
|
||||||
parent::__construct($sCodeName);
|
|
||||||
$this->parseFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getTracks() {
|
|
||||||
return $this->asTracks;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function parseFile() {
|
|
||||||
$this->addNotice('Parsing: '.$this->sFilePath);
|
|
||||||
if(!file_exists($this->sFilePath)) $this->addError($this->sFilePath.' file missing');
|
|
||||||
else {
|
|
||||||
$oXml = simplexml_load_file($this->sFilePath);
|
|
||||||
|
|
||||||
//Tracks
|
|
||||||
$this->addNotice('Converting '.count($oXml->trk).' tracks');
|
|
||||||
foreach($oXml->trk as $aoTrack) {
|
|
||||||
$asTrack = array(
|
|
||||||
'name' => (string) $aoTrack->name,
|
|
||||||
'desc' => str_replace("\n", '', ToolBox::fixEOL((strip_tags($aoTrack->desc)))),
|
|
||||||
'cmt' => ToolBox::fixEOL((strip_tags($aoTrack->cmt))),
|
|
||||||
'color' => (string) $aoTrack->extensions->children('gpxx', true)->TrackExtension->DisplayColor,
|
|
||||||
'points'=> array()
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach($aoTrack->trkseg as $asSegment) {
|
|
||||||
foreach($asSegment as $asPoint) {
|
|
||||||
$asTrack['points'][] = array(
|
|
||||||
'lon' => (float) $asPoint['lon'],
|
|
||||||
'lat' => (float) $asPoint['lat'],
|
|
||||||
'ele' => (int) $asPoint->ele
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$this->asTracks[] = $asTrack;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Waypoints
|
|
||||||
$this->addNotice('Ignoring '.count($oXml->wpt).' waypoints');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GeoJson extends Geo {
|
class GeoJson extends Geo {
|
||||||
|
|
||||||
@@ -191,6 +77,11 @@ class GeoJson extends Geo {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if($sType != 'hitchhiking' && str_contains($asTrackProps['desc'], ' ➜ ')) {
|
||||||
|
list($sFrom, $sTo) = explode(' ➜ ', $asTrackProps['desc']);
|
||||||
|
$asTrack['properties']['leg'] = ['from'=> $sFrom, 'to'=> $sTo];
|
||||||
|
}
|
||||||
|
|
||||||
//Track points
|
//Track points
|
||||||
$asTrackPoints = $asTrackProps['points'];
|
$asTrackPoints = $asTrackProps['points'];
|
||||||
$iPointCount = count($asTrackPoints);
|
$iPointCount = count($asTrackPoints);
|
||||||
@@ -217,7 +108,6 @@ class GeoJson extends Geo {
|
|||||||
if($bSimplify) $this->addNotice('Total: '.$iGlobalInvalidPointCount.'/'.$iGlobalPointCount.' points removed ('.round($iGlobalInvalidPointCount / $iGlobalPointCount * 100, 1).'%)');
|
if($bSimplify) $this->addNotice('Total: '.$iGlobalInvalidPointCount.'/'.$iGlobalPointCount.' points removed ('.round($iGlobalInvalidPointCount / $iGlobalPointCount * 100, 1).'%)');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function sortOffTracks() {
|
public function sortOffTracks() {
|
||||||
$this->addNotice('Sorting off-tracks');
|
$this->addNotice('Sorting off-tracks');
|
||||||
|
|
||||||
@@ -269,7 +159,19 @@ class GeoJson extends Geo {
|
|||||||
$this->asTracks = array_values($asTracks);
|
$this->asTracks = array_values($asTracks);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function parseOptions($sComment){
|
public function getCenter() {
|
||||||
|
$asCoords = array();
|
||||||
|
$asMainTracks = array_filter($this->asTracks, function ($astrack) {return $astrack['properties']['type'] == 'main';});
|
||||||
|
foreach($asMainTracks as $asMainTrack) {
|
||||||
|
foreach($asMainTrack['geometry']['coordinates'] as $aiCoords) {
|
||||||
|
$asCoords[] = $aiCoords;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $asCoords[(int) floor(count($asCoords) / 2)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseOptions($sComment) {
|
||||||
$sComment = strip_tags(html_entity_decode($sComment));
|
$sComment = strip_tags(html_entity_decode($sComment));
|
||||||
$asOptions = array(self::OPT_SIMPLE=>'');
|
$asOptions = array(self::OPT_SIMPLE=>'');
|
||||||
foreach(explode("\n", $sComment) as $sLine) {
|
foreach(explode("\n", $sComment) as $sLine) {
|
||||||
52
lib/Gpx.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Franzz\Spot;
|
||||||
|
use Franzz\Objects\ToolBox;
|
||||||
|
|
||||||
|
class Gpx extends Geo {
|
||||||
|
|
||||||
|
const EXT = '.gpx';
|
||||||
|
|
||||||
|
public function __construct($sCodeName) {
|
||||||
|
parent::__construct($sCodeName);
|
||||||
|
$this->parseFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTracks() {
|
||||||
|
return $this->asTracks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseFile() {
|
||||||
|
$this->addNotice('Parsing: '.$this->sFilePath);
|
||||||
|
if(!file_exists($this->sFilePath)) $this->addError($this->sFilePath.' file missing');
|
||||||
|
else {
|
||||||
|
$oXml = simplexml_load_file($this->sFilePath);
|
||||||
|
|
||||||
|
//Tracks
|
||||||
|
$this->addNotice('Converting '.count($oXml->trk).' tracks');
|
||||||
|
foreach($oXml->trk as $aoTrack) {
|
||||||
|
$asTrack = array(
|
||||||
|
'name' => (string) $aoTrack->name,
|
||||||
|
'desc' => str_replace("\n", '', ToolBox::fixEOL((strip_tags($aoTrack->desc)))),
|
||||||
|
'cmt' => ToolBox::fixEOL((strip_tags($aoTrack->cmt))),
|
||||||
|
'color' => (string) $aoTrack->extensions->children('gpxx', true)->TrackExtension->DisplayColor,
|
||||||
|
'points'=> array()
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach($aoTrack->trkseg as $asSegment) {
|
||||||
|
foreach($asSegment as $asPoint) {
|
||||||
|
$asTrack['points'][] = array(
|
||||||
|
'lon' => (float) $asPoint['lon'],
|
||||||
|
'lat' => (float) $asPoint['lat'],
|
||||||
|
'ele' => (int) $asPoint->ele
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->asTracks[] = $asTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Waypoints
|
||||||
|
$this->addNotice('Ignoring '.count($oXml->wpt).' waypoints');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
66
lib/Map.php
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Franzz\Spot;
|
||||||
|
use Franzz\Objects\PhpObject;
|
||||||
|
use Franzz\Objects\Db;
|
||||||
|
use \Settings;
|
||||||
|
|
||||||
|
class Map extends PhpObject {
|
||||||
|
|
||||||
|
const MAP_TABLE = 'maps';
|
||||||
|
const MAPPING_TABLE = 'mappings';
|
||||||
|
|
||||||
|
private Db $oDb;
|
||||||
|
private $asMaps;
|
||||||
|
|
||||||
|
public function __construct(Db &$oDb) {
|
||||||
|
parent::__construct(__CLASS__);
|
||||||
|
$this->oDb = &$oDb;
|
||||||
|
$this->asMaps = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setMaps() {
|
||||||
|
$asMaps = $this->oDb->selectRows(array('from'=>self::MAP_TABLE));
|
||||||
|
foreach($asMaps as $asMap) $this->asMaps[$asMap['codename']] = $asMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getMaps($sCodeName='') {
|
||||||
|
if(empty($this->asMaps)) $this->setMaps();
|
||||||
|
return ($sCodeName=='')?$this->asMaps:$this->asMaps[$sCodeName];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProjectMaps($iProjectId) {
|
||||||
|
$asMappings = $this->oDb->selectRows(
|
||||||
|
array(
|
||||||
|
'select' => array(Db::getId(self::MAP_TABLE), 'default_map'),
|
||||||
|
'from' => self::MAPPING_TABLE,
|
||||||
|
'constraint'=> array("IFNULL(id_project, {$iProjectId})" => $iProjectId)
|
||||||
|
),
|
||||||
|
Db::getId(self::MAP_TABLE)
|
||||||
|
);
|
||||||
|
|
||||||
|
$asProjectMaps = array();
|
||||||
|
foreach($this->getMaps() as $asMap) {
|
||||||
|
if(array_key_exists($asMap['id_map'], $asMappings)) {
|
||||||
|
$asMap['default_map'] = $asMappings[$asMap['id_map']];
|
||||||
|
$asProjectMaps[] = $asMap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $asProjectMaps;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMapUrl($sCodeName, $asParams) {
|
||||||
|
$asMap = $this->getMaps($sCodeName);
|
||||||
|
$asParams['token'] = $asMap['token'];
|
||||||
|
return self::populateParams($asMap['pattern'], $asParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function populateParams($sUrl, $asParams) {
|
||||||
|
foreach($asParams as $sParam=>$sValue) {
|
||||||
|
$sUrl = str_replace('{'.$sParam.'}', $sValue, $sUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,36 +11,27 @@ class Media extends PhpObject {
|
|||||||
//DB Tables
|
//DB Tables
|
||||||
const MEDIA_TABLE = 'medias';
|
const MEDIA_TABLE = 'medias';
|
||||||
|
|
||||||
//Media folders
|
//Media folders (works because /public/files is a symlink of /files)
|
||||||
const MEDIA_FOLDER = 'files/';
|
const MEDIA_FOLDER = 'files';
|
||||||
const THUMB_FOLDER = self::MEDIA_FOLDER.'thumbs/';
|
const THUMB_FOLDER = self::MEDIA_FOLDER.'/thumbs';
|
||||||
|
|
||||||
const THUMB_MAX_WIDTH = 400;
|
const THUMB_MAX_WIDTH = 400;
|
||||||
|
|
||||||
/**
|
private Db $oDb;
|
||||||
* Database Handle
|
private Project $oProject;
|
||||||
* @var Db
|
|
||||||
*/
|
|
||||||
private $oDb;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Media Project
|
|
||||||
* @var Project
|
|
||||||
*/
|
|
||||||
private $oProject;
|
|
||||||
private $asMedia;
|
private $asMedia;
|
||||||
private $asMedias;
|
private $asMedias;
|
||||||
private $sSystemType;
|
//private $sSystemType;
|
||||||
|
|
||||||
private $iMediaId;
|
private $iMediaId;
|
||||||
|
|
||||||
public function __construct(Db &$oDb, &$oProject, $iMediaId=0) {
|
public function __construct(Db &$oDb, Project &$oProject, $iMediaId=0) {
|
||||||
parent::__construct(__CLASS__);
|
parent::__construct(__CLASS__);
|
||||||
$this->oDb = &$oDb;
|
$this->oDb = &$oDb;
|
||||||
$this->oProject = &$oProject;
|
$this->oProject = &$oProject;
|
||||||
$this->asMedia = array();
|
$this->asMedia = array();
|
||||||
$this->asMedias = array();
|
$this->asMedias = array();
|
||||||
$this->sSystemType = (substr(php_uname(), 0, 7) == "Windows")?'win':'unix';
|
//$this->sSystemType = (substr(php_uname(), 0, 7) == "Windows")?'win':'unix';
|
||||||
$this->setMediaId($iMediaId);
|
$this->setMediaId($iMediaId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,10 +52,10 @@ class Media extends PhpObject {
|
|||||||
$asData = array();
|
$asData = array();
|
||||||
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, array('comment'=>$sComment));
|
||||||
if(!$bResult) $sError = 'error_commit_db';
|
if(!$bResult) $sError = 'error.commit_db';
|
||||||
else $asData = $this->getInfo();
|
else $asData = $this->getInfo();
|
||||||
}
|
}
|
||||||
else $sError = 'media_no_id';
|
else $sError = 'media.no_id';
|
||||||
|
|
||||||
return Spot::getResult(($sError==''), $sError, $asData);
|
return Spot::getResult(($sError==''), $sError, $asData);
|
||||||
}
|
}
|
||||||
@@ -110,11 +101,11 @@ class Media extends PhpObject {
|
|||||||
$sError = '';
|
$sError = '';
|
||||||
$asParams = array();
|
$asParams = array();
|
||||||
if(!$this->isProjectEditable() && $sMethod!='sync') {
|
if(!$this->isProjectEditable() && $sMethod!='sync') {
|
||||||
$sError = 'upload_mode_archived';
|
$sError = '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, array('filename'=>$sMediaName)) && $sMethod!='sync') {
|
||||||
$sError = 'upload_media_exist';
|
$sError = 'upload.media.exists';
|
||||||
$asParams[] = $sMediaName;
|
$asParams[] = $sMediaName;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -140,7 +131,7 @@ class Media extends PhpObject {
|
|||||||
if($sMethod=='sync') $iMediaId = $this->oDb->insertUpdateRow(self::MEDIA_TABLE, $asDbInfo, array('filename'));
|
if($sMethod=='sync') $iMediaId = $this->oDb->insertUpdateRow(self::MEDIA_TABLE, $asDbInfo, array('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) $sError = 'error.commit_db';
|
||||||
else {
|
else {
|
||||||
$this->setMediaId($iMediaId);
|
$this->setMediaId($iMediaId);
|
||||||
$asParams = $this->getInfo(); //Creates thumbnail
|
$asParams = $this->getInfo(); //Creates thumbnail
|
||||||
@@ -176,7 +167,7 @@ class Media extends PhpObject {
|
|||||||
'-print_format json', //output format: json
|
'-print_format json', //output format: json
|
||||||
'-i' //input file
|
'-i' //input file
|
||||||
));
|
));
|
||||||
exec('ffprobe '.$sParams.' "'.$sMediaPath.'"', $asResult);
|
exec('ffprobe '.$sParams.' '.escapeshellarg($sMediaPath), $asResult);
|
||||||
$asExif = json_decode(implode('', $asResult), true);
|
$asExif = json_decode(implode('', $asResult), true);
|
||||||
|
|
||||||
//Taken On
|
//Taken On
|
||||||
@@ -278,10 +269,10 @@ class Media extends PhpObject {
|
|||||||
$sTempPath = self::getMediaPath(uniqid('temp_').'.png');
|
$sTempPath = self::getMediaPath(uniqid('temp_').'.png');
|
||||||
$asResult = array();
|
$asResult = array();
|
||||||
$sParams = implode(' ', array(
|
$sParams = implode(' ', array(
|
||||||
'-i "'.$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
|
||||||
'"'.$sTempPath.'"', //output file
|
escapeshellarg($sTempPath), //output file
|
||||||
));
|
));
|
||||||
exec('ffmpeg '.$sParams, $asResult);
|
exec('ffmpeg '.$sParams, $asResult);
|
||||||
|
|
||||||
@@ -297,15 +288,16 @@ class Media extends PhpObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static function getMediaPath($sMediaName, $sFileType='media') {
|
private static function getMediaPath($sMediaName, $sFileType='media') {
|
||||||
if($sFileType=='thumbnail') return self::THUMB_FOLDER.$sMediaName.(strtolower(substr($sMediaName, -3))=='mov'?'.png':'');
|
if($sFileType=='thumbnail') return self::THUMB_FOLDER.'/'.$sMediaName.(strtolower(substr($sMediaName, -3))=='mov'?'.png':'');
|
||||||
else return self::MEDIA_FOLDER.$sMediaName;
|
else return self::MEDIA_FOLDER.'/'.$sMediaName;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function getMediaType($sMediaName) {
|
private static function getMediaType($sMediaName) {
|
||||||
$sMediaPath = self::getMediaPath($sMediaName);
|
$sMediaPath = self::getMediaPath($sMediaName);
|
||||||
$sMediaMime = mime_content_type($sMediaPath);
|
$sMediaMime = mime_content_type($sMediaPath);
|
||||||
switch($sMediaMime) {
|
switch($sMediaMime) {
|
||||||
case 'video/quicktime': $sType = 'video'; break;
|
case 'video/quicktime':
|
||||||
|
case 'video/mp4': $sType = 'video'; break;
|
||||||
default: $sType = 'image'; break;
|
default: $sType = 'image'; break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,16 +120,20 @@ class Project extends PhpObject {
|
|||||||
|
|
||||||
public function getProjects($iProjectId=0) {
|
public function getProjects($iProjectId=0) {
|
||||||
$bSpecificProj = ($iProjectId > 0);
|
$bSpecificProj = ($iProjectId > 0);
|
||||||
|
$sDefaultProjectCodeName = $this->getProjectCodeName();
|
||||||
$asInfo = array(
|
$asInfo = array(
|
||||||
'select'=> array(
|
'select'=> array(
|
||||||
Db::getId(self::PROJ_TABLE)." AS id",
|
Db::getId(self::PROJ_TABLE)." AS id",
|
||||||
'codename',
|
'codename',
|
||||||
'name',
|
'name',
|
||||||
|
'latitude',
|
||||||
|
'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')
|
||||||
);
|
);
|
||||||
if($bSpecificProj) $asInfo['constraint'] = array(Db::getId(self::PROJ_TABLE)=>$iProjectId);
|
if($bSpecificProj) $asInfo['constraint'] = array(Db::getId(self::PROJ_TABLE)=>$iProjectId);
|
||||||
|
|
||||||
@@ -141,16 +145,22 @@ class Project extends PhpObject {
|
|||||||
case 2: $asProject['mode'] = self::MODE_HISTO; break;
|
case 2: $asProject['mode'] = self::MODE_HISTO; break;
|
||||||
}
|
}
|
||||||
$asProject['editable'] = $this->isModeEditable($asProject['mode']);
|
$asProject['editable'] = $this->isModeEditable($asProject['mode']);
|
||||||
|
$asProject['gpxfilepath'] = Spot::addTimestampToFilePath(Gpx::getFrontendFilePath($sCodeName));
|
||||||
if($sCodeName != '' && !Converter::isGeoJsonValid($sCodeName)) Converter::convertToGeoJson($sCodeName);
|
|
||||||
|
|
||||||
$asProject['geofilepath'] = Spot::addTimestampToFilePath(Geo::getFilePath($sCodeName, GeoJson::EXT));
|
|
||||||
$asProject['gpxfilepath'] = Spot::addTimestampToFilePath(Geo::getFilePath($sCodeName, Gpx::EXT));
|
|
||||||
$asProject['codename'] = $sCodeName;
|
$asProject['codename'] = $sCodeName;
|
||||||
|
$asProject['default'] = ($sCodeName == $sDefaultProjectCodeName);
|
||||||
}
|
}
|
||||||
return $bSpecificProj?$asProject:$asProjects;
|
return $bSpecificProj?$asProject:$asProjects;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getGeoJson() {
|
||||||
|
if($this->sCodeName != '' && !Converter::isGeoJsonValid($this->sCodeName)){
|
||||||
|
$aiCenter = Converter::convertToGeoJson($this->sCodeName)['center'];
|
||||||
|
$this->oDb->updateRow(self::PROJ_TABLE, $this->iProjectId, ['latitude' => $aiCenter[1], 'longitude' => $aiCenter[0]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_decode(file_get_contents(GeoJson::getBackendFilePath($this->sCodeName)), true);
|
||||||
|
}
|
||||||
|
|
||||||
public function getProject() {
|
public function getProject() {
|
||||||
return $this->getProjects($this->getProjectId());
|
return $this->getProjects($this->getProjectId());
|
||||||
}
|
}
|
||||||
@@ -185,7 +195,7 @@ class Project extends PhpObject {
|
|||||||
$this->sCodeName = $asProject['codename'];
|
$this->sCodeName = $asProject['codename'];
|
||||||
$this->sMode = $asProject['mode'];
|
$this->sMode = $asProject['mode'];
|
||||||
$this->asActive = array('from'=>$asProject['active_from'], 'to'=>$asProject['active_to']);
|
$this->asActive = array('from'=>$asProject['active_from'], 'to'=>$asProject['active_to']);
|
||||||
$this->asGeo = array('geofile'=>$asProject['geofilepath'], 'gpxfile'=>$asProject['gpxfilepath']);
|
$this->asGeo = array(/*'geofile'=>$asProject['geofilepath'], */'gpxfile'=>$asProject['gpxfilepath']);
|
||||||
}
|
}
|
||||||
else $this->addError('Error while setting project: no project ID');
|
else $this->addError('Error while setting project: no project ID');
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,8 @@ use \Settings;
|
|||||||
* - Posts (table `posts`):
|
* - Posts (table `posts`):
|
||||||
* - site_time: timestamp in Site Time
|
* - site_time: timestamp in Site Time
|
||||||
* - timezone: Local Timezone
|
* - timezone: Local Timezone
|
||||||
|
* - Users (table `users`):
|
||||||
|
* - timezone: Site Timezone (stored user's timezone for emails)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class Spot extends Main
|
class Spot extends Main
|
||||||
@@ -39,6 +41,9 @@ class Spot extends Main
|
|||||||
const MAIL_CHUNK_SIZE = 5;
|
const MAIL_CHUNK_SIZE = 5;
|
||||||
|
|
||||||
const DEFAULT_LANG = 'en';
|
const DEFAULT_LANG = 'en';
|
||||||
|
const PROJECT_NAME = 'LiveTrail';
|
||||||
|
|
||||||
|
const MAIN_PAGE = 'index';
|
||||||
|
|
||||||
private Project $oProject;
|
private Project $oProject;
|
||||||
private Media $oMedia;
|
private Media $oMedia;
|
||||||
@@ -108,8 +113,8 @@ class Spot extends Main
|
|||||||
'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(7,5)",
|
'latitude' => "DECIMAL(8,6)",
|
||||||
'longitude' => "DECIMAL(8,5)",
|
'longitude' => "DECIMAL(9,6)",
|
||||||
'altitude' => "SMALLINT",
|
'altitude' => "SMALLINT",
|
||||||
'model' => "VARCHAR(20)",
|
'model' => "VARCHAR(20)",
|
||||||
'name' => "VARCHAR(100)",
|
'name' => "VARCHAR(100)",
|
||||||
@@ -146,7 +151,8 @@ class Spot extends Main
|
|||||||
Project::PROJ_TABLE => "UNIQUE KEY `uni_proj_name` (`codename`)",
|
Project::PROJ_TABLE => "UNIQUE KEY `uni_proj_name` (`codename`)",
|
||||||
Media::MEDIA_TABLE => "UNIQUE KEY `uni_file_name` (`filename`)",
|
Media::MEDIA_TABLE => "UNIQUE KEY `uni_file_name` (`filename`)",
|
||||||
User::USER_TABLE => "UNIQUE KEY `uni_email` (`email`)",
|
User::USER_TABLE => "UNIQUE KEY `uni_email` (`email`)",
|
||||||
Map::MAP_TABLE => "UNIQUE KEY `uni_map_name` (`codename`)"
|
Map::MAP_TABLE => "UNIQUE KEY `uni_map_name` (`codename`)",
|
||||||
|
Map::MAPPING_TABLE => "default_on_generic_map_only CHECK (`default_map` = 0 OR `id_project` IS NULL)"
|
||||||
),
|
),
|
||||||
'cascading_delete' => array
|
'cascading_delete' => array
|
||||||
(
|
(
|
||||||
@@ -158,42 +164,40 @@ class Spot extends Main
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAppMainPage()
|
public function getAppMainPage(string $sCsrfToken='') {
|
||||||
{
|
|
||||||
//Cache Page List
|
|
||||||
$asPages = array_diff($this->asMasks, array('email_update', 'email_conf'));
|
|
||||||
if(!$this->oUser->checkUserClearance(User::CLEARANCE_ADMIN)) {
|
|
||||||
$asPages = array_diff($asPages, array('admin', 'upload'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return parent::getMainPage(
|
return parent::getMainPage(
|
||||||
array(
|
array(
|
||||||
'vars' => array(
|
|
||||||
'chunk_size' => self::FEED_CHUNK_SIZE,
|
|
||||||
'default_project_codename' => $this->oProject->getProjectCodeName(),
|
|
||||||
'projects' => $this->oProject->getProjects(),
|
'projects' => $this->oProject->getProjects(),
|
||||||
'user' => $this->oUser->getUserInfo()
|
'user' => $this->oUser->getUserInfo(),
|
||||||
),
|
|
||||||
'consts' => array(
|
'consts' => array(
|
||||||
'server' => $this->asContext['serv_name'],
|
|
||||||
'modes' => Project::MODES,
|
'modes' => Project::MODES,
|
||||||
'clearances' => User::CLEARANCES,
|
'clearances' => User::CLEARANCES,
|
||||||
'default_timezone' => Settings::TIMEZONE
|
'default_timezone' => Settings::TIMEZONE,
|
||||||
|
'default_maps' => $this->oMap->getProjectMaps(-1),
|
||||||
|
'chunk_size' => self::FEED_CHUNK_SIZE,
|
||||||
|
'hash_sep' => '-',
|
||||||
|
'title' => self::PROJECT_NAME,
|
||||||
|
'default_page' => 'project',
|
||||||
|
'csrf_token' => $sCsrfToken
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
'index',
|
self::MAIN_PAGE,
|
||||||
array(
|
array(
|
||||||
|
'tags' => [
|
||||||
'language' => $this->oLang->getLanguage(),
|
'language' => $this->oLang->getLanguage(),
|
||||||
'host_url' => $this->asContext['serv_name'],
|
'title' => self::PROJECT_NAME,
|
||||||
'filepath_css' => self::addTimestampToFilePath('style/spot.css'),
|
],
|
||||||
'filepath_js_d3' => self::addTimestampToFilePath('script/d3.min.js'),
|
'instances' => [
|
||||||
'filepath_js_leaflet' => self::addTimestampToFilePath('script/leaflet.min.js'),
|
'entrypoint' => $this->getAppEntryPoints()
|
||||||
'filepath_js_jquery' => self::addTimestampToFilePath('script/jquery.min.js'),
|
]
|
||||||
'filepath_js_jquery_mods' => self::addTimestampToFilePath('script/jquery.mods.js'),
|
)
|
||||||
'filepath_js_spot' => self::addTimestampToFilePath('script/spot.js'),
|
);
|
||||||
'filepath_js_lightbox' => self::addTimestampToFilePath('script/lightbox.js')
|
}
|
||||||
),
|
|
||||||
$asPages
|
private function getAppEntryPoints() {
|
||||||
|
return array_map(
|
||||||
|
function($sFileName) {return ['filename' => self::addTimestampToFilePath($sFileName)];},
|
||||||
|
json_decode(file_get_contents('assets/entrypoints.json'), true)['entrypoints']['app']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,6 +211,10 @@ class Spot extends Main
|
|||||||
$this->oProject->setProjectId($iProjectId);
|
$this->oProject->setProjectId($iProjectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getProjectGeoJson() {
|
||||||
|
return self::getJsonResult(true, '', $this->oProject->getGeoJson());
|
||||||
|
}
|
||||||
|
|
||||||
public function updateProject() {
|
public function updateProject() {
|
||||||
$bNewMsg = false;
|
$bNewMsg = false;
|
||||||
$bSuccess = true;
|
$bSuccess = true;
|
||||||
@@ -221,11 +229,21 @@ class Spot extends Main
|
|||||||
|
|
||||||
//Send Update Email
|
//Send Update Email
|
||||||
if($bNewMsg) {
|
if($bNewMsg) {
|
||||||
$oEmail = new Email($this->asContext['serv_name'], 'email_update');
|
$bSuccess = $this->sendEmail();
|
||||||
|
$sDesc = $bSuccess?'mail_sent':'mail_failure';
|
||||||
|
}
|
||||||
|
else $sDesc = 'no_new_msg';
|
||||||
|
|
||||||
|
return self::getJsonResult($bSuccess, $sDesc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sendEmail() {
|
||||||
|
$oEmail = new Email($this->asContext['serv_name'], 'email.update');
|
||||||
$oEmail->setDestInfo($this->oUser->getActiveUsersInfo());
|
$oEmail->setDestInfo($this->oUser->getActiveUsersInfo());
|
||||||
|
|
||||||
//Add Position
|
//Add Position
|
||||||
$asLastMessage = array_shift($this->getSpotMessages(array($this->oProject->getLastMessageId($this->getFeedConstraints(Feed::MSG_TABLE)))));
|
$asSpotMessages = $this->getSpotMessages(array($this->oProject->getLastMessageId($this->getFeedConstraints(Feed::MSG_TABLE))));
|
||||||
|
$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');
|
||||||
|
|
||||||
@@ -248,42 +266,42 @@ class Spot extends Main
|
|||||||
if($iPostCount == self::MAIL_CHUNK_SIZE) break;
|
if($iPostCount == self::MAIL_CHUNK_SIZE) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
$bSuccess = $oEmail->send();
|
return $oEmail->send();
|
||||||
$sDesc = $bSuccess?'mail_sent':'mail_failure';
|
|
||||||
}
|
|
||||||
else $sDesc = 'no_new_msg';
|
|
||||||
|
|
||||||
return self::getJsonResult($bSuccess, $sDesc);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function genCronFile() {
|
|
||||||
//$bSuccess = (file_put_contents('spot_cron.sh', '#!/bin/bash'."\n".'cd '.dirname($_SERVER['SCRIPT_FILENAME'])."\n".'php -f index.php a=update_feed')!==false);
|
|
||||||
$sFileName = 'spot_cron.sh';
|
|
||||||
$sContent =
|
|
||||||
'#!/bin/bash'."\n".
|
|
||||||
'wget -qO- '.$this->asContext['serv_name'].'index.php?a=update_project > /dev/null'."\n".
|
|
||||||
'#Crontab job: 0 * * * * . '.dirname($_SERVER['SCRIPT_FILENAME']).'/'.$sFileName.' > /dev/null'."\n";
|
|
||||||
$bSuccess = (file_put_contents($sFileName, $sContent)!==false);
|
|
||||||
return self::getJsonResult($bSuccess, '');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getMarkers($asMessageIds=array(), $asMediaIds=array(), $bInternal=false)
|
public function getMarkers($asMessageIds=array(), $asMediaIds=array(), $bInternal=false)
|
||||||
{
|
{
|
||||||
|
//Get messages
|
||||||
$asMessages = $this->getSpotMessages($asMessageIds);
|
$asMessages = $this->getSpotMessages($asMessageIds);
|
||||||
$asGeoMedias = array();
|
foreach($asMessages as &$asMessage) {
|
||||||
usort($asMessages, function($a, $b){return $a['unix_time'] > $b['unix_time'];});
|
$asMessage['id'] = $asMessage[Db::getId(Feed::MSG_TABLE)];
|
||||||
$bHasMsg = !empty($asMessages);
|
$asMessage['type'] = 'message';
|
||||||
|
$asMessage['subtype'] = 'message';
|
||||||
|
}
|
||||||
|
|
||||||
//Add medias
|
//Get Geo-positioned Medias
|
||||||
|
//FIXME Make more efficient than requesting images twice from DB
|
||||||
$asMedias = $this->getMedias('taken_on', $asMediaIds);
|
$asMedias = $this->getMedias('taken_on', $asMediaIds);
|
||||||
usort($asMedias, function($a, $b){return $a['unix_time'] > $b['unix_time'];});
|
$asGeoMedias = $this->getMedias('posted_on', $asMediaIds, true);
|
||||||
|
foreach($asGeoMedias as &$asGeoMedia) {
|
||||||
|
$iId = $asGeoMedia[Db::getId(Media::MEDIA_TABLE)];
|
||||||
|
unset($asGeoMedia[Db::getId(Media::MEDIA_TABLE)]);
|
||||||
|
|
||||||
|
$asGeoMedia['id'] = $iId;
|
||||||
|
$asGeoMedia['type'] = 'media';
|
||||||
|
$asGeoMedia['medias'] = array_values(array_filter($asMedias, function($asMedia) use ($iId) {
|
||||||
|
return $asMedia['id_media'] == $iId;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
//Assign medias to closest message
|
//Assign medias to closest message
|
||||||
|
if(!empty($asMessages)) {
|
||||||
|
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'];});
|
||||||
|
|
||||||
$iIndex = 0;
|
$iIndex = 0;
|
||||||
$iMaxIndex = count($asMessages) - 1;
|
$iMaxIndex = count($asMessages) - 1;
|
||||||
foreach($asMedias as $asMedia) {
|
foreach($asMedias as $asMedia) {
|
||||||
if($asMedia['latitude']!='' && $asMedia['longitude']!='') $asGeoMedias[] = $asMedia;
|
|
||||||
elseif($bHasMsg) {
|
|
||||||
while($iIndex <= $iMaxIndex && $asMedia['unix_time'] > $asMessages[$iIndex]['unix_time']) $iIndex++;
|
while($iIndex <= $iMaxIndex && $asMedia['unix_time'] > $asMessages[$iIndex]['unix_time']) $iIndex++;
|
||||||
|
|
||||||
//All medias before first message or after last message are assigned to first/last message respectively
|
//All medias before first message or after last message are assigned to first/last message respectively
|
||||||
@@ -298,27 +316,31 @@ class Spot extends Main
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Spot Last Update
|
//Combine markers
|
||||||
$asLastUpdate = array();
|
$asMarkers = [...$asMessages, ...$asGeoMedias];
|
||||||
$this->addTimeStamp($asLastUpdate, $this->oProject->getLastUpdate());
|
usort($asMarkers, function($a, $b){return (int) $a['unix_time'] <=> (int) $b['unix_time'];});
|
||||||
|
|
||||||
$asResult = array(
|
$asResult = array(
|
||||||
'messages' => $asMessages,
|
'markers' => $asMarkers,
|
||||||
'medias' => $asGeoMedias,
|
'maps' => $this->oMap->getProjectMaps($this->oProject->getProjectId())
|
||||||
'maps' => $this->oMap->getProjectMaps($this->oProject->getProjectId()),
|
|
||||||
'last_update' => $asLastUpdate
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return $bInternal?$asResult:self::getJsonResult(true, '', $asResult);
|
return $bInternal?$asResult:self::getJsonResult(true, '', $asResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getLastUpdate() {
|
||||||
|
$asLastUpdate = array();
|
||||||
|
$this->addTimeStamp($asLastUpdate, $this->oProject->getLastUpdate());
|
||||||
|
return self::getJsonResult(true, '', $asLastUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
public function subscribe($sEmail, $sNickName) {
|
public function subscribe($sEmail, $sNickName) {
|
||||||
$asResult = $this->oUser->addUser($sEmail, $this->oLang->getLanguage(), date_default_timezone_get(), $sNickName);
|
$asResult = $this->oUser->addUser($sEmail, $this->oLang->getLanguage(), date_default_timezone_get(), $sNickName);
|
||||||
$asUserInfo = $this->oUser->getUserInfo();
|
$asUserInfo = $this->oUser->getUserInfo();
|
||||||
|
|
||||||
//Send Confirmation Email
|
//Send Confirmation Email
|
||||||
if($asResult['result'] && $asResult['desc']=='lang:nl_subscribed') {
|
if($asResult['result'] && $asResult['desc']=='lang:newsletter.subscribed' && !Settings::DEBUG) {
|
||||||
$oConfEmail = new Email($this->asContext['serv_name'], 'email_conf');
|
$oConfEmail = new Email($this->asContext['serv_name'], 'email.confirmation');
|
||||||
$oConfEmail->setDestInfo($asUserInfo);
|
$oConfEmail->setDestInfo($asUserInfo);
|
||||||
$oConfEmail->send();
|
$oConfEmail->send();
|
||||||
}
|
}
|
||||||
@@ -328,7 +350,7 @@ class Spot extends Main
|
|||||||
|
|
||||||
public function unsubscribe() {
|
public function unsubscribe() {
|
||||||
$asResult = $this->oUser->removeUser();
|
$asResult = $this->oUser->removeUser();
|
||||||
return self::getJsonResult($asResult['result'], $asResult['desc'], $asResult['data']);
|
return self::getJsonResult($asResult['result'], $asResult['desc'], User::DEFAULT_USER);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function unsubscribeFromEmail($iUserId) {
|
public function unsubscribeFromEmail($iUserId) {
|
||||||
@@ -380,26 +402,36 @@ class Spot 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())
|
private function getMedias($sTimeRefField, $asMediaIds=array(), $bOnlyGeoMedia=false)
|
||||||
{
|
{
|
||||||
//Constraints
|
//Constraints
|
||||||
$asConstraints = $this->getFeedConstraints(Media::MEDIA_TABLE, $sTimeRefField);
|
$asConstraints = $this->getFeedConstraints(Media::MEDIA_TABLE, $sTimeRefField);
|
||||||
if(!empty($asMediaIds)) {
|
if(!empty($asMediaIds)) {
|
||||||
$asConstraints['constraint'][Db::getId(Media::MEDIA_TABLE)] = $asMediaIds;
|
|
||||||
$asConstraints['constOpe'][Db::getId(Media::MEDIA_TABLE)] = 'IN';
|
$asConstraints['constOpe'][Db::getId(Media::MEDIA_TABLE)] = 'IN';
|
||||||
|
$asConstraints['constraint'][Db::getId(Media::MEDIA_TABLE)] = $asMediaIds;
|
||||||
|
}
|
||||||
|
if($bOnlyGeoMedia) {
|
||||||
|
$asConstraints['constOpe']['latitude'] = ' IS NOT ';
|
||||||
|
$asConstraints['constraint']['latitude'] = 'NULL';
|
||||||
|
$asConstraints['constOpe']['longitude'] = ' IS NOT ';
|
||||||
|
$asConstraints['constraint']['longitude'] = 'NULL';
|
||||||
}
|
}
|
||||||
|
|
||||||
$asMedias = $this->oMedia->getMediasInfo($asConstraints);
|
$asMedias = $this->oMedia->getMediasInfo($asConstraints);
|
||||||
foreach($asMedias as &$asMedia) {
|
foreach($asMedias as &$asMedia) {
|
||||||
$iTimeStampTakenOn = strtotime($asMedia['taken_on']);
|
|
||||||
$iTimeStampPostedOn = strtotime($asMedia['posted_on']);
|
|
||||||
$asMedia['taken_on_formatted'] = $this->getTimeFormat($iTimeStampTakenOn);
|
|
||||||
$asMedia['taken_on_formatted_local'] = $this->getTimeFormat($iTimeStampTakenOn, $asMedia['timezone']);
|
|
||||||
$asMedia['posted_on_formatted'] = $this->getTimeFormat($iTimeStampPostedOn);
|
|
||||||
$asMedia['posted_on_formatted_local'] = $this->getTimeFormat($iTimeStampPostedOn, $asMedia['timezone']);
|
|
||||||
$asMedia['displayed_id'] = $asMedia[Db::getId(Media::MEDIA_TABLE)];
|
$asMedia['displayed_id'] = $asMedia[Db::getId(Media::MEDIA_TABLE)];
|
||||||
|
|
||||||
$this->addTimeStamp($asMedia, strtotime($asMedia[$sTimeRefField]), $asMedia['timezone']);
|
$this->addTimeStamp($asMedia, strtotime($asMedia[$sTimeRefField]), $asMedia['timezone']);
|
||||||
|
$this->addTimeStamp($asMedia, strtotime($asMedia['taken_on']), $asMedia['timezone'], 'taken_on');
|
||||||
|
$this->addTimeStamp($asMedia, strtotime($asMedia['posted_on']), $asMedia['timezone'], 'posted_on');
|
||||||
|
|
||||||
|
if($asMedia['latitude'] != '' && $asMedia['longitude'] != '') {
|
||||||
|
$asMedia['lat_dms'] = self::decToDms($asMedia['latitude'], 'lat');
|
||||||
|
$asMedia['lon_dms'] = self::decToDms($asMedia['longitude'], 'lon');
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($asMedia['taken_on']);
|
||||||
|
unset($asMedia['posted_on']);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $asMedias;
|
return $asMedias;
|
||||||
@@ -431,14 +463,16 @@ class Spot extends Main
|
|||||||
return $asPosts;
|
return $asPosts;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function addTimeStamp(&$asData, $iTime, $sTimeZone='') {
|
private function addTimeStamp(&$asData, $iTime, $sTimeZone='', $sPrefix='') {
|
||||||
$asData['unix_time'] = (int) $iTime;
|
if($sPrefix != '') $sPrefix = $sPrefix.'_';
|
||||||
$asData['relative_time'] = Toolbox::getDateTimeDesc($iTime, $this->oLang->getLanguage());
|
|
||||||
$asData['formatted_time'] = $this->getTimeFormat($iTime);
|
$asData[$sPrefix.'unix_time'] = (int) $iTime;
|
||||||
|
$asData[$sPrefix.'relative_time'] = Toolbox::getDateTimeDesc($iTime, $this->oLang->getLanguage());
|
||||||
|
$asData[$sPrefix.'formatted_time'] = $this->getTimeFormat($iTime);
|
||||||
|
|
||||||
if($sTimeZone != '') {
|
if($sTimeZone != '') {
|
||||||
$asData['formatted_time_local'] = $this->getTimeFormat($iTime, $sTimeZone);
|
$asData[$sPrefix.'formatted_time_local'] = $this->getTimeFormat($iTime, $sTimeZone);
|
||||||
$asData['day_offset'] = self::getTimeZoneDayOffset($iTime, $sTimeZone);
|
$asData[$sPrefix.'day_offset'] = self::getTimeZoneDayOffset($iTime, $sTimeZone);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,7 +541,7 @@ class Spot extends Main
|
|||||||
|
|
||||||
$asResult = array_merge($asResult, $asMarkers);
|
$asResult = array_merge($asResult, $asMarkers);
|
||||||
}
|
}
|
||||||
else $sDesc = 'mode_histo';
|
else $sDesc = 'project.modes.histo';
|
||||||
|
|
||||||
return self::getJsonResult(true, $sDesc, $asResult);
|
return self::getJsonResult(true, $sDesc, $asResult);
|
||||||
}
|
}
|
||||||
@@ -525,12 +559,11 @@ class Spot extends Main
|
|||||||
return $bInternal?$asResult['feed']:self::getJsonResult(true, '', $asResult);
|
return $bInternal?$asResult['feed']:self::getJsonResult(true, '', $asResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFeed($iRefId=0, $sDirection, $sSort) {
|
private function getFeed($iRefId, $sDirection, $sSort) {
|
||||||
$this->oDb->cleanSql($iRefId);
|
$sRefId = is_scalar($iRefId) && preg_match('/^\d+(?:\.\d+)?$/D', (string) $iRefId) ? (string) $iRefId : '0';
|
||||||
$this->oDb->cleanSql($sDirection);
|
$sDirection = ($sDirection === '>')?'>':'<';
|
||||||
$this->oDb->cleanSql($sSort);
|
$sSort = ($sSort === 'ASC')?'ASC':'DESC';
|
||||||
|
|
||||||
$sMediaRefField = 'posted_on';
|
|
||||||
$sProjectIdField = Db::getId(Project::PROJ_TABLE);
|
$sProjectIdField = Db::getId(Project::PROJ_TABLE);
|
||||||
$sMsgIdField = Db::getId(Feed::MSG_TABLE);
|
$sMsgIdField = Db::getId(Feed::MSG_TABLE);
|
||||||
$sMediaIdField = Db::getId(Media::MEDIA_TABLE);
|
$sMediaIdField = Db::getId(Media::MEDIA_TABLE);
|
||||||
@@ -544,15 +577,15 @@ class Spot extends Main
|
|||||||
"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({$sMediaRefField}), '.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, $sMediaRefField, '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",
|
||||||
($iRefId > 0)?("WHERE ref ".$sDirection." ".$iRefId):"",
|
($sRefId !== '0')?("WHERE ref ".$sDirection." ".$sRefId):"",
|
||||||
"ORDER BY ref ".$sSort,
|
"ORDER BY ref ".$sSort,
|
||||||
"LIMIT ".self::FEED_CHUNK_SIZE
|
"LIMIT ".self::FEED_CHUNK_SIZE
|
||||||
));
|
));
|
||||||
@@ -568,17 +601,18 @@ class Spot extends Main
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Sort Table IDs by type & Get attributes
|
//Sort Table IDs by type & Get attributes
|
||||||
$asFeedIds = array('message'=>array(), 'media'=>array(), 'message'=>array());
|
$asFeedIds = array('message'=>array(), 'media'=>array(), 'post'=>array());
|
||||||
foreach($asItems as $asItem) {
|
foreach($asItems as $asItem) {
|
||||||
$asFeedIds[$asItem['type']][$asItem['id']] = $asItem;
|
$asFeedIds[$asItem['type']][$asItem['id']] = $asItem;
|
||||||
}
|
}
|
||||||
$asFeedAttrs = array(
|
$asFeedAttrs = array(
|
||||||
'message' => empty($asFeedIds['message'])?array():$this->getSpotMessages(array_keys($asFeedIds['message'])),
|
'message' => empty($asFeedIds['message'])?array():$this->getSpotMessages(array_keys($asFeedIds['message'])),
|
||||||
'media' => empty($asFeedIds['media'])?array():$this->getMedias($sMediaRefField, array_keys($asFeedIds['media'])),
|
'media' => empty($asFeedIds['media'])?array():$this->getMedias('posted_on', array_keys($asFeedIds['media'])),
|
||||||
'post' => empty($asFeedIds['post'])?array():$this->getPosts(array_keys($asFeedIds['post']))
|
'post' => empty($asFeedIds['post'])?array():$this->getPosts(array_keys($asFeedIds['post']))
|
||||||
);
|
);
|
||||||
|
|
||||||
//Replace Array Key with Item ID
|
//Replace Array Key with Item ID
|
||||||
|
$asFeeds = array();
|
||||||
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;
|
||||||
@@ -612,7 +646,7 @@ class Spot extends Main
|
|||||||
|
|
||||||
$this->oUser->updateNickname($sName);
|
$this->oUser->updateNickname($sName);
|
||||||
}
|
}
|
||||||
else $sDesc = 'mode_histo';
|
else $sDesc = 'project.modes.histo';
|
||||||
|
|
||||||
return self::getJsonResult(($iPostId > 0), $sDesc);
|
return self::getJsonResult(($iPostId > 0), $sDesc);
|
||||||
}
|
}
|
||||||
@@ -632,9 +666,15 @@ class Spot extends Main
|
|||||||
|
|
||||||
public function addPosition($sLat, $sLng, $iTimestamp) {
|
public function addPosition($sLat, $sLng, $iTimestamp) {
|
||||||
$oFeed = new Feed($this->oDb, $this->oProject->getFeedIds()[0]);
|
$oFeed = new Feed($this->oDb, $this->oProject->getFeedIds()[0]);
|
||||||
$bResult = ($oFeed->addManualPosition($sLat, $sLng, $iTimestamp) > 0);
|
$bSuccess = ($oFeed->addManualPosition($sLat, $sLng, $iTimestamp) > 0);
|
||||||
|
|
||||||
return self::getJsonResult($bResult, $bResult?'':$this->oDb->getLastError());
|
if($bSuccess) {
|
||||||
|
$bSuccess = $this->sendEmail();
|
||||||
|
$sDesc = $bSuccess?'mail_sent':'mail_failure';
|
||||||
|
}
|
||||||
|
else $sDesc = 'error.commit_db';
|
||||||
|
|
||||||
|
return self::getJsonResult($bSuccess, $sDesc);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAdminSettings($sType='') {
|
public function getAdminSettings($sType='') {
|
||||||
@@ -659,6 +699,8 @@ class Spot extends Main
|
|||||||
$sDesc = '';
|
$sDesc = '';
|
||||||
$asResult = array();
|
$asResult = array();
|
||||||
|
|
||||||
|
if($this->oDb->isId($sField) && $sValue <= 0) return self::getJsonResult(false, $this->oLang->getTranslation('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);
|
||||||
@@ -676,7 +718,7 @@ class Spot 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('unknown_field', $sField);
|
$sDesc = $this->oLang->getTranslation('error.unknown_field', $sField);
|
||||||
}
|
}
|
||||||
$asResult = $oProject->getProject();
|
$asResult = $oProject->getProject();
|
||||||
$asResult['active_from'] = substr($asResult['active_from'], 0, 10);
|
$asResult['active_from'] = substr($asResult['active_from'], 0, 10);
|
||||||
@@ -695,7 +737,7 @@ class Spot extends Main
|
|||||||
$bSuccess = $oFeed->setProjectId($sValue);
|
$bSuccess = $oFeed->setProjectId($sValue);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
$sDesc = $this->oLang->getTranslation('unknown_field', $sField);
|
$sDesc = $this->oLang->getTranslation('error.unknown_field', $sField);
|
||||||
}
|
}
|
||||||
$asResult = $oFeed->getFeed();
|
$asResult = $oFeed->getFeed();
|
||||||
break;
|
break;
|
||||||
@@ -707,52 +749,78 @@ class Spot extends Main
|
|||||||
$sDesc = $asReturnCode['desc'];
|
$sDesc = $asReturnCode['desc'];
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
$sDesc = $this->oLang->getTranslation('unknown_field', $sField);
|
$sDesc = $this->oLang->getTranslation('error.unknown_field', $sField);
|
||||||
}
|
}
|
||||||
$asResult = $this->oUser->getActiveUserInfo($iId);
|
$asResult = $this->oUser->getActiveUserInfo($iId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if(!$bSuccess && $sDesc=='') $sDesc = Mask::LANG_PREFIX.'error_commit_db';
|
if(!$bSuccess && $sDesc=='') $sDesc = Mask::LANG_PREFIX.'error.commit_db';
|
||||||
|
|
||||||
return self::getJsonResult($bSuccess, $sDesc, array($sType=>array($asResult)));
|
return self::getJsonResult($bSuccess, $sDesc, array($sType=>array($asResult)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delAdminSettings($sType, $iId) {
|
public function createAdminSettings($sType) {
|
||||||
$bSuccess = false;
|
$bSuccess = false;
|
||||||
$sDesc = '';
|
$sDesc = '';
|
||||||
|
$asResult = array();
|
||||||
|
|
||||||
switch($sType) {
|
switch($sType) {
|
||||||
case 'project':
|
case 'project':
|
||||||
$oProject = new Project($this->oDb, $iId);
|
|
||||||
$asResult = $oProject->delete();
|
|
||||||
$sDesc = $asResult['project'][0]['desc'];
|
|
||||||
break;
|
|
||||||
case 'feed':
|
|
||||||
$oFeed = new Feed($this->oDb, $iId);
|
|
||||||
$asResult = array('feed'=>array($oFeed->delete()));
|
|
||||||
$sDesc = $asResult['feed'][0]['desc'];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
$bSuccess = ($sDesc=='');
|
|
||||||
|
|
||||||
return self::getJsonResult($bSuccess, $sDesc, $asResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function createProject() {
|
|
||||||
$oProject = new Project($this->oDb);
|
$oProject = new Project($this->oDb);
|
||||||
$iNewProjectId = $oProject->createProjectId();
|
$iNewProjectId = $oProject->createProjectId();
|
||||||
|
|
||||||
$oFeed = new Feed($this->oDb);
|
$oFeed = new Feed($this->oDb);
|
||||||
$oFeed->createFeedId($iNewProjectId);
|
$oFeed->createFeedId($iNewProjectId);
|
||||||
|
|
||||||
return self::getJsonResult($iNewProjectId>0, '', array(
|
$bSuccess = $iNewProjectId > 0;
|
||||||
|
$asResult = array(
|
||||||
'project' => array($oProject->getProject()),
|
'project' => array($oProject->getProject()),
|
||||||
'feed' => array($oFeed->getFeed())
|
'feed' => array($oFeed->getFeed())
|
||||||
));
|
);
|
||||||
|
break;
|
||||||
|
case 'feed':
|
||||||
|
$oFeed = new Feed($this->oDb);
|
||||||
|
$iNewFeedId = $oFeed->createFeedId($this->oProject->getProjectId());
|
||||||
|
$bSuccess = $iNewFeedId > 0;
|
||||||
|
$asResult = array(
|
||||||
|
'feed' => array($oFeed->getFeed())
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::getJsonResult($bSuccess, $sDesc, $asResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteAdminSettings($sType, $iId) {
|
||||||
|
$bSuccess = false;
|
||||||
|
$sDesc = '';
|
||||||
|
$asResult = array();
|
||||||
|
|
||||||
|
switch($sType) {
|
||||||
|
case 'project':
|
||||||
|
$oProject = new Project($this->oDb, $iId);
|
||||||
|
$asResult = $oProject->delete();
|
||||||
|
$sDesc = $asResult['project'][0]['desc'];
|
||||||
|
$bSuccess = $asResult['project'][0]['del'];
|
||||||
|
break;
|
||||||
|
case 'feed':
|
||||||
|
$oFeed = new Feed($this->oDb, $iId);
|
||||||
|
$asResult = array('feed' => array($oFeed->delete()));
|
||||||
|
$sDesc = $asResult['feed'][0]['desc'];
|
||||||
|
$bSuccess = $asResult['feed'][0]['del'];
|
||||||
|
break;
|
||||||
|
case 'user':
|
||||||
|
$asResult = array('user' => array($this->oUser->removeUser($iId)));
|
||||||
|
$sDesc = $asResult['user'][0]['desc'];
|
||||||
|
$bSuccess = $asResult['user'][0]['result'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::getJsonResult($bSuccess, $sDesc, $asResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function buildGeoJSON($sCodeName) {
|
public function buildGeoJSON($sCodeName) {
|
||||||
return Converter::convertToGeoJson($sCodeName);
|
return Converter::convertToGeoJson($sCodeName)['logs'];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function decToDms($dValue, $sType) {
|
public static function decToDms($dValue, $sType) {
|
||||||
@@ -794,7 +862,7 @@ class Spot 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('date_time', array($sDate, $sTime));
|
return $this->oLang->getTranslation('time.date_time', array($sDate, $sTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getTimeZoneDayOffset($iTime, $sLocalTimeZone) {
|
public static function getTimeZoneDayOffset($iTime, $sLocalTimeZone) {
|
||||||
@@ -802,7 +870,7 @@ class Spot extends Main
|
|||||||
$iLocalDate = (int) (new \DateTime('@'.$iTime))->setTimezone(new \DateTimeZone($sLocalTimeZone))->format('Ymd');
|
$iLocalDate = (int) (new \DateTime('@'.$iTime))->setTimezone(new \DateTimeZone($sLocalTimeZone))->format('Ymd');
|
||||||
$iSiteDate = (int) (new \DateTime('@'.$iTime))->setTimezone(new \DateTimeZone($sSiteTimeZone ))->format('Ymd');
|
$iSiteDate = (int) (new \DateTime('@'.$iTime))->setTimezone(new \DateTimeZone($sSiteTimeZone ))->format('Ymd');
|
||||||
|
|
||||||
return ($iLocalDate == $iSiteDate)?'0':(($iLocalDate > $iSiteDate)?'+1':'-1');
|
return ($iLocalDate == $iSiteDate)?'0':(($iLocalDate < $iSiteDate)?'+1':'-1');
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getTimeZoneFromDate($sDate) {
|
public static function getTimeZoneFromDate($sDate) {
|
||||||
@@ -6,26 +6,22 @@ use Franzz\Objects\Translator;
|
|||||||
|
|
||||||
class Uploader extends UploadHandler
|
class Uploader extends UploadHandler
|
||||||
{
|
{
|
||||||
/**
|
private Media $oMedia;
|
||||||
* Medias Management
|
private Translator $oLang;
|
||||||
* @var Media
|
|
||||||
*/
|
|
||||||
private $oMedia;
|
|
||||||
|
|
||||||
/**
|
public string $sBody;
|
||||||
* Languages
|
|
||||||
* @var Translator
|
|
||||||
*/
|
|
||||||
private $oLang;
|
|
||||||
|
|
||||||
public $sBody;
|
|
||||||
|
|
||||||
function __construct(Media &$oMedia, Translator &$oLang)
|
function __construct(Media &$oMedia, Translator &$oLang)
|
||||||
{
|
{
|
||||||
$this->oMedia = &$oMedia;
|
$this->oMedia = &$oMedia;
|
||||||
$this->oLang = &$oLang;
|
$this->oLang = &$oLang;
|
||||||
$this->sBody = '';
|
$this->sBody = '';
|
||||||
parent::__construct(array('image_versions'=>array(), 'accept_file_types'=>'/\.(gif|jpe?g|png|mov|mp4)$/i'));
|
|
||||||
|
parent::__construct(array(
|
||||||
|
'upload_dir' => Media::MEDIA_FOLDER.'/',
|
||||||
|
'image_versions' => array(),
|
||||||
|
'accept_file_types' => '/\.(gif|jpe?g|png|mov|mp4)$/i'
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function validate($uploaded_file, $file, $error, $index, $content_range) {
|
protected function validate($uploaded_file, $file, $error, $index, $content_range) {
|
||||||
@@ -33,7 +29,7 @@ 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 = $this->get_error_message('upload.mode_archived', array($this->oMedia->getProjectCodeName()));
|
||||||
$bResult = false;
|
$bResult = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,12 +37,15 @@ class Uploader extends UploadHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected function handle_file_upload($uploaded_file, $name, $size, $type, $error, $index = null, $content_range = null) {
|
protected function handle_file_upload($uploaded_file, $name, $size, $type, $error, $index = null, $content_range = null) {
|
||||||
$file = parent::handle_file_upload($uploaded_file, $name, $size, $type, $error, $index, $content_range);
|
$sExt = strtolower(pathinfo((string) $name, PATHINFO_EXTENSION));
|
||||||
|
$sStoredName = bin2hex(random_bytes(16)).($sExt !== ''?'.'.$sExt:'');
|
||||||
|
$file = parent::handle_file_upload($uploaded_file, $sStoredName, $size, $type, $error, $index, $content_range);
|
||||||
|
|
||||||
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 = $this->get_error_message($asResult['desc'], $asResult['data']);
|
||||||
else {
|
else {
|
||||||
|
$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'];
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,18 @@ class User extends PhpObject {
|
|||||||
//Cookie
|
//Cookie
|
||||||
const COOKIE_ID_USER = 'subscriber';
|
const COOKIE_ID_USER = 'subscriber';
|
||||||
const COOKIE_DURATION = 60 * 60 * 24 * 365; //1 year
|
const COOKIE_DURATION = 60 * 60 * 24 * 365; //1 year
|
||||||
|
|
||||||
|
const DEFAULT_USER = array(
|
||||||
|
'id' => 0,
|
||||||
|
'id_user' => 0,
|
||||||
|
'name' => '',
|
||||||
|
'email' => '',
|
||||||
|
'language' => '',
|
||||||
|
'timezone' => '',
|
||||||
|
'active' => self::USER_INACTIVE,
|
||||||
|
'clearance' => self::CLEARANCE_USER
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database Handle
|
* Database Handle
|
||||||
* @var Db
|
* @var Db
|
||||||
@@ -33,104 +45,11 @@ class User extends PhpObject {
|
|||||||
public function __construct(Db &$oDb) {
|
public function __construct(Db &$oDb) {
|
||||||
parent::__construct(__CLASS__);
|
parent::__construct(__CLASS__);
|
||||||
$this->oDb = &$oDb;
|
$this->oDb = &$oDb;
|
||||||
$this->iUserId = 0;
|
$this->setUserId(0);
|
||||||
$this->asUserInfo = array(
|
$this->asUserInfo = self::DEFAULT_USER;
|
||||||
'id' => 0,
|
|
||||||
Db::getId(self::USER_TABLE) => 0,
|
|
||||||
'name' => '',
|
|
||||||
'email' => '',
|
|
||||||
'language' => '',
|
|
||||||
'timezone' => '',
|
|
||||||
'active' => self::USER_INACTIVE,
|
|
||||||
'clearance' => self::CLEARANCE_USER
|
|
||||||
);
|
|
||||||
$this->checkUserCookie();
|
$this->checkUserCookie();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLang() {
|
|
||||||
return $this->asUserInfo['language'];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function addUser($sEmail, $sLang, $sTimezone, $sNickName='') {
|
|
||||||
$bSuccess = false;
|
|
||||||
$sDesc = '';
|
|
||||||
$sEmail = trim($sEmail);
|
|
||||||
|
|
||||||
//Check Email availability
|
|
||||||
$iUserId = $this->oDb->selectValue(self::USER_TABLE, Db::getId(self::USER_TABLE), array('email'=>$sEmail, 'active'=>self::USER_ACTIVE));
|
|
||||||
|
|
||||||
if($iUserId > 0) {
|
|
||||||
//Just log user in
|
|
||||||
$sDesc = 'lang:nl_email_exists';
|
|
||||||
$bSuccess = true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
//Add/Reactivate user
|
|
||||||
$iUserId = $this->oDb->insertUpdateRow(
|
|
||||||
self::USER_TABLE,
|
|
||||||
array('email'=>$sEmail, 'language'=>$sLang, 'timezone'=>$sTimezone, 'active'=>self::USER_ACTIVE),
|
|
||||||
array('email')
|
|
||||||
);
|
|
||||||
|
|
||||||
if($iUserId==0) $sDesc = 'lang:error_commit_db';
|
|
||||||
else {
|
|
||||||
$sDesc = 'lang:nl_subscribed';
|
|
||||||
$bSuccess = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if($bSuccess) {
|
|
||||||
$this->setUserId($iUserId);
|
|
||||||
|
|
||||||
//Set Cookie (valid 1 year)
|
|
||||||
$this->updateCookie(self::COOKIE_DURATION);
|
|
||||||
|
|
||||||
//Update Nickname if user has already posted
|
|
||||||
$this->updateNickname($sNickName);
|
|
||||||
|
|
||||||
//Retrieve Gravatar image
|
|
||||||
$this->updateGravatar($iUserId, $sEmail);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Spot::getResult($bSuccess, $sDesc);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function removeUser() {
|
|
||||||
$bSuccess = false;
|
|
||||||
$sDesc = '';
|
|
||||||
|
|
||||||
if($this->iUserId > 0) {
|
|
||||||
$iUserId = $this->oDb->updateRow(self::USER_TABLE, $this->getUserId(), array('active'=>self::USER_INACTIVE));
|
|
||||||
if($iUserId==0) $sDesc = 'lang:error_commit_db';
|
|
||||||
else {
|
|
||||||
$sDesc = 'lang:nl_unsubscribed';
|
|
||||||
$this->updateCookie(-60 * 60); //Set Cookie in the past, deleting it
|
|
||||||
$bSuccess = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else $sDesc = 'lang:nl_unknown_email';
|
|
||||||
|
|
||||||
return Spot::getResult($bSuccess, $sDesc);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updateNickname($sNickname) {
|
|
||||||
if($this->getUserId() > 0 && $sNickname!='') $this->oDb->updateRow(self::USER_TABLE, $this->getUserId(), array('name'=>$sNickname));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function updateGravatar($iUserId, $sEmail) {
|
|
||||||
$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)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function checkUserCookie() {
|
|
||||||
if(isset($_COOKIE[self::COOKIE_ID_USER])){
|
|
||||||
$this->setUserId($_COOKIE[self::COOKIE_ID_USER]);
|
|
||||||
|
|
||||||
//Extend cookie life
|
|
||||||
if($this->getUserId() > 0) $this->updateCookie(self::COOKIE_DURATION);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getUserId() {
|
public function getUserId() {
|
||||||
return $this->iUserId;
|
return $this->iUserId;
|
||||||
}
|
}
|
||||||
@@ -138,12 +57,14 @@ class User extends PhpObject {
|
|||||||
public function setUserId($iUserId) {
|
public function setUserId($iUserId) {
|
||||||
$this->iUserId = 0;
|
$this->iUserId = 0;
|
||||||
|
|
||||||
|
if($iUserId > 0) {
|
||||||
$asUser = $this->getActiveUserInfo($iUserId);
|
$asUser = $this->getActiveUserInfo($iUserId);
|
||||||
if(!empty($asUser)) {
|
if(!empty($asUser)) {
|
||||||
$this->iUserId = $iUserId;
|
$this->iUserId = $iUserId;
|
||||||
$this->asUserInfo = $asUser;
|
$this->asUserInfo = $asUser;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function getUserInfo() {
|
public function getUserInfo() {
|
||||||
return $this->asUserInfo;
|
return $this->asUserInfo;
|
||||||
@@ -174,6 +95,95 @@ class User extends PhpObject {
|
|||||||
return $this->oDb->selectRows($asInfo);
|
return $this->oDb->selectRows($asInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getLang() {
|
||||||
|
return $this->asUserInfo['language'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addUser($sEmail, $sLang, $sTimezone, $sNickName='') {
|
||||||
|
$bSuccess = false;
|
||||||
|
$sDesc = '';
|
||||||
|
$sEmail = trim($sEmail);
|
||||||
|
|
||||||
|
//Check Email availability
|
||||||
|
$iUserId = $this->oDb->selectValue(self::USER_TABLE, Db::getId(self::USER_TABLE), array('email'=>$sEmail, 'active'=>self::USER_ACTIVE));
|
||||||
|
|
||||||
|
if($iUserId > 0) {
|
||||||
|
//Just log user in
|
||||||
|
$sDesc = 'lang:newsletter.email_exists';
|
||||||
|
$bSuccess = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//Add/Reactivate user
|
||||||
|
$iUserId = $this->oDb->insertUpdateRow(
|
||||||
|
self::USER_TABLE,
|
||||||
|
array('email'=>$sEmail, 'language'=>$sLang, 'timezone'=>$sTimezone, 'active'=>self::USER_ACTIVE),
|
||||||
|
array('email')
|
||||||
|
);
|
||||||
|
|
||||||
|
if($iUserId==0) $sDesc = 'lang:error.commit_db';
|
||||||
|
else {
|
||||||
|
$sDesc = 'lang:newsletter.subscribed';
|
||||||
|
$bSuccess = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($bSuccess) {
|
||||||
|
$this->setUserId($iUserId);
|
||||||
|
|
||||||
|
//Set Cookie (valid 1 year)
|
||||||
|
$this->updateCookie(self::COOKIE_DURATION);
|
||||||
|
|
||||||
|
//Update Nickname if user has already posted
|
||||||
|
$this->updateNickname($sNickName);
|
||||||
|
|
||||||
|
//Retrieve Gravatar image
|
||||||
|
$this->updateGravatar($iUserId, $sEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Spot::getResult($bSuccess, $sDesc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeUser($iUserId=0) {
|
||||||
|
$iUserId = ($iUserId > 0)?$iUserId:$this->getUserId();
|
||||||
|
$bSelf = ($iUserId == $this->getUserId());
|
||||||
|
$bSuccess = false;
|
||||||
|
$sDesc = '';
|
||||||
|
|
||||||
|
if($bSelf || $this->checkUserClearance(self::CLEARANCE_ADMIN)) {
|
||||||
|
if($this->getUserId() > 0) {
|
||||||
|
$iUserId = $this->oDb->updateRow(self::USER_TABLE, $iUserId, array('active' => self::USER_INACTIVE));
|
||||||
|
if($iUserId==0) $sDesc = 'lang:error.commit_db';
|
||||||
|
else {
|
||||||
|
$sDesc = 'lang:newsletter.unsubscribed';
|
||||||
|
if($bSelf) $this->updateCookie(-60 * 60); //Set Cookie in the past, deleting it
|
||||||
|
$bSuccess = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else $sDesc = 'lang:newsletter.unknown_email';
|
||||||
|
}
|
||||||
|
else $sDesc = 'lang:error.no_auth';
|
||||||
|
|
||||||
|
return Spot::getResult($bSuccess, $sDesc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateNickname($sNickname) {
|
||||||
|
if($this->getUserId() > 0 && $sNickname!='') $this->oDb->updateRow(self::USER_TABLE, $this->getUserId(), array('name'=>$sNickname));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function updateGravatar($iUserId, $sEmail) {
|
||||||
|
$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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkUserCookie() {
|
||||||
|
if(isset($_COOKIE[self::COOKIE_ID_USER])){
|
||||||
|
$this->setUserId($_COOKIE[self::COOKIE_ID_USER]);
|
||||||
|
|
||||||
|
//Extend cookie life
|
||||||
|
if($this->getUserId() > 0) $this->updateCookie(self::COOKIE_DURATION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function checkUserClearance($iClearance)
|
public function checkUserClearance($iClearance)
|
||||||
{
|
{
|
||||||
return ($this->asUserInfo['clearance'] >= $iClearance);
|
return ($this->asUserInfo['clearance'] >= $iClearance);
|
||||||
@@ -188,7 +198,7 @@ class User extends PhpObject {
|
|||||||
if(!in_array($iClearance, self::CLEARANCES)) $sDesc = 'Setting wrong clearance "'.$iClearance.'" to user ID "'.$iUserId.'"';
|
if(!in_array($iClearance, self::CLEARANCES)) $sDesc = 'Setting wrong clearance "'.$iClearance.'" to user ID "'.$iUserId.'"';
|
||||||
else {
|
else {
|
||||||
$iUserId = $this->oDb->updateRow(self::USER_TABLE, $iUserId, array('clearance'=>$iClearance));
|
$iUserId = $this->oDb->updateRow(self::USER_TABLE, $iUserId, array('clearance'=>$iClearance));
|
||||||
if(!$iUserId) $sDesc = 'lang:error_commit_db';
|
if(!$iUserId) $sDesc = 'lang:error.commit_db';
|
||||||
else $bSuccess = true;
|
else $bSuccess = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
212
masks/admin.html
@@ -1,212 +0,0 @@
|
|||||||
<div id="admin">
|
|
||||||
<a name="back" class="button" href="[#]host_url[#]"><i class="fa fa-back push"></i>[#]lang:nav_back[#]</a>
|
|
||||||
<h1>[#]lang:projects[#]</h1>
|
|
||||||
<div id="project_section">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>[#]lang:id_project[#]</th>
|
|
||||||
<th>[#]lang:project[#]</th>
|
|
||||||
<th>[#]lang:mode[#]</th>
|
|
||||||
<th>[#]lang:code_name[#]</th>
|
|
||||||
<th>[#]lang:start[#]</th>
|
|
||||||
<th>[#]lang:end[#]</th>
|
|
||||||
<th>[#]lang:delete[#]</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody></tbody>
|
|
||||||
</table>
|
|
||||||
<div id="new"></div>
|
|
||||||
</div>
|
|
||||||
<h1>[#]lang:feeds[#]</h1>
|
|
||||||
<div id="feed_section">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>[#]lang:id_feed[#]</th>
|
|
||||||
<th>[#]lang:ref_feed_id[#]</th>
|
|
||||||
<th>[#]lang:id_spot[#]</th>
|
|
||||||
<th>[#]lang:id_project[#]</th>
|
|
||||||
<th>[#]lang:name[#]</th>
|
|
||||||
<th>[#]lang:status[#]</th>
|
|
||||||
<th>[#]lang:last_update[#]</th>
|
|
||||||
<th>[#]lang:delete[#]</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<h1>Spots</h1>
|
|
||||||
<div id="spot_section">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>[#]lang:id_spot[#]</th>
|
|
||||||
<th>[#]lang:ref_spot_id[#]</th>
|
|
||||||
<th>[#]lang:name[#]</th>
|
|
||||||
<th>[#]lang:model[#]</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<h1>[#]lang:active_users[#]</h1>
|
|
||||||
<div id="user_section">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>[#]lang:id_user[#]</th>
|
|
||||||
<th>[#]lang:user_name[#]</th>
|
|
||||||
<th>[#]lang:language[#]</th>
|
|
||||||
<th>[#]lang:time_zone[#]</th>
|
|
||||||
<th>[#]lang:clearance[#]</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<h1>[#]lang:toolbox[#]</h1>
|
|
||||||
<div id="toolbox"></div>
|
|
||||||
<div id="feedback" class="feedback"></div>
|
|
||||||
</div>
|
|
||||||
<script type="text/javascript">
|
|
||||||
oSpot.pageInit = function(asHash) {
|
|
||||||
self.get('admin_get', setProjects);
|
|
||||||
$('#new').addButton('new', self.lang('new_project'), 'new', createProject);
|
|
||||||
$('#toolbox').addButton('refresh', self.lang('update_project'), 'refresh', updateProject);
|
|
||||||
};
|
|
||||||
|
|
||||||
oSpot.onFeedback = function(sType, sMsg, asContext) {
|
|
||||||
delete asContext.a;
|
|
||||||
delete asContext.t;
|
|
||||||
sMsg += ' (';
|
|
||||||
$.each(asContext, function(sKey, sElem) {
|
|
||||||
sMsg += sKey+'='+sElem+' / ' ;
|
|
||||||
});
|
|
||||||
sMsg = sMsg.slice(0, -3)+')';
|
|
||||||
$('#feedback').append($('<p>', {'class': sType}).text(sMsg));
|
|
||||||
};
|
|
||||||
|
|
||||||
function setProjects(asElemTypes) {
|
|
||||||
var aoEvents = [{on:'change', callback:commit}, {on:'keyup', callback:waitAndCommit}];
|
|
||||||
var aoChangeEvent = [aoEvents[0]];
|
|
||||||
|
|
||||||
$.each(asElemTypes, function(sElemType, aoElems) {
|
|
||||||
$.each(aoElems, function(iKey, oElem) {
|
|
||||||
var sElemId = sElemType+'_'+oElem.id;
|
|
||||||
var bNew = ($('#'+sElemId).length == 0);
|
|
||||||
|
|
||||||
var $Elem = (bNew?$('<tr>', {'id': sElemId}):$('#'+sElemId))
|
|
||||||
.data('type', sElemType)
|
|
||||||
.data('id', oElem.id);
|
|
||||||
|
|
||||||
if(oElem.del) $Elem.remove();
|
|
||||||
else if(!bNew) {
|
|
||||||
$Elem.find('input').each(function(iKey, oInput){
|
|
||||||
var $Input = $(oInput);
|
|
||||||
if($Input.attr('name') in oElem && $Input.attr('type')!='date') $Input.val(oElem[$Input.attr('name')]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$Elem.append($('<td>').text(oElem.id || ''));
|
|
||||||
switch(sElemType) {
|
|
||||||
case 'project':
|
|
||||||
$Elem
|
|
||||||
.append($('<td>').addInput('text', 'name', oElem.name, aoEvents))
|
|
||||||
.append($('<td>', {'class': 'mode'}).text(oElem.mode))
|
|
||||||
.append($('<td>').addInput('text', 'codename', oElem.codename, aoEvents))
|
|
||||||
.append($('<td>').addInput('date', 'active_from', oElem.active_from, aoChangeEvent))
|
|
||||||
.append($('<td>').addInput('date', 'active_to', oElem.active_to, aoChangeEvent))
|
|
||||||
.append($('<td>').addButton('close fa-lg', '', 'del_proj', del));
|
|
||||||
break;
|
|
||||||
case 'feed':
|
|
||||||
$Elem
|
|
||||||
.append($('<td>').addInput('text', 'ref_feed_id', oElem.ref_feed_id, aoEvents))
|
|
||||||
.append($('<td>').addInput('number', 'id_spot', oElem.id_spot, aoEvents))
|
|
||||||
.append($('<td>').addInput('number', 'id_project', oElem.id_project, aoEvents))
|
|
||||||
.append($('<td>').text(oElem.name))
|
|
||||||
.append($('<td>').text(oElem.status))
|
|
||||||
.append($('<td>').text(oElem.last_update))
|
|
||||||
.append($('<td>').addButton('close fa-lg', '', 'del_feed', del));
|
|
||||||
break;
|
|
||||||
case 'spot':
|
|
||||||
$Elem
|
|
||||||
.append($('<td>').text(oElem.ref_spot_id))
|
|
||||||
.append($('<td>').text(oElem.name))
|
|
||||||
.append($('<td>').text(oElem.model))
|
|
||||||
break;
|
|
||||||
case 'user':
|
|
||||||
$Elem
|
|
||||||
.append($('<td>').text(oElem.name))
|
|
||||||
.append($('<td>').text(oElem.language))
|
|
||||||
.append($('<td>').text(oElem.timezone))
|
|
||||||
.append($('<td>').addInput('number', 'clearance', oElem.clearance, aoEvents))
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
$Elem.appendTo($('#'+sElemType+'_section').find('table tbody'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createProject() {
|
|
||||||
self.get('admin_new', setProjects);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateProject() {
|
|
||||||
self.get(
|
|
||||||
'update_project',
|
|
||||||
function(asData, sMsg){oSpot.onFeedback('success', sMsg, {'update':'project'});},
|
|
||||||
{},
|
|
||||||
function(sMsg){oSpot.onFeedback('error', sMsg, {'update':'project'});}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function commit(event, $This) {
|
|
||||||
$This = $This || $(this);
|
|
||||||
if(typeof self.tmp('wait') != 'undefined') clearTimeout(self.tmp('wait'));
|
|
||||||
|
|
||||||
var sOldVal = $This.data('old_value');
|
|
||||||
var sNewVal = $This.val();
|
|
||||||
if(sOldVal!=sNewVal) {
|
|
||||||
$This.data('old_value', sNewVal);
|
|
||||||
|
|
||||||
var $Record = $This.closest('tr');
|
|
||||||
var asInputs = {type: $Record.data('type'), id: $Record.data('id'), field: $This.attr('name'), value: sNewVal};
|
|
||||||
self.get(
|
|
||||||
'admin_set',
|
|
||||||
function(asData){
|
|
||||||
oSpot.onFeedback('success', self.lang('admin_save_success'), asInputs);
|
|
||||||
setProjects(asData);
|
|
||||||
},
|
|
||||||
asInputs,
|
|
||||||
function(sError){
|
|
||||||
$This.data('old_value', sOldVal);
|
|
||||||
oSpot.onFeedback('error', sError, asInputs);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function waitAndCommit(event) {
|
|
||||||
if(typeof self.tmp('wait') != 'undefined') clearTimeout(self.tmp('wait'));
|
|
||||||
self.tmp('wait', setTimeout(()=>{commit(event,$(this));}, 2000));
|
|
||||||
}
|
|
||||||
|
|
||||||
function del() {
|
|
||||||
var $Record = $(this).closest('tr');
|
|
||||||
var asInputs = {type: $Record.data('type'), id: $Record.data('id')};
|
|
||||||
self.get(
|
|
||||||
'admin_del',
|
|
||||||
function(asData){
|
|
||||||
oSpot.onFeedback('success', self.lang('admin_save_success'), asInputs);
|
|
||||||
setProjects(asData);
|
|
||||||
},
|
|
||||||
asInputs,
|
|
||||||
function(sError){
|
|
||||||
oSpot.onFeedback('error', sError, asInputs);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
|
|
||||||
<title>[#]lang:email_conf_subject[#]</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<span style="color: transparent; display: none !important; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">[#]lang:conf_preheader[#]</span>
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="width:100%;max-width:600px;">
|
|
||||||
<tr>
|
|
||||||
<td width="20%"><img src="[#]local_server[#]images/icons/mstile-144x144.png" width="90%" border="0" alt="logo" /></td>
|
|
||||||
<td><h1>[#]lang:conf_thanks_sub[#]</h1></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="2">
|
|
||||||
<p align="justify">[#]lang:conf_body_para_1[#]</p>
|
|
||||||
<p align="justify">[#]lang:conf_body_para_2[#]</p>
|
|
||||||
<p align="justify">[#]lang:conf_body_para_3[#]</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="2">
|
|
||||||
<p>[#]lang:conf_body_conclusion[#]<br />[#]lang:conf_signature[#]</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="2">
|
|
||||||
<p>[#]lang:email_unsubscribe[#] <a href="[#]unsubscribe_link[#]" target="_blank" rel="noopener">[#]lang:email_unsub_btn[#]</a></p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="[#]language[#]">
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<meta name="description" content="[#]lang:page_og_desc[#]">
|
|
||||||
<meta property="og:title" content="Spotty" />
|
|
||||||
<meta property="og:description" content="[#]lang:page_og_desc[#]" />
|
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
<meta property="og:url" content="[#]host_url[#]" />
|
|
||||||
<meta property="og:image" content="images/ogp.png" />
|
|
||||||
<meta property="og:locale" content="[#]lang:locale[#]" />
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="images/icons/apple-touch-icon.png?v=GvmqYyKwbb">
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="images/icons/favicon-32x32.png?v=GvmqYyKwbb">
|
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="images/icons/favicon-16x16.png?v=GvmqYyKwbb">
|
|
||||||
<link rel="manifest" href="images/icons/site.webmanifest?v=GvmqYyKwbb">
|
|
||||||
<link rel="mask-icon" href="images/icons/safari-pinned-tab.svg?v=GvmqYyKwbb" color="#44d15a">
|
|
||||||
<link rel="shortcut icon" href="images/icons/favicon.ico?v=GvmqYyKwbb">
|
|
||||||
<meta name="msapplication-TileColor" content="#00a300">
|
|
||||||
<meta name="msapplication-config" content="images/icons/browserconfig.xml?v=GvmqYyKwbb">
|
|
||||||
<meta name="theme-color" content="#ffffff">
|
|
||||||
<link type="text/css" href="[#]filepath_css[#]" rel="stylesheet" media="all" />
|
|
||||||
<script type="text/javascript" src="[#]filepath_js_d3[#]"></script>
|
|
||||||
<script type="text/javascript" src="[#]filepath_js_leaflet[#]"></script>
|
|
||||||
<script type="text/javascript" src="[#]filepath_js_jquery[#]"></script>
|
|
||||||
<script type="text/javascript" src="[#]filepath_js_jquery_mods[#]"></script>
|
|
||||||
<script type="text/javascript" src="[#]filepath_js_spot[#]"></script>
|
|
||||||
<script type="text/javascript" src="[#]filepath_js_lightbox[#]"></script>
|
|
||||||
<script type="text/javascript">
|
|
||||||
var oSpot = new Spot([#]GLOBAL_VARS[#]);
|
|
||||||
$(document).ready(oSpot.init);
|
|
||||||
</script>
|
|
||||||
<title></title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="container">
|
|
||||||
<div id="main"></div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
1205
masks/project.html
@@ -1,94 +0,0 @@
|
|||||||
<div id="upload">
|
|
||||||
<a name="back" class="button" href="[#]host_url[#]"><i class="fa fa-back push"></i>[#]lang:nav_back[#]</a>
|
|
||||||
<h1>[#]lang:upload_title[#]</h1>
|
|
||||||
<input id="fileupload" type="file" name="files[]" multiple>
|
|
||||||
<div id="progress">
|
|
||||||
<div class="bar" style="width: 0%;"></div>
|
|
||||||
</div>
|
|
||||||
<div id="comments"></div>
|
|
||||||
<div id="location">
|
|
||||||
<button id="add_loc"><i class="fa fa-message push"></i>New Position</button>
|
|
||||||
</div>
|
|
||||||
<div id="status"></div>
|
|
||||||
</div>
|
|
||||||
<script type="text/javascript">
|
|
||||||
oSpot.pageInit = function(asHash) {
|
|
||||||
var asProject = self.vars(['projects', self.vars('default_project_codename')]);
|
|
||||||
self.tmp('status-box', $('#status'));
|
|
||||||
if(asProject.editable) {
|
|
||||||
$('#fileupload')
|
|
||||||
.attr('data-url', self.getActionLink('upload'))
|
|
||||||
.fileupload({
|
|
||||||
dataType: 'json',
|
|
||||||
formData: {t: self.consts.timezone},
|
|
||||||
acceptFileTypes: /(\.|\/)(gif|jpe?g|png|mov)$/i,
|
|
||||||
done: function (e, asData) {
|
|
||||||
$.each(asData.result.files, function(iKey, oFile) {
|
|
||||||
var bError = ('error' in oFile);
|
|
||||||
|
|
||||||
//Feedback
|
|
||||||
addStatus(bError?oFile.error:(self.lang('upload_success', [oFile.name])));
|
|
||||||
|
|
||||||
//Comments
|
|
||||||
if(!bError) addCommentBox(oFile.id, oFile.thumbnail);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
progressall: function (e, data) {
|
|
||||||
var progress = parseInt(data.loaded / data.total * 100, 10);
|
|
||||||
$('#progress .bar').css('width', progress+'%');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#add_loc').click(() => {
|
|
||||||
if(navigator.geolocation) {
|
|
||||||
addStatus('Determining position...');
|
|
||||||
navigator.geolocation.getCurrentPosition(
|
|
||||||
(position) => {
|
|
||||||
addStatus('Sending position...');
|
|
||||||
oSpot.get(
|
|
||||||
'add_position',
|
|
||||||
function(asData){addStatus('Position sent');},
|
|
||||||
{'latitude':position.coords.latitude, 'longitude':position.coords.longitude, 'timestamp':Math.round(position.timestamp / 1000)},
|
|
||||||
function(sMsgId){addStatus(self.lang(sMsgId));},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
addStatus(error.message);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else addStatus('This browser does not support geolocation');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else addStatus(self.lang('upload_mode_archived', [asProject.name]), true);
|
|
||||||
};
|
|
||||||
|
|
||||||
function addCommentBox(iMediaId, sThumbnailPath) {
|
|
||||||
$('#comments').append($('<div>', {'class':'comment'})
|
|
||||||
.append($('<img>', {'class':'thumb', 'src':sThumbnailPath}))
|
|
||||||
.append($('<form>')
|
|
||||||
.append($('<input>', {'class':'content', 'name':'content', 'type':'text'}))
|
|
||||||
.append($('<input>', {'class':'id', 'name':'id', 'type':'hidden', 'value':iMediaId}))
|
|
||||||
.append($('<button>', {'class':'save', 'type':'button'})
|
|
||||||
.click(function(){
|
|
||||||
var $Form = $(this).parent();
|
|
||||||
oSpot.get(
|
|
||||||
'add_comment',
|
|
||||||
function(asData){addStatus(self.lang('media_comment_update', asData.filename));},
|
|
||||||
{id:$Form.find('.id').val(), content:$Form.find('.content').val()},
|
|
||||||
function(sMsgId){addStatus(self.lang(sMsgId));},
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.text(self.lang('save'))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addStatus(sMsg, bClear) {
|
|
||||||
bClear = bClear || false;
|
|
||||||
if(bClear) self.tmp('status-box').empty();
|
|
||||||
|
|
||||||
self.tmp('status-box').append($('<p>').text(sMsg));
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
4878
package-lock.json
generated
Normal file
37
package.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.23.9",
|
||||||
|
"@babel/preset-env": "^7.23.9",
|
||||||
|
"babel-loader": "^10.0.0",
|
||||||
|
"symlink-webpack-plugin": "^1.1.0",
|
||||||
|
"vue-loader": "^17.4.2",
|
||||||
|
"webpack": "^5.99.7",
|
||||||
|
"webpack-cli": "^7.0.2"
|
||||||
|
},
|
||||||
|
"name": "spot",
|
||||||
|
"description": "FindMeSpot & GPX integration",
|
||||||
|
"version": "2.1.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "flock -n node_modules/.webpack-build.lock webpack --config build/webpack.config.js --mode development",
|
||||||
|
"prod": "flock -n node_modules/.webpack-build.lock webpack --config build/webpack.config.js --mode production"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "Franzz",
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^7.2.0",
|
||||||
|
"@fortawesome/vue-fontawesome": "^3.2.0",
|
||||||
|
"@uppy/core": "^5.2.0",
|
||||||
|
"@uppy/xhr-upload": "^5.2.0",
|
||||||
|
"autosize": "^6.0.1",
|
||||||
|
"css-loader": "^7.1.2",
|
||||||
|
"maplibre-gl": "^5.4.0",
|
||||||
|
"sass": "^1.97.2",
|
||||||
|
"sass-loader": "^17.0.0",
|
||||||
|
"simplebar-vue": "^2.3.3",
|
||||||
|
"vue": "^3.3.8",
|
||||||
|
"vue-style-loader": "^4.1.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
public/index.php
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require __DIR__.'/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use Franzz\Spot\Controller;
|
||||||
|
|
||||||
|
echo (new Controller())->handle(__FILE__, $argv ?? array());
|
||||||
40
readme.md
@@ -1,6 +1,10 @@
|
|||||||
# Spot Project
|
# Spot Project
|
||||||
[Spot](https://www.findmespot.com) & GPX integration
|
[Spot](https://www.findmespot.com) & GPX integration
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
|
* npm 24+
|
||||||
|
* composer
|
||||||
* php-mbstring
|
* php-mbstring
|
||||||
* php-imagick
|
* php-imagick
|
||||||
* php-gd
|
* php-gd
|
||||||
@@ -9,24 +13,44 @@
|
|||||||
* ffprobe & ffmpeg
|
* ffprobe & ffmpeg
|
||||||
* STARTTLS Email Server (use Gmail if none available)
|
* STARTTLS Email Server (use Gmail if none available)
|
||||||
* Optional: Geo Caching Server (WMTS Caching Service)
|
* Optional: Geo Caching Server (WMTS Caching Service)
|
||||||
|
|
||||||
## PHP Configuration
|
## PHP Configuration
|
||||||
|
|
||||||
* max_execution_time = 300
|
* max_execution_time = 300
|
||||||
* memory_limit = 500M
|
* memory_limit = 500M
|
||||||
* post_max_size = 4G
|
* post_max_size = 4G
|
||||||
* upload_max_filesize = 4G
|
* upload_max_filesize = 4G
|
||||||
* max_file_uploads = 50
|
* max_file_uploads = 50
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
1. Clone Git onto web server
|
1. Clone Git onto web server
|
||||||
2. Install dependencies & update php.ini parameters
|
2. Update php.ini parameters
|
||||||
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. Go to #admin and create a new project, feed & maps
|
5. Follow CI/CD script in .gitea/workflows/deploy.yml
|
||||||
6. Add a GPX file named <project_codename>.gpx to /geo/
|
8. Go to #admin and create a new project, feed & maps
|
||||||
7. Run composer install
|
9. Add a GPX file named <project_codename>.gpx to /resources/geo/
|
||||||
|
|
||||||
|
## Web Root
|
||||||
|
|
||||||
|
The web server should serve `public/` as the application document root. PHP source, configuration, Composer dependencies, uploaded files, and GPX data stay outside the public tree; `public/index.php` is the front controller and webpack writes generated frontend assets to `public/assets/`.
|
||||||
|
|
||||||
|
Runtime data is exposed through symlinks only: `public/files -> ../files` and `public/geo -> ../resources/geo`. The build must not copy uploaded media or GPX data into `public/`.
|
||||||
|
|
||||||
|
## Local Development
|
||||||
|
|
||||||
|
When developing Spot and the sibling `objects` library together, install dependencies through the local Composer manifest:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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 Spot version that relies on them.
|
||||||
|
|
||||||
## To Do List
|
## To Do List
|
||||||
* ECMA import/export
|
|
||||||
* Add mail frequency slider
|
* Add mail frequency slider
|
||||||
* Use WMTS servers directly when not using Geo Caching Server
|
* Use WMTS servers directly when not using Geo Caching Server
|
||||||
* Allow HEIF picture format
|
* Allow HEIF picture format
|
||||||
* Vector tiles support (https://www.arcgis.com/home/item.html?id=7dc6cea0b1764a1f9af2e679f642f0f5) + Use of GL library. Use Mapbox GL JS / Maplibre GL JS / ESRI-Leaflet-vector?
|
* Garmin InReach Integration
|
||||||
* Fix .MOV playback on windows firefox
|
|
||||||
|
|||||||
215376
resources/geo/gr20.gpx
Normal file
208
resources/lang/en.json
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
{
|
||||||
|
"action": {
|
||||||
|
"back": "Back",
|
||||||
|
"delete": "Delete",
|
||||||
|
"save": "Save",
|
||||||
|
"send": "Send"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"config": "Settings",
|
||||||
|
"create_success": "Created",
|
||||||
|
"delete_success": "Deleted",
|
||||||
|
"save_success": "Saved",
|
||||||
|
"title": "Admin Panel",
|
||||||
|
"toolbox": "Toolbox",
|
||||||
|
"upload": "Upload"
|
||||||
|
},
|
||||||
|
"credits": {
|
||||||
|
"git": "Git repository",
|
||||||
|
"license": "licensed under GPLv3",
|
||||||
|
"project": "$0 Project"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"confirmation": {
|
||||||
|
"body_1": "Thank you for following my wanderings :). I'll make sure to keep you posted on my progress along the trail.",
|
||||||
|
"body_2": "I usually check in once a day, and sometimes at special moments too, like successful peak ascents. I'm using a GPS-based device (PLB), which doesn't require phone reception to work. So messages should be fairly frequent, but — being awestruck by the beauty of nature — I might also just forget to send a signal once in a while. So don't worry if you don't receive anything for a couple of days.",
|
||||||
|
"body_3": "If I've posted any pictures recently, you'll also find them in the same email.",
|
||||||
|
"conclusion": "See you down the road!",
|
||||||
|
"preheader": "Thanks for keeping in touch!",
|
||||||
|
"signature": "--François",
|
||||||
|
"subject": "Registration confirmed",
|
||||||
|
"thanks_subject": "You're all set!"
|
||||||
|
},
|
||||||
|
"unsubscribe": "PS: Changed your mind?",
|
||||||
|
"unsubscribe_button": "Unsubscribe",
|
||||||
|
"update": {
|
||||||
|
"latest_news": "Latest news:",
|
||||||
|
"preheader": "New position received",
|
||||||
|
"subject": "Spotted!",
|
||||||
|
"title": "Message"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"commit_db": "Error committing to database",
|
||||||
|
"impossible_value": "Value \"$0\" is not valid for field \"$1\"",
|
||||||
|
"no_auth": "Not authorized",
|
||||||
|
"unknown_field": "Unknown field \"$0\""
|
||||||
|
},
|
||||||
|
"feed": {
|
||||||
|
"counter": "#$0",
|
||||||
|
"id": "Feed ID",
|
||||||
|
"last_update": "Last Spot update",
|
||||||
|
"name": "Name",
|
||||||
|
"new": "New feed",
|
||||||
|
"plural": "Feeds",
|
||||||
|
"ref_id": "Feed reference ID",
|
||||||
|
"status": "Status"
|
||||||
|
},
|
||||||
|
"map": {
|
||||||
|
"ign_france": "IGN (France)",
|
||||||
|
"ign_spain": "IGN (Spain)",
|
||||||
|
"linz": "LINZ",
|
||||||
|
"natgeo": "National Geographic",
|
||||||
|
"otm": "Open Topo Map",
|
||||||
|
"outdoors": "Mapbox Outdoors",
|
||||||
|
"satellite": "Satellite",
|
||||||
|
"see_on_google": "View position on Google Maps",
|
||||||
|
"title": "Base maps",
|
||||||
|
"usgs": "USGS"
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"click_watch": "Click to watch the video",
|
||||||
|
"click_zoom": "Click to zoom",
|
||||||
|
"comment_update": "Comment for media \"$0\" updated",
|
||||||
|
"image": "Picture",
|
||||||
|
"image_taken_on": "Taken on $0",
|
||||||
|
"images": "Pictures",
|
||||||
|
"nearby": "Nearby pictures",
|
||||||
|
"no_id": "Missing media ID in request",
|
||||||
|
"posted_on": "Posted on $0",
|
||||||
|
"video": "Video",
|
||||||
|
"video_taken_on": "Shot on $0"
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"locale": "en_NZ",
|
||||||
|
"page_og_desc": "Stay in touch while I'm away hiking."
|
||||||
|
},
|
||||||
|
"newsletter": {
|
||||||
|
"email_exists": "This email address is already subscribed. You can unsubscribe by clicking the button above.",
|
||||||
|
"email_placeholder": "my@email.com",
|
||||||
|
"invalid_email": "This doesn't look like a valid email address",
|
||||||
|
"subscribe": "Subscribe",
|
||||||
|
"subscribed": "Thanks! You'll receive a confirmation email shortly.",
|
||||||
|
"subscribed_desc": "You're all set. I'll send you updates!",
|
||||||
|
"title": "Keep in touch!",
|
||||||
|
"unknown_email": "Unknown email address",
|
||||||
|
"unsubscribe": "Unsubscribe",
|
||||||
|
"unsubscribed": "Done. No more junk mail from me.",
|
||||||
|
"unsubscribed_desc": "Enter your email address to receive my position as soon as I find it :)"
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"copy_to_clipboard": "Copy direct link to clipboard",
|
||||||
|
"link_copied": "Link copied!",
|
||||||
|
"message": "Message",
|
||||||
|
"name": "Name",
|
||||||
|
"new_message": "New message"
|
||||||
|
},
|
||||||
|
"project": {
|
||||||
|
"wip": "In progress",
|
||||||
|
"code_name": "Code name",
|
||||||
|
"end": "End",
|
||||||
|
"hikes": "Hikes",
|
||||||
|
"id": "Project ID",
|
||||||
|
"mode": "Mode",
|
||||||
|
"modes": {
|
||||||
|
"blog": "Active project",
|
||||||
|
"histo": "Archived project",
|
||||||
|
"previz": "Project in preparation"
|
||||||
|
},
|
||||||
|
"new": "New project",
|
||||||
|
"overview": "Overview",
|
||||||
|
"plural": "Projects",
|
||||||
|
"single": "Project",
|
||||||
|
"start": "Start",
|
||||||
|
"update_messages": "Update project messages"
|
||||||
|
},
|
||||||
|
"spot": {
|
||||||
|
"id": "Spot ID",
|
||||||
|
"model": "Model",
|
||||||
|
"name": "Spot name",
|
||||||
|
"plural": "Spots",
|
||||||
|
"ref_id": "Ref. Spot ID"
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"duration": "Duration",
|
||||||
|
"distance": "Distance",
|
||||||
|
"elevation": "Elevation",
|
||||||
|
"elevation_gain": "Elevation gain",
|
||||||
|
"elevation_loss": "Elevation loss",
|
||||||
|
"from": "Start",
|
||||||
|
"legend": "Legend",
|
||||||
|
"segment_length": "Segment length",
|
||||||
|
"to": "Finish",
|
||||||
|
"type": "Track type"
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"city": "$0 time",
|
||||||
|
"date_time": "$0 at $1",
|
||||||
|
"local": "$0 local time",
|
||||||
|
"user": "$0 your time",
|
||||||
|
"zone": "Time zone"
|
||||||
|
},
|
||||||
|
"track": {
|
||||||
|
"download": "Download GPX track",
|
||||||
|
"hitchhiking": "Hitchhiking",
|
||||||
|
"main": "Main track",
|
||||||
|
"off-track": "Off-track"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"day": "day",
|
||||||
|
"day_short": "D",
|
||||||
|
"days": "days",
|
||||||
|
"hour": "h"
|
||||||
|
},
|
||||||
|
"upload": {
|
||||||
|
"error": "Upload failed",
|
||||||
|
"media": {
|
||||||
|
"exists": "Picture $0 already exists",
|
||||||
|
"title": "Picture and video uploads"
|
||||||
|
},
|
||||||
|
"mode_archived": "Project \"$0\" is archived. Uploads are not allowed.",
|
||||||
|
"position": {
|
||||||
|
"new": "New position",
|
||||||
|
"title": "Add position"
|
||||||
|
},
|
||||||
|
"success": "$0 uploaded successfully"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"active": "Active users",
|
||||||
|
"clearance": "Clearance",
|
||||||
|
"id": "User ID",
|
||||||
|
"language": "Language",
|
||||||
|
"name": "User name"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"clear-day": "Cloud cover is less than 20% during the day",
|
||||||
|
"clear-night": "Cloud cover is less than 20% during the night",
|
||||||
|
"cloudy": "Cloud cover is greater than 90%",
|
||||||
|
"fog": "Visibility is low (less than 1km)",
|
||||||
|
"hail": "Hail showers",
|
||||||
|
"partly-cloudy-day": "Cloud cover is greater than 20% during the day",
|
||||||
|
"partly-cloudy-night": "Cloud cover is greater than 20% during the night",
|
||||||
|
"rain": "Rainfall is greater than zero",
|
||||||
|
"rain-snow": "Snow and rain showers",
|
||||||
|
"rain-snow-showers-day": "Possible rain/snow throughout the day",
|
||||||
|
"rain-snow-showers-night": "Possible rain/snow throughout the night",
|
||||||
|
"showers-day": "Rain showers during the day",
|
||||||
|
"showers-night": "Rain showers during the night",
|
||||||
|
"sleet": "Sleet",
|
||||||
|
"snow": "Snowfall is greater than zero",
|
||||||
|
"snow-showers-day": "Periods of snow during the day",
|
||||||
|
"snow-showers-night": "Periods of snow during the night",
|
||||||
|
"thunder": "Thunderstorms",
|
||||||
|
"thunder-rain": "Thunderstorms throughout the day or night",
|
||||||
|
"thunder-showers-day": "Possible thunderstorms throughout the day",
|
||||||
|
"thunder-showers-night": "Possible thunderstorms throughout the night",
|
||||||
|
"wind": "Wind speed is high (greater than 30 km/h)"
|
||||||
|
}
|
||||||
|
}
|
||||||
208
resources/lang/es.json
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
{
|
||||||
|
"action": {
|
||||||
|
"back": "Atrás",
|
||||||
|
"delete": "Borrar",
|
||||||
|
"save": "Guardar",
|
||||||
|
"send": "Enviar"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"config": "Configuración",
|
||||||
|
"create_success": "Creado",
|
||||||
|
"delete_success": "Eliminado",
|
||||||
|
"save_success": "Guardado",
|
||||||
|
"title": "Administración",
|
||||||
|
"toolbox": "Herramientas",
|
||||||
|
"upload": "Cargar"
|
||||||
|
},
|
||||||
|
"credits": {
|
||||||
|
"git": "Repositorio de Git",
|
||||||
|
"license": "bajo licencia GPLv3",
|
||||||
|
"project": "Proyecto $0"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"confirmation": {
|
||||||
|
"body_1": "Te agradezco mucho que sigas mi proyecto y que te intereses por su evolución. Te prometo que te mantendré informado sobre mi progreso.",
|
||||||
|
"body_2": "Normalmente envío un mensaje una vez al día. Cuando voy a lugares especiales, envío uno extra (cimas, ese tipo de cosas). Uso un dispositivo GPS para enviar la señal, así que no necesito cobertura telefónica para que funcione. Sin embargo, puede haber ocasiones en las que no presione el botón. Por lo tanto, no te preocupes si no recibes mensajes durante uno o dos días.",
|
||||||
|
"body_3": "Cuando añada fotos a la página, también las encontrarás en este correo electrónico.",
|
||||||
|
"conclusion": "¡Nos vemos en el camino!",
|
||||||
|
"preheader": "¡Gracias por mantenerte en contacto!",
|
||||||
|
"signature": "--François",
|
||||||
|
"subject": "Confirmación",
|
||||||
|
"thanks_subject": "¡Hecho!"
|
||||||
|
},
|
||||||
|
"unsubscribe": "PD: ¿Demasiados correos electrónicos?",
|
||||||
|
"unsubscribe_button": "Darse de baja",
|
||||||
|
"update": {
|
||||||
|
"latest_news": "Últimas noticias:",
|
||||||
|
"preheader": "¡Nueva posición!",
|
||||||
|
"subject": "Nueva posición recibida",
|
||||||
|
"title": "Mensaje"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"commit_db": "Error SQL",
|
||||||
|
"impossible_value": "El valor \"$0\" no es posible para el campo \"$1\"",
|
||||||
|
"no_auth": "Sin autorización",
|
||||||
|
"unknown_field": "Campo \"$0\" desconocido"
|
||||||
|
},
|
||||||
|
"feed": {
|
||||||
|
"counter": "N.º $0",
|
||||||
|
"id": "ID Feed",
|
||||||
|
"last_update": "Última actualización de Spot",
|
||||||
|
"name": "Descripción",
|
||||||
|
"new": "Nuevo feed",
|
||||||
|
"plural": "Feeds",
|
||||||
|
"ref_id": "ID Feed ref.",
|
||||||
|
"status": "Estado"
|
||||||
|
},
|
||||||
|
"map": {
|
||||||
|
"ign_france": "IGN (Francia)",
|
||||||
|
"ign_spain": "IGN (España)",
|
||||||
|
"linz": "LINZ",
|
||||||
|
"natgeo": "National Geographic",
|
||||||
|
"otm": "Open Topo Map",
|
||||||
|
"outdoors": "Mapbox Topo",
|
||||||
|
"satellite": "Satélite",
|
||||||
|
"see_on_google": "Ver la posición en Google Maps",
|
||||||
|
"title": "Mapas de base",
|
||||||
|
"usgs": "USGS"
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"click_watch": "Haz clic para ver el vídeo",
|
||||||
|
"click_zoom": "Haz clic para ampliar",
|
||||||
|
"comment_update": "Comentario \"$0\" actualizado",
|
||||||
|
"image": "Foto",
|
||||||
|
"image_taken_on": "Foto tomada el $0",
|
||||||
|
"images": "Fotos",
|
||||||
|
"nearby": "Fotos cercanas",
|
||||||
|
"no_id": "Falta el ID del archivo multimedia",
|
||||||
|
"posted_on": "Añadido el $0",
|
||||||
|
"video": "Vídeo",
|
||||||
|
"video_taken_on": "Vídeo grabado el $0"
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"locale": "es_ES",
|
||||||
|
"page_og_desc": "Mantente en contacto conmigo durante mis aventuras en la montaña."
|
||||||
|
},
|
||||||
|
"newsletter": {
|
||||||
|
"email_exists": "Esta dirección de correo electrónico ya está registrada. Puedes darte de baja haciendo clic en el botón de arriba.",
|
||||||
|
"email_placeholder": "nombre@email.com",
|
||||||
|
"invalid_email": "Esto no parece una dirección de correo electrónico",
|
||||||
|
"subscribe": "Suscribirse",
|
||||||
|
"subscribed": "¡Gracias! Recibirás un correo electrónico de confirmación.",
|
||||||
|
"subscribed_desc": "Todo está listo. Te enviaré noticias frescas en cuanto las reciba. Prometido...",
|
||||||
|
"title": "Mantente en contacto",
|
||||||
|
"unknown_email": "Dirección de correo electrónico desconocida",
|
||||||
|
"unsubscribe": "Darse de baja",
|
||||||
|
"unsubscribed": "Listo. ¡No más spam!",
|
||||||
|
"unsubscribed_desc": "Añade tu dirección de correo electrónico y te enviaré mi posición actualizada tan pronto como la reciba :)"
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"copy_to_clipboard": "Copiar el enlace",
|
||||||
|
"link_copied": "¡Enlace copiado!",
|
||||||
|
"message": "Mensaje",
|
||||||
|
"name": "Nombre",
|
||||||
|
"new_message": "Mensaje nuevo"
|
||||||
|
},
|
||||||
|
"project": {
|
||||||
|
"wip": "En curso",
|
||||||
|
"code_name": "Nombre clave",
|
||||||
|
"end": "Fin",
|
||||||
|
"hikes": "Senderos",
|
||||||
|
"id": "ID del proyecto",
|
||||||
|
"mode": "Modo",
|
||||||
|
"modes": {
|
||||||
|
"blog": "Proyecto activo",
|
||||||
|
"histo": "Proyecto archivado",
|
||||||
|
"previz": "Proyecto en preparación"
|
||||||
|
},
|
||||||
|
"new": "Nuevo proyecto",
|
||||||
|
"overview": "Vista general",
|
||||||
|
"plural": "Proyectos",
|
||||||
|
"single": "Proyecto",
|
||||||
|
"start": "Inicio",
|
||||||
|
"update_messages": "Actualizar los mensajes del proyecto"
|
||||||
|
},
|
||||||
|
"spot": {
|
||||||
|
"id": "ID de Spot",
|
||||||
|
"model": "Modelo",
|
||||||
|
"name": "Spot",
|
||||||
|
"plural": "Spots",
|
||||||
|
"ref_id": "ID de referencia de Spot"
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"duration": "Duración",
|
||||||
|
"distance": "Distancia",
|
||||||
|
"elevation": "Elevación",
|
||||||
|
"elevation_gain": "Ascenso acumulado",
|
||||||
|
"elevation_loss": "Descenso acumulado",
|
||||||
|
"from": "Inicio",
|
||||||
|
"legend": "Leyenda",
|
||||||
|
"segment_length": "Tamaño del segmento",
|
||||||
|
"to": "Fin",
|
||||||
|
"type": "Tipo de sendero"
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"city": "Hora en $0",
|
||||||
|
"date_time": "$0 a las $1",
|
||||||
|
"local": "$0 hora local",
|
||||||
|
"user": "$0 en tu zona horaria",
|
||||||
|
"zone": "Huso horario"
|
||||||
|
},
|
||||||
|
"track": {
|
||||||
|
"download": "Descargar la ruta GPX",
|
||||||
|
"hitchhiking": "Autostop",
|
||||||
|
"main": "Camino principal",
|
||||||
|
"off-track": "Variante"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"day": "día",
|
||||||
|
"day_short": "D",
|
||||||
|
"days": "días",
|
||||||
|
"hour": "h"
|
||||||
|
},
|
||||||
|
"upload": {
|
||||||
|
"error": "Error al subir el archivo",
|
||||||
|
"media": {
|
||||||
|
"exists": "La imagen $0 ya existe",
|
||||||
|
"title": "Cargar fotos y vídeos"
|
||||||
|
},
|
||||||
|
"mode_archived": "El proyecto \"$0\" está archivado. No se puede cargar.",
|
||||||
|
"position": {
|
||||||
|
"new": "Nueva posición",
|
||||||
|
"title": "Subir posición"
|
||||||
|
},
|
||||||
|
"success": "$0 se ha subido correctamente."
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"active": "Usuarios activos",
|
||||||
|
"clearance": "Nivel de autorización",
|
||||||
|
"id": "ID del usuario",
|
||||||
|
"language": "Idioma",
|
||||||
|
"name": "Nombre"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"clear-day": "La nubosidad es inferior al 20 % durante el día",
|
||||||
|
"clear-night": "La nubosidad es inferior al 20 % durante la noche",
|
||||||
|
"cloudy": "La nubosidad es superior al 90 %",
|
||||||
|
"fog": "La visibilidad es baja (menos de 1km)",
|
||||||
|
"hail": "Chubascos de granizo",
|
||||||
|
"partly-cloudy-day": "La nubosidad es superior al 20 % durante el día",
|
||||||
|
"partly-cloudy-night": "La nubosidad es superior al 20 % durante la noche",
|
||||||
|
"rain": "La cantidad de lluvia es superior a cero",
|
||||||
|
"rain-snow": "Chubascos de nieve y lluvia",
|
||||||
|
"rain-snow-showers-day": "Posible lluvia/nieve durante todo el día",
|
||||||
|
"rain-snow-showers-night": "Posible lluvia/nieve durante toda la noche",
|
||||||
|
"showers-day": "Chubascos de lluvia durante el día",
|
||||||
|
"showers-night": "Chubascos de lluvia durante la noche",
|
||||||
|
"sleet": "Aguanieve",
|
||||||
|
"snow": "La cantidad de nieve es superior a cero",
|
||||||
|
"snow-showers-day": "Periodos de nieve durante el día",
|
||||||
|
"snow-showers-night": "Periodos de nieve durante la noche",
|
||||||
|
"thunder": "Tormentas",
|
||||||
|
"thunder-rain": "Tormentas durante el día o la noche",
|
||||||
|
"thunder-showers-day": "Posibles tormentas durante todo el día",
|
||||||
|
"thunder-showers-night": "Posibles tormentas durante toda la noche",
|
||||||
|
"wind": "La velocidad del viento es alta (más de 30 km/h)"
|
||||||
|
}
|
||||||
|
}
|
||||||
208
resources/lang/fr.json
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
{
|
||||||
|
"action": {
|
||||||
|
"back": "Retour",
|
||||||
|
"delete": "Supprimer",
|
||||||
|
"save": "Sauvegarder",
|
||||||
|
"send": "Envoyer"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"config": "Paramètres",
|
||||||
|
"create_success": "Créé",
|
||||||
|
"delete_success": "Supprimé",
|
||||||
|
"save_success": "Sauvegardé",
|
||||||
|
"title": "Administration",
|
||||||
|
"toolbox": "Boîte à outils",
|
||||||
|
"upload": "Téléverser"
|
||||||
|
},
|
||||||
|
"credits": {
|
||||||
|
"git": "Dépôt Git",
|
||||||
|
"license": "sous licence GPLv3",
|
||||||
|
"project": "Projet $0"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"confirmation": {
|
||||||
|
"body_1": "C'est gentil de venir voir où j'en suis. Promis, je te tiendrai au courant de mon avancée.",
|
||||||
|
"body_2": "En général, j'envoie un message une fois par jour. Lorsque je passe par des endroits sympas, j'en envoie un supplémentaire (sommets, ce genre de choses). J'utilise une balise GPS pour envoyer le signal, je n'ai donc pas besoin de réseau téléphonique pour que cela fonctionne. Cependant, il peut m'arriver d'oublier d'appuyer sur le bouton. Donc pas de raison de t'inquiéter si tu ne reçois pas de messages pendant une journée ou deux.",
|
||||||
|
"body_3": "Si j'ai ajouté des photos sur le site récemment, tu devrais aussi les retrouver dans cet e-mail.",
|
||||||
|
"conclusion": "À bientôt sur les chemins !",
|
||||||
|
"preheader": "Merci de rester en contact !",
|
||||||
|
"signature": "--François",
|
||||||
|
"subject": "Confirmation",
|
||||||
|
"thanks_subject": "C'est tout bon !"
|
||||||
|
},
|
||||||
|
"unsubscribe": "PS : Trop d'e-mails ?",
|
||||||
|
"unsubscribe_button": "Se désinscrire",
|
||||||
|
"update": {
|
||||||
|
"latest_news": "Dernières nouvelles :",
|
||||||
|
"preheader": "Nouvelle position !",
|
||||||
|
"subject": "Nouvelle position reçue",
|
||||||
|
"title": "Message"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"commit_db": "Erreur lors de la requête SQL",
|
||||||
|
"impossible_value": "La valeur \"$0\" n'est pas possible pour le champ \"$1\"",
|
||||||
|
"no_auth": "Pas d'autorisation",
|
||||||
|
"unknown_field": "Champ \"$0\" inconnu"
|
||||||
|
},
|
||||||
|
"feed": {
|
||||||
|
"counter": "N°$0",
|
||||||
|
"id": "ID feed",
|
||||||
|
"last_update": "Dernière vérification Spot",
|
||||||
|
"name": "Description",
|
||||||
|
"new": "Nouveau feed",
|
||||||
|
"plural": "Feeds",
|
||||||
|
"ref_id": "ID Feed ref.",
|
||||||
|
"status": "Statut"
|
||||||
|
},
|
||||||
|
"map": {
|
||||||
|
"ign_france": "IGN (France)",
|
||||||
|
"ign_spain": "IGN (Espagne)",
|
||||||
|
"linz": "LINZ",
|
||||||
|
"natgeo": "National Geographic",
|
||||||
|
"otm": "Open Topo Map",
|
||||||
|
"outdoors": "Mapbox Topo",
|
||||||
|
"satellite": "Satellite",
|
||||||
|
"see_on_google": "Voir la position sur Google Maps",
|
||||||
|
"title": "Fonds de carte",
|
||||||
|
"usgs": "USGS"
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"click_watch": "Cliquer pour voir la vidéo",
|
||||||
|
"click_zoom": "Cliquer pour zoomer",
|
||||||
|
"comment_update": "Commentaire du média \"$0\" mis à jour",
|
||||||
|
"image": "Photo",
|
||||||
|
"image_taken_on": "Prise le $0",
|
||||||
|
"images": "Photos",
|
||||||
|
"nearby": "Photos prises dans le coin",
|
||||||
|
"no_id": "ID du média manquant",
|
||||||
|
"posted_on": "Ajouté le $0",
|
||||||
|
"video": "Vidéo",
|
||||||
|
"video_taken_on": "Filmé le $0"
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"locale": "fr_CH",
|
||||||
|
"page_og_desc": "Garde le contact lorsque je suis sur les chemins."
|
||||||
|
},
|
||||||
|
"newsletter": {
|
||||||
|
"email_exists": "Cette adresse e-mail est déjà enregistrée. Tu peux te désinscrire en cliquant sur le bouton ci-dessus.",
|
||||||
|
"email_placeholder": "mon@email.com",
|
||||||
|
"invalid_email": "Ceci ne ressemble pas à une adresse e-mail",
|
||||||
|
"subscribe": "S'abonner",
|
||||||
|
"subscribed": "Merci ! Tu vas recevoir un e-mail de confirmation très bientôt.",
|
||||||
|
"subscribed_desc": "C'est tout bon. Je t'enverrai des nouvelles fraîches. Parole de scout.",
|
||||||
|
"title": "Rester en contact",
|
||||||
|
"unknown_email": "Adresse e-mail inconnue",
|
||||||
|
"unsubscribe": "Se désinscrire",
|
||||||
|
"unsubscribed": "C'est fait. Fini le spam !",
|
||||||
|
"unsubscribed_desc": "Ajoute ton adresse e-mail et je t'enverrai ma nouvelle position dès que je la trouve :)"
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"copy_to_clipboard": "Copier le lien dans le presse-papiers",
|
||||||
|
"link_copied": "Lien copié !",
|
||||||
|
"message": "Message",
|
||||||
|
"name": "Nom",
|
||||||
|
"new_message": "Nouveau message"
|
||||||
|
},
|
||||||
|
"project": {
|
||||||
|
"wip": "En cours",
|
||||||
|
"code_name": "Nom de code",
|
||||||
|
"end": "Arrivée",
|
||||||
|
"hikes": "Randonnées",
|
||||||
|
"id": "ID projet",
|
||||||
|
"mode": "Mode",
|
||||||
|
"modes": {
|
||||||
|
"blog": "Projet actif",
|
||||||
|
"histo": "Projet archivé",
|
||||||
|
"previz": "Projet en cours de préparation"
|
||||||
|
},
|
||||||
|
"new": "Nouveau projet",
|
||||||
|
"overview": "Vue d'ensemble",
|
||||||
|
"plural": "Projets",
|
||||||
|
"single": "Projet",
|
||||||
|
"start": "Départ",
|
||||||
|
"update_messages": "Mettre à jour les messages du projet"
|
||||||
|
},
|
||||||
|
"spot": {
|
||||||
|
"id": "ID Spot",
|
||||||
|
"model": "Modèle",
|
||||||
|
"name": "Spot",
|
||||||
|
"plural": "Spots",
|
||||||
|
"ref_id": "ID Spot ref."
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"duration": "Durée",
|
||||||
|
"distance": "Distance",
|
||||||
|
"elevation": "Dénivelé",
|
||||||
|
"elevation_gain": "Dénivelé positif",
|
||||||
|
"elevation_loss": "Dénivelé négatif",
|
||||||
|
"from": "Départ",
|
||||||
|
"legend": "Légende",
|
||||||
|
"segment_length": "Taille du segment",
|
||||||
|
"to": "Arrivée",
|
||||||
|
"type": "Type de rando"
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"city": "Heure à $0",
|
||||||
|
"date_time": "$0 à $1",
|
||||||
|
"local": "$0 heure locale",
|
||||||
|
"user": "$0 dans ton fuseau horaire",
|
||||||
|
"zone": "Fuseau horaire"
|
||||||
|
},
|
||||||
|
"track": {
|
||||||
|
"download": "Télécharger la trace GPX",
|
||||||
|
"hitchhiking": "Auto-stop",
|
||||||
|
"main": "Itinéraire",
|
||||||
|
"off-track": "Variante"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"day": "jour",
|
||||||
|
"day_short": "J",
|
||||||
|
"days": "jours",
|
||||||
|
"hour": "h"
|
||||||
|
},
|
||||||
|
"upload": {
|
||||||
|
"error": "Erreur lors du téléversement",
|
||||||
|
"media": {
|
||||||
|
"exists": "L'image $0 existe déjà",
|
||||||
|
"title": "Téléverser des photos et vidéos"
|
||||||
|
},
|
||||||
|
"mode_archived": "Le projet \"$0\" a été archivé. Aucun téléversement possible.",
|
||||||
|
"position": {
|
||||||
|
"new": "Nouvelle position",
|
||||||
|
"title": "Position supplémentaire"
|
||||||
|
},
|
||||||
|
"success": "$0 a été téléversé"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"active": "Utilisateurs actifs",
|
||||||
|
"clearance": "Niveau d'autorisation",
|
||||||
|
"id": "ID utilisateur",
|
||||||
|
"language": "Langue",
|
||||||
|
"name": "Nom"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"clear-day": "La couverture nuageuse est inférieure à 20 % pendant la journée",
|
||||||
|
"clear-night": "La couverture nuageuse est inférieure à 20 % pendant la nuit",
|
||||||
|
"cloudy": "La couverture nuageuse est supérieure à 90 %",
|
||||||
|
"fog": "La visibilité est faible (moins de 1km)",
|
||||||
|
"hail": "Averses de grêle",
|
||||||
|
"partly-cloudy-day": "La couverture nuageuse est supérieure à 20 % pendant la journée",
|
||||||
|
"partly-cloudy-night": "La couverture nuageuse est supérieure à 20 % pendant la nuit",
|
||||||
|
"rain": "La quantité de pluie est supérieure à zéro",
|
||||||
|
"rain-snow": "Averses de neige et de pluie",
|
||||||
|
"rain-snow-showers-day": "Pluie/neige possible tout au long de la journée",
|
||||||
|
"rain-snow-showers-night": "Pluie/neige possible tout au long de la nuit",
|
||||||
|
"showers-day": "Averses de pluie pendant la journée",
|
||||||
|
"showers-night": "Averses de pluie pendant la nuit",
|
||||||
|
"sleet": "Grésil",
|
||||||
|
"snow": "La quantité de neige est supérieure à zéro",
|
||||||
|
"snow-showers-day": "Périodes de neige pendant la journée",
|
||||||
|
"snow-showers-night": "Périodes de neige pendant la nuit",
|
||||||
|
"thunder": "Orages",
|
||||||
|
"thunder-rain": "Orages tout au long de la journée ou de la nuit",
|
||||||
|
"thunder-showers-day": "Orages possibles tout au long de la journée",
|
||||||
|
"thunder-showers-night": "Orages possibles tout au long de la nuit",
|
||||||
|
"wind": "La vitesse du vent est élevée (plus de 30 km/h)"
|
||||||
|
}
|
||||||
|
}
|
||||||
33
resources/masks/email.confirmation.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
|
||||||
|
<title>[#]lang:email.confirmation.subject[#]</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<span style="color: transparent; display: none !important; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">[#]lang:email.confirmation.preheader[#]</span>
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="width:100%;max-width:600px;">
|
||||||
|
<tr>
|
||||||
|
<td width="20%"><img src="[#]local_server[#]assets/images/icons/favicon-96x96.png" width="90%" border="0" alt="logo" /></td>
|
||||||
|
<td><h1>[#]lang:email.confirmation.thanks_subject[#]</h1></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">
|
||||||
|
<p align="justify">[#]lang:email.confirmation.body_1[#]</p>
|
||||||
|
<p align="justify">[#]lang:email.confirmation.body_2[#]</p>
|
||||||
|
<p align="justify">[#]lang:email.confirmation.body_3[#]</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">
|
||||||
|
<p>[#]lang:email.confirmation.conclusion[#]<br />[#]lang:email.confirmation.signature[#]</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">
|
||||||
|
<p>[#]lang:email.unsubscribe[#] <a href="[#]unsubscribe_link[#]" target="_blank" rel="noopener">[#]lang:email.unsubscribe_button[#]</a></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -2,14 +2,14 @@
|
|||||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
<head>
|
<head>
|
||||||
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
|
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
|
||||||
<title>[#]lang:email_update_subject[#]</title>
|
<title>[#]lang:email.update.subject[#]</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<span style="color: transparent; display: none !important; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">[#]lang:update_preheader[#]</span>
|
<span style="color: transparent; display: none !important; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">[#]lang:email.update.preheader[#]</span>
|
||||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="width:100%;max-width:600px;">
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="width:100%;max-width:600px;">
|
||||||
<tr>
|
<tr>
|
||||||
<td width="20%"><img src="[#]local_server[#]images/icons/mstile-144x144.png" width="90%" border="0" alt="logo" /></td>
|
<td width="20%"><img src="[#]local_server[#]assets/images/icons/favicon-96x96.png" width="90%" border="0" alt="logo" /></td>
|
||||||
<td><h1>[#]lang:update_title[#] [#]type[#] #[#]displayed_id[#]</h1></td>
|
<td><h1>[#]lang:email.update.title[#] [#]type[#] #[#]displayed_id[#]</h1></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<h2>[#]lang:update_latest_news[#]</h2>
|
<h2>[#]lang:email.update.latest_news[#]</h2>
|
||||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
<!-- [PART] news [START] -->
|
<!-- [PART] news [START] -->
|
||||||
<tr>
|
<tr>
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<p>[#]lang:email_unsubscribe[#] <a href="[#]unsubscribe_link[#]" target="_blank" rel="noopener">[#]lang:email_unsub_btn[#]</a></p>
|
<p>[#]lang:email.unsubscribe[#] <a href="[#]unsubscribe_link[#]" target="_blank" rel="noopener">[#]lang:email.unsubscribe_button[#]</a></p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
29
resources/masks/index.html
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="[#]language[#]">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="description" content="[#]lang:meta.page_og_desc[#]">
|
||||||
|
<meta property="og:title" content="[#]title[#]" />
|
||||||
|
<meta property="og:description" content="[#]lang:meta.page_og_desc[#]" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:url" content="[#]server[#]" />
|
||||||
|
<meta property="og:image" content="assets/images/icons/ogp.svg?v=20260528" />
|
||||||
|
<meta property="og:locale" content="[#]lang:meta.locale[#]" />
|
||||||
|
<link rel="icon" type="image/png" href="assets/images/icons/favicon-96x96.png?v=20260528" sizes="96x96" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="assets/images/icons/favicon.svg?v=20260528" />
|
||||||
|
<link rel="shortcut icon" href="assets/images/icons/favicon.ico?v=20260528" />
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="assets/images/icons/apple-touch-icon.png?v=20260528" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="[#]title[#]" />
|
||||||
|
<link rel="manifest" href="assets/images/icons/site.webmanifest?v=20260528" />
|
||||||
|
<meta name="theme-color" content="#081B19">
|
||||||
|
<script id="app-config" type="application/json">[#]app_config[#]</script>
|
||||||
|
<title>[#]title[#]</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="container"></div>
|
||||||
|
<!-- [PART] entrypoint [START] -->
|
||||||
|
<script defer src="[#]filename[#]"></script>
|
||||||
|
<!-- [PART] entrypoint [END] -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2
script/d3.min.js
vendored
2
script/jquery.min.js
vendored
23
script/leaflet.min.js
vendored
@@ -1,738 +0,0 @@
|
|||||||
/*!
|
|
||||||
* Lightbox v2.11.4
|
|
||||||
* by Lokesh Dhakar
|
|
||||||
*
|
|
||||||
* More info:
|
|
||||||
* http://lokeshdhakar.com/projects/lightbox2/
|
|
||||||
*
|
|
||||||
* Copyright Lokesh Dhakar
|
|
||||||
* Released under the MIT license
|
|
||||||
* https://github.com/lokesh/lightbox2/blob/master/LICENSE
|
|
||||||
*
|
|
||||||
* @preserve
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Uses Node, AMD or browser globals to create a module.
|
|
||||||
(function (root, factory) {
|
|
||||||
if (typeof define === 'function' && define.amd) {
|
|
||||||
// AMD. Register as an anonymous module.
|
|
||||||
define(['jquery'], factory);
|
|
||||||
} else if (typeof exports === 'object') {
|
|
||||||
// Node. Does not work with strict CommonJS, but
|
|
||||||
// only CommonJS-like environments that support module.exports,
|
|
||||||
// like Node.
|
|
||||||
module.exports = factory(require('jquery'));
|
|
||||||
} else {
|
|
||||||
// Browser globals (root is window)
|
|
||||||
root.lightbox = factory(root.jQuery);
|
|
||||||
}
|
|
||||||
}(this, function ($) {
|
|
||||||
|
|
||||||
function Lightbox(options) {
|
|
||||||
this.album = [];
|
|
||||||
this.currentImageIndex = void 0;
|
|
||||||
this.init();
|
|
||||||
|
|
||||||
// options
|
|
||||||
this.options = $.extend({}, this.constructor.defaults);
|
|
||||||
this.option(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Descriptions of all options available on the demo site:
|
|
||||||
// http://lokeshdhakar.com/projects/lightbox2/index.html#options
|
|
||||||
Lightbox.defaults = {
|
|
||||||
albumLabel: 'Image %1 of %2',
|
|
||||||
alwaysShowNavOnTouchDevices: false,
|
|
||||||
fadeDuration: 600,
|
|
||||||
fitImagesInViewport: true,
|
|
||||||
imageFadeDuration: 600,
|
|
||||||
positionFromTop: 50,
|
|
||||||
resizeDuration: 700,
|
|
||||||
showImageNumberLabel: true,
|
|
||||||
wrapAround: false,
|
|
||||||
disableScrolling: false,
|
|
||||||
/*
|
|
||||||
Sanitize Title
|
|
||||||
If the caption data is trusted, for example you are hardcoding it in, then leave this to false.
|
|
||||||
This will free you to add html tags, such as links, in the caption.
|
|
||||||
|
|
||||||
If the caption data is user submitted or from some other untrusted source, then set this to true
|
|
||||||
to prevent xss and other injection attacks.
|
|
||||||
*/
|
|
||||||
sanitizeTitle: false
|
|
||||||
, hasVideo: true
|
|
||||||
, onMediaChange: (oMedia) => {}
|
|
||||||
};
|
|
||||||
|
|
||||||
Lightbox.prototype.option = function(options) {
|
|
||||||
$.extend(this.options, options);
|
|
||||||
};
|
|
||||||
|
|
||||||
Lightbox.prototype.imageCountLabel = function(currentImageNum, totalImages) {
|
|
||||||
return this.options.albumLabel.replace(/%1/g, currentImageNum).replace(/%2/g, totalImages);
|
|
||||||
};
|
|
||||||
|
|
||||||
Lightbox.prototype.init = function() {
|
|
||||||
var self = this;
|
|
||||||
// Both enable and build methods require the body tag to be in the DOM.
|
|
||||||
$(document).ready(function() {
|
|
||||||
self.enable();
|
|
||||||
self.build();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Loop through anchors and areamaps looking for either data-lightbox attributes or rel attributes
|
|
||||||
// that contain 'lightbox'. When these are clicked, start lightbox.
|
|
||||||
Lightbox.prototype.enable = function() {
|
|
||||||
var self = this;
|
|
||||||
$('body').on('click', 'a[rel^=lightbox], area[rel^=lightbox], a[data-lightbox], area[data-lightbox]', function(event) {
|
|
||||||
self.start($(event.currentTarget));
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build html for the lightbox and the overlay.
|
|
||||||
// Attach event handlers to the new DOM elements. click click click
|
|
||||||
Lightbox.prototype.build = function() {
|
|
||||||
if ($('#lightbox').length > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
// The two root notes generated, #lightboxOverlay and #lightbox are given
|
|
||||||
// tabindex attrs so they are focusable. We attach our keyboard event
|
|
||||||
// listeners to these two elements, and not the document. Clicking anywhere
|
|
||||||
// while Lightbox is opened will keep the focus on or inside one of these
|
|
||||||
// two elements.
|
|
||||||
//
|
|
||||||
// We do this so we can prevent propogation of the Esc keypress when
|
|
||||||
// Lightbox is open. This prevents it from intefering with other components
|
|
||||||
// on the page below.
|
|
||||||
//
|
|
||||||
// Github issue: https://github.com/lokesh/lightbox2/issues/663
|
|
||||||
$('\
|
|
||||||
<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="" />\
|
|
||||||
<div class="lb-nav">\
|
|
||||||
<div class="lb-prev-area">\
|
|
||||||
<a class="lb-prev" aria-label="Previous image" href="" role="button"></a>\
|
|
||||||
</div>\
|
|
||||||
<div class="lb-next-area">\
|
|
||||||
<a class="lb-next" aria-label="Next image" href="" role="button"></a>\
|
|
||||||
</div>\
|
|
||||||
</div>\
|
|
||||||
<div class="lb-loader">\
|
|
||||||
<a class="lb-cancel" href="#"></a>\
|
|
||||||
</div>\
|
|
||||||
</div>\
|
|
||||||
</div>\
|
|
||||||
<div class="lb-dataContainer desktop">\
|
|
||||||
<div class="lb-data">\
|
|
||||||
<div class="lb-details">\
|
|
||||||
<span class="lb-caption"></span>\
|
|
||||||
<span class="lb-number"></span>\
|
|
||||||
</div>\
|
|
||||||
<div class="lb-closeContainer">\
|
|
||||||
<a class="lb-close" role="button"></a>\
|
|
||||||
</div>\
|
|
||||||
</div>\
|
|
||||||
</div>\
|
|
||||||
').appendTo($('body'));
|
|
||||||
|
|
||||||
// Cache jQuery objects
|
|
||||||
this.$lightbox = $('#lightbox');
|
|
||||||
this.$overlay = $('#lightboxOverlay');
|
|
||||||
this.$outerContainer = this.$lightbox.find('.lb-outerContainer');
|
|
||||||
this.$container = this.$lightbox.find('.lb-container');
|
|
||||||
this.$image = this.$lightbox.find('.lb-image');
|
|
||||||
this.$nav = this.$lightbox.find('.lb-nav');
|
|
||||||
|
|
||||||
if(self.options.hasVideo) {
|
|
||||||
this.$video = $('<video class="lb-video" controls autoplay></video>');
|
|
||||||
this.$image.after(this.$video);
|
|
||||||
this.videoBorderWidth = {
|
|
||||||
top: parseInt(this.$video.css('border-top-width'), 10),
|
|
||||||
right: parseInt(this.$video.css('border-right-width'), 10),
|
|
||||||
bottom: parseInt(this.$video.css('border-bottom-width'), 10),
|
|
||||||
left: parseInt(this.$video.css('border-left-width'), 10)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store css values for future lookup
|
|
||||||
this.containerPadding = {
|
|
||||||
top: parseInt(this.$container.css('padding-top'), 10),
|
|
||||||
right: parseInt(this.$container.css('padding-right'), 10),
|
|
||||||
bottom: parseInt(this.$container.css('padding-bottom'), 10),
|
|
||||||
left: parseInt(this.$container.css('padding-left'), 10)
|
|
||||||
};
|
|
||||||
|
|
||||||
this.imageBorderWidth = {
|
|
||||||
top: parseInt(this.$image.css('border-top-width'), 10),
|
|
||||||
right: parseInt(this.$image.css('border-right-width'), 10),
|
|
||||||
bottom: parseInt(this.$image.css('border-bottom-width'), 10),
|
|
||||||
left: parseInt(this.$image.css('border-left-width'), 10)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Attach event handlers to the newly minted DOM elements
|
|
||||||
this.$overlay.hide().add(this.$lightbox.find('.lb-dataContainer')).on('click', function() {
|
|
||||||
self.end();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$lightbox.hide().on('click', function(event) {
|
|
||||||
if ($(event.target).attr('id') === 'lightbox') {
|
|
||||||
self.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$outerContainer.on('click', function(event) {
|
|
||||||
if ($(event.target).attr('id') === 'lightbox') {
|
|
||||||
self.end();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$lightbox.find('.lb-prev').on('click', function() {
|
|
||||||
if (self.currentImageIndex === 0) {
|
|
||||||
self.changeImage(self.album.length - 1);
|
|
||||||
} else {
|
|
||||||
self.changeImage(self.currentImageIndex - 1);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$lightbox.find('.lb-next').on('click', function() {
|
|
||||||
if (self.currentImageIndex === self.album.length - 1) {
|
|
||||||
self.changeImage(0);
|
|
||||||
} else {
|
|
||||||
self.changeImage(self.currentImageIndex + 1);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
Show context menu for image on right-click
|
|
||||||
|
|
||||||
There is a div containing the navigation that spans the entire image and lives above of it. If
|
|
||||||
you right-click, you are right clicking this div and not the image. This prevents users from
|
|
||||||
saving the image or using other context menu actions with the image.
|
|
||||||
|
|
||||||
To fix this, when we detect the right mouse button is pressed down, but not yet clicked, we
|
|
||||||
set pointer-events to none on the nav div. This is so that the upcoming right-click event on
|
|
||||||
the next mouseup will bubble down to the image. Once the right-click/contextmenu event occurs
|
|
||||||
we set the pointer events back to auto for the nav div so it can capture hover and left-click
|
|
||||||
events as usual.
|
|
||||||
*/
|
|
||||||
this.$nav.on('mousedown', function(event) {
|
|
||||||
if (event.which === 3) {
|
|
||||||
self.$nav.css('pointer-events', 'none');
|
|
||||||
|
|
||||||
self.$lightbox.one('contextmenu', function() {
|
|
||||||
setTimeout(function() {
|
|
||||||
this.$nav.css('pointer-events', 'auto');
|
|
||||||
}.bind(self), 0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
this.$lightbox.find('.lb-loader, .lb-close').on('click keyup', function(e) {
|
|
||||||
// If mouse click OR 'enter' or 'space' keypress, close LB
|
|
||||||
if (
|
|
||||||
e.type === 'click' || (e.type === 'keyup' && (e.which === 13 || e.which === 32))) {
|
|
||||||
self.end();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Show overlay and lightbox. If the image is part of a set, add siblings to album array.
|
|
||||||
Lightbox.prototype.start = function($link) {
|
|
||||||
var self = this;
|
|
||||||
var $window = $(window);
|
|
||||||
|
|
||||||
$window.on('resize', $.proxy(this.sizeOverlay, this));
|
|
||||||
|
|
||||||
this.sizeOverlay();
|
|
||||||
|
|
||||||
//Manage Zoom Event
|
|
||||||
this.$nav.mousewheel((e) => {
|
|
||||||
var asImg = self.album[this.currentImageIndex];
|
|
||||||
if(!asImg.type != 'video') {
|
|
||||||
asTransform = this.$image.css('transform').replace(/[^0-9\-.,]/g, '').split(',');
|
|
||||||
var fOldZoom = parseFloat(asTransform[0] || 1);
|
|
||||||
var fOldTranslateX = parseFloat(asTransform[4] || 0);
|
|
||||||
var fOldTranslateY = parseFloat(asTransform[5] || 0);
|
|
||||||
var fOldZoom = parseFloat(asTransform[0] || 1);
|
|
||||||
var fNewZoom = Math.min(Math.max(fOldZoom + e.deltaY / 10, 1), Math.max(asImg.width/this.$image.width(), asImg.height/this.$image.height()));
|
|
||||||
|
|
||||||
var fTransX = fOldTranslateX + (fNewZoom - fOldZoom) * (this.$image.width()/2 - e.offsetX);
|
|
||||||
var fTransY = fOldTranslateY + (fNewZoom - fOldZoom) * (this.$image.height()/2 - e.offsetY);
|
|
||||||
var fTransMaxX = (fNewZoom - 1) * this.$image.width() / 2;
|
|
||||||
var fTransMaxY = (fNewZoom - 1) * this.$image.height() / 2;
|
|
||||||
|
|
||||||
fTransX = Math.max(Math.min(fTransX, fTransMaxX), fTransMaxX * -1);
|
|
||||||
fTransY = Math.max(Math.min(fTransY, fTransMaxY), fTransMaxY * -1);
|
|
||||||
|
|
||||||
this.$image.css('--scale', fNewZoom);
|
|
||||||
this.$container.toggleClass('moveable', (fNewZoom > 1));
|
|
||||||
this.$image.css('--translate-x', fTransX+'px');
|
|
||||||
this.$image.css('--translate-y', fTransY+'px');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//Manage Repositioning Event
|
|
||||||
this.$nav.on('mousedown', (e) => {
|
|
||||||
if(this.$image.css('--scale') > 1) {
|
|
||||||
//The following block gets the X/Y offset (the difference between where it starts and where it was clicked)
|
|
||||||
this.gMouseDownOffsetX = e.clientX - parseFloat(this.$image.css('--translate-x') || 0);
|
|
||||||
this.gMouseDownOffsetY = e.clientY - parseFloat(this.$image.css('--translate-y') || 0);
|
|
||||||
|
|
||||||
//Change cursor
|
|
||||||
this.$container.addClass('moving');
|
|
||||||
|
|
||||||
$window.on('mousemove', divMove);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$window.on('mouseup', () => {
|
|
||||||
$window.off('mousemove', divMove);
|
|
||||||
this.$container.removeClass('moving');
|
|
||||||
});
|
|
||||||
|
|
||||||
function divMove(e){
|
|
||||||
let iZoom = self.$image.css('--scale');
|
|
||||||
let fTransX = e.clientX - self.gMouseDownOffsetX;
|
|
||||||
let fTransY = e.clientY - self.gMouseDownOffsetY;
|
|
||||||
let fTransMaxX = (iZoom - 1) * self.$image.width() / 2;
|
|
||||||
let fTransMaxY = (iZoom - 1) * self.$image.height() / 2;
|
|
||||||
|
|
||||||
fTransX = Math.max(Math.min(fTransX, fTransMaxX), fTransMaxX * -1);
|
|
||||||
fTransY = Math.max(Math.min(fTransY, fTransMaxY), fTransMaxY * -1);
|
|
||||||
|
|
||||||
self.$image.css('--translate-x', fTransX + 'px');
|
|
||||||
self.$image.css('--translate-y', fTransY + 'px');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.album = [];
|
|
||||||
var imageNumber = 0;
|
|
||||||
|
|
||||||
// Support both data-lightbox attribute and rel attribute implementations
|
|
||||||
var dataLightboxValue = $link.attr('data-lightbox');
|
|
||||||
var $links;
|
|
||||||
|
|
||||||
if (dataLightboxValue) {
|
|
||||||
$links = $($link.prop('tagName') + '[data-lightbox="' + dataLightboxValue + '"]');
|
|
||||||
for (var i = 0; i < $links.length; i = ++i) {
|
|
||||||
this.addToAlbum($($links[i]));
|
|
||||||
if ($links[i] === $link[0]) {
|
|
||||||
imageNumber = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ($link.attr('rel') === 'lightbox') {
|
|
||||||
// If image is not part of a set
|
|
||||||
this.addToAlbum($link);
|
|
||||||
} else {
|
|
||||||
// If image is part of a set
|
|
||||||
$links = $($link.prop('tagName') + '[rel="' + $link.attr('rel') + '"]');
|
|
||||||
for (var j = 0; j < $links.length; j = ++j) {
|
|
||||||
this.addToAlbum($($links[j]));
|
|
||||||
if ($links[j] === $link[0]) {
|
|
||||||
imageNumber = j;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Position Lightbox
|
|
||||||
this.$lightbox.fadeIn(this.options.fadeDuration);
|
|
||||||
|
|
||||||
// Disable scrolling of the page while open
|
|
||||||
if (this.options.disableScrolling) {
|
|
||||||
$('body').addClass('lb-disable-scrolling');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.changeImage(imageNumber);
|
|
||||||
};
|
|
||||||
|
|
||||||
Lightbox.prototype.addToAlbum = function($link) {
|
|
||||||
this.album.push({
|
|
||||||
alt: $link.attr('data-alt'),
|
|
||||||
link: $link.attr('href'),
|
|
||||||
title: $link.attr('data-title') || $link.attr('title'),
|
|
||||||
|
|
||||||
orientation: $link.attr('data-orientation'),
|
|
||||||
type: $link.attr('data-type'),
|
|
||||||
id: $link.attr('data-id'),
|
|
||||||
$Media: $link.attr('data-type')=='video'?this.$video:this.$image,
|
|
||||||
width: $link.find('img').attr('width'),
|
|
||||||
height: $link.find('img').attr('height'),
|
|
||||||
set: $link.attr('data-lightbox') || $link.attr('rel')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Lightbox.prototype.getMaxSizes = function(iMediaWidth, iMediaHeight, sMediaType) {
|
|
||||||
var iWindowWidth = $(window).width();
|
|
||||||
var iWindowHeight = $(window).height();
|
|
||||||
var oBorder = (sMediaType=='image')?this.imageBorderWidth:this.videoBorderWidth;
|
|
||||||
var iMaxMediaWidth = iWindowWidth - this.containerPadding.left - this.containerPadding.right - oBorder.left - oBorder.right;
|
|
||||||
var iMaxMediaHeight = iWindowHeight - this.containerPadding.top - this.containerPadding.bottom - oBorder.top - oBorder.bottom - this.options.positionFromTop;
|
|
||||||
|
|
||||||
var iDataMaxWidth = this.$lightbox.find('.lb-dataContainer').width(), iDataMaxHeight = this.$lightbox.find('.lb-dataContainer').height();
|
|
||||||
var iImageRatio = iMediaWidth / iMediaHeight;
|
|
||||||
|
|
||||||
//Case horizontal
|
|
||||||
var iHeightH = Math.min(iMaxMediaHeight, iMediaHeight);
|
|
||||||
var iWidthH = Math.min(iHeightH * iImageRatio, iMaxMediaWidth - iDataMaxWidth);
|
|
||||||
var iSurfaceH = Math.min(iHeightH, iWidthH / iImageRatio) * iWidthH;
|
|
||||||
|
|
||||||
//Case vertical
|
|
||||||
var iWidthV = Math.min(iMaxMediaWidth, iMediaWidth);
|
|
||||||
var iHeightV = Math.min(iWidthV / iImageRatio, iMaxMediaHeight - iDataMaxHeight);
|
|
||||||
var iSurfaceV = Math.min(iWidthV, iHeightV * iImageRatio) * iHeightV;
|
|
||||||
|
|
||||||
var sDirection = (iSurfaceV > iSurfaceH)?'vertical':'horizontal';
|
|
||||||
|
|
||||||
if(sDirection == 'vertical') iMaxMediaHeight -= iDataMaxHeight;
|
|
||||||
else iMaxMediaWidth -= iDataMaxWidth;
|
|
||||||
|
|
||||||
return {maxWidth: iMaxMediaWidth, maxHeight: iMaxMediaHeight, direction: sDirection};
|
|
||||||
};
|
|
||||||
|
|
||||||
Lightbox.prototype.updateSize = function(iMediaNumber) {
|
|
||||||
var oMedia = this.album[iMediaNumber];
|
|
||||||
var sFileType = oMedia.link.split('.').slice(-1)[0];
|
|
||||||
var oMaxSizes = this.getMaxSizes(oMedia.width, oMedia.height, oMedia.type);
|
|
||||||
var iMaxMediaWidth = oMaxSizes.maxWidth;
|
|
||||||
var iMaxMediaHeight = oMaxSizes.maxHeight;
|
|
||||||
this.$lightbox.removeClass('vertical horizontal').addClass(oMaxSizes.direction);
|
|
||||||
|
|
||||||
/*
|
|
||||||
Since many SVGs have small intrinsic dimensions, but they support scaling
|
|
||||||
up without quality loss because of their vector format, max out their
|
|
||||||
size.
|
|
||||||
*/
|
|
||||||
if(sFileType === 'svg') {
|
|
||||||
oMedia.$Media.width(iMaxMediaWidth);
|
|
||||||
oMedia.$Media.height(iMaxMediaHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.options.fitImagesInViewport) {
|
|
||||||
//Check if image size is larger than maxWidth|maxHeight in settings
|
|
||||||
if(this.options.maxWidth && this.options.maxWidth < iMaxMediaWidth) iMaxMediaWidth = this.options.maxWidth;
|
|
||||||
if(this.options.maxHeight && this.options.maxHeight < iMaxMediaHeight) iMaxMediaHeight = this.options.maxHeight;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
iMaxMediaWidth = this.options.maxWidth || oMedia.width || iMaxMediaWidth;
|
|
||||||
iMaxMediaHeight = this.options.maxHeight || oMedia.height || iMaxMediaHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Is the current image's width or height is greater than the maxImageWidth or maxImageHeight
|
|
||||||
//option than we need to size down while maintaining the aspect ratio.
|
|
||||||
var iMediaFinalWidth, iMediaFinalHeight;
|
|
||||||
if((oMedia.width > iMaxMediaWidth) || (oMedia.height > iMaxMediaHeight)) {
|
|
||||||
if ((oMedia.width / iMaxMediaWidth) > (oMedia.height / iMaxMediaHeight)) {
|
|
||||||
iMediaFinalWidth = iMaxMediaWidth;
|
|
||||||
iMediaFinalHeight = Math.round(oMedia.height / (oMedia.width / iMaxMediaWidth));
|
|
||||||
} else {
|
|
||||||
iMediaFinalWidth = Math.round(oMedia.width / (oMedia.height / iMaxMediaHeight));
|
|
||||||
iMediaFinalHeight = iMaxMediaHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
iMediaFinalWidth = oMedia.width;
|
|
||||||
iMediaFinalHeight = oMedia.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
oMedia.$Media.width(iMediaFinalWidth);
|
|
||||||
oMedia.$Media.height(iMediaFinalHeight);
|
|
||||||
this.sizeContainer(iMediaFinalWidth, iMediaFinalHeight, oMedia.type);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Hide most UI elements in preparation for the animated resizing of the lightbox.
|
|
||||||
Lightbox.prototype.changeImage = function(imageNumber) {
|
|
||||||
var self = this;
|
|
||||||
var filename = this.album[imageNumber].link;
|
|
||||||
|
|
||||||
// Disable keyboard nav during transitions
|
|
||||||
this.disableKeyboardNav();
|
|
||||||
|
|
||||||
// Show loading state
|
|
||||||
this.$overlay.fadeIn(this.options.fadeDuration);
|
|
||||||
$('.lb-loader').fadeIn('slow');
|
|
||||||
|
|
||||||
this.$lightbox.find('.lb-image, .lb-video, .lb-nav, .lb-prev, .lb-next, .lb-number, .lb-caption, .lb-close').hide();
|
|
||||||
this.$image.css({'--scale': '1', '--translate-x': '0', '--translate-y': '0'});
|
|
||||||
self.$lightbox.find('.lb-dataContainer').css({width:'200px', height:'30px'});
|
|
||||||
this.$outerContainer.addClass('animating');
|
|
||||||
this.$container.removeClass('moveable moving');
|
|
||||||
|
|
||||||
this.options.onMediaChange(self.album[imageNumber]);
|
|
||||||
|
|
||||||
var $hasVideoNav = this.$container.hasClass('lb-video-nav');
|
|
||||||
switch(self.album[imageNumber].type) {
|
|
||||||
case 'video':
|
|
||||||
this.$video.on('loadedmetadata', function(){
|
|
||||||
self.album[imageNumber].width = this.videoWidth;
|
|
||||||
self.album[imageNumber].height = this.videoHeight;
|
|
||||||
self.updateSize(imageNumber);
|
|
||||||
$(this).off('loadedmetadata');
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$video.attr('src', filename);
|
|
||||||
|
|
||||||
if(!$hasVideoNav) this.$container.addClass('lb-video-nav');
|
|
||||||
break;
|
|
||||||
case 'image':
|
|
||||||
this.$video.attr('src', '');
|
|
||||||
if($hasVideoNav) this.$container.removeClass('lb-video-nav');
|
|
||||||
|
|
||||||
// When image to show is preloaded, we send the width and height to sizeContainer()
|
|
||||||
var preloader = new Image();
|
|
||||||
preloader.onload = function(){
|
|
||||||
self.$image.attr({
|
|
||||||
'alt': self.album[imageNumber].alt,
|
|
||||||
'src': filename
|
|
||||||
});
|
|
||||||
|
|
||||||
//Orientation management
|
|
||||||
if(Math.abs(self.album[imageNumber].orientation) == 90 && preloader.width > preloader.height) {
|
|
||||||
var sWidth = preloader.width;
|
|
||||||
preloader.width = preloader.height;
|
|
||||||
preloader.height = sWidth;
|
|
||||||
}
|
|
||||||
self.album[imageNumber].width = preloader.width;
|
|
||||||
self.album[imageNumber].height = preloader.height;
|
|
||||||
|
|
||||||
self.updateSize(imageNumber);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Preload image before showing
|
|
||||||
preloader.src = this.album[imageNumber].link;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentImageIndex = imageNumber;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Stretch overlay to fit the viewport
|
|
||||||
Lightbox.prototype.sizeOverlay = function(e) {
|
|
||||||
/*
|
|
||||||
We use a setTimeout 0 to pause JS execution and let the rendering catch-up.
|
|
||||||
Why do this? If the `disableScrolling` option is set to true, a class is added to the body
|
|
||||||
tag that disables scrolling and hides the scrollbar. We want to make sure the scrollbar is
|
|
||||||
hidden before we measure the document width, as the presence of the scrollbar will affect the
|
|
||||||
number.
|
|
||||||
*/
|
|
||||||
if(e) {
|
|
||||||
if(typeof oResizeTimer != 'undefined') clearTimeout(oResizeTimer);
|
|
||||||
oResizeTimer = setTimeout(
|
|
||||||
() => {
|
|
||||||
switch(this.album[this.currentImageIndex].type) {
|
|
||||||
case 'image':
|
|
||||||
this.changeImage(this.currentImageIndex);
|
|
||||||
break;
|
|
||||||
case 'video':
|
|
||||||
this.updateSize(this.currentImageIndex);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
200
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Animate the size of the lightbox to fit the image we are showing
|
|
||||||
// This method also shows the the image.
|
|
||||||
//ADDED-START
|
|
||||||
//Lightbox.prototype.sizeContainer = function(imageWidth, imageHeight) {
|
|
||||||
Lightbox.prototype.sizeContainer = function(imageWidth, imageHeight, media) {
|
|
||||||
media = media || 'image';
|
|
||||||
//ADDED-END
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
var oldWidth = this.$outerContainer.outerWidth();
|
|
||||||
var oldHeight = this.$outerContainer.outerHeight();
|
|
||||||
//ADDED-START
|
|
||||||
//var newWidth = imageWidth + this.containerPadding.left + this.containerPadding.right + this.imageBorderWidth.left + this.imageBorderWidth.right;
|
|
||||||
//var newHeight = imageHeight + this.containerPadding.top + this.containerPadding.bottom + this.imageBorderWidth.top + this.imageBorderWidth.bottom;
|
|
||||||
var mediaBorderWidth = (media=='image')?this.imageBorderWidth:this.videoBorderWidth;
|
|
||||||
var newWidth = imageWidth + this.containerPadding.left + this.containerPadding.right + mediaBorderWidth.left + mediaBorderWidth.right;
|
|
||||||
var newHeight = imageHeight + this.containerPadding.top + this.containerPadding.bottom + mediaBorderWidth.top + mediaBorderWidth.bottom;
|
|
||||||
//ADDED-END
|
|
||||||
|
|
||||||
function postResize() {
|
|
||||||
if(self.$lightbox.hasClass('vertical')) self.$lightbox.find('.lb-dataContainer').width(newWidth);
|
|
||||||
else self.$lightbox.find('.lb-dataContainer').height(newHeight);
|
|
||||||
self.$lightbox.find('.lb-prevLink').height(newHeight);
|
|
||||||
self.$lightbox.find('.lb-nextLink').height(newHeight);
|
|
||||||
|
|
||||||
// Set focus on one of the two root nodes so keyboard events are captured.
|
|
||||||
self.$overlay.trigger('focus');
|
|
||||||
|
|
||||||
self.showImage();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldWidth !== newWidth || oldHeight !== newHeight) {
|
|
||||||
this.$outerContainer.animate({
|
|
||||||
width: newWidth,
|
|
||||||
height: newHeight
|
|
||||||
}, this.options.resizeDuration, 'swing', function() {
|
|
||||||
postResize();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
postResize();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Display the image and its details and begin preload neighboring images.
|
|
||||||
Lightbox.prototype.showImage = function() {
|
|
||||||
this.$lightbox.find('.lb-loader').stop(true).hide();
|
|
||||||
|
|
||||||
if(this.options.hasVideo && this.album[this.currentImageIndex].type == 'video') this.$lightbox.find('.lb-video').fadeIn(this.options.imageFadeDuration);
|
|
||||||
else this.$lightbox.find('.lb-image').fadeIn(this.options.imageFadeDuration);
|
|
||||||
|
|
||||||
this.updateNav();
|
|
||||||
this.updateDetails();
|
|
||||||
this.preloadNeighboringImages();
|
|
||||||
this.enableKeyboardNav();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Display previous and next navigation if appropriate.
|
|
||||||
Lightbox.prototype.updateNav = function() {
|
|
||||||
// Check to see if the browser supports touch events. If so, we take the conservative approach
|
|
||||||
// and assume that mouse hover events are not supported and always show prev/next navigation
|
|
||||||
// arrows in image sets.
|
|
||||||
var alwaysShowNav = false;
|
|
||||||
try {
|
|
||||||
document.createEvent('TouchEvent');
|
|
||||||
alwaysShowNav = (this.options.alwaysShowNavOnTouchDevices) ? true : false;
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
this.$lightbox.find('.lb-nav').show();
|
|
||||||
|
|
||||||
if (this.album.length > 1) {
|
|
||||||
if (this.options.wrapAround) {
|
|
||||||
if (alwaysShowNav) {
|
|
||||||
this.$lightbox.find('.lb-prev, .lb-next').css('opacity', '1');
|
|
||||||
}
|
|
||||||
this.$lightbox.find('.lb-prev, .lb-next').show();
|
|
||||||
} else {
|
|
||||||
if (this.currentImageIndex > 0) {
|
|
||||||
this.$lightbox.find('.lb-prev').show();
|
|
||||||
if (alwaysShowNav) {
|
|
||||||
this.$lightbox.find('.lb-prev').css('opacity', '1');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.currentImageIndex < this.album.length - 1) {
|
|
||||||
this.$lightbox.find('.lb-next').show();
|
|
||||||
if (alwaysShowNav) {
|
|
||||||
this.$lightbox.find('.lb-next').css('opacity', '1');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Display caption, image number, and closing button.
|
|
||||||
Lightbox.prototype.updateDetails = function() {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
// Enable anchor clicks in the injected caption html.
|
|
||||||
// Thanks Nate Wright for the fix. @https://github.com/NateWr
|
|
||||||
if (typeof this.album[this.currentImageIndex].title !== 'undefined' &&
|
|
||||||
this.album[this.currentImageIndex].title !== '') {
|
|
||||||
var $caption = this.$lightbox.find('.lb-caption');
|
|
||||||
if (this.options.sanitizeTitle) {
|
|
||||||
$caption.text(this.album[this.currentImageIndex].title);
|
|
||||||
} else {
|
|
||||||
$caption.html(this.album[this.currentImageIndex].title);
|
|
||||||
}
|
|
||||||
$caption.add(this.$lightbox.find('.lb-close')).fadeIn('fast');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$outerContainer.removeClass('animating');
|
|
||||||
|
|
||||||
this.$lightbox.find('.lb-dataContainer').fadeIn(this.options.resizeDuration, function() {
|
|
||||||
return self.sizeOverlay();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Preload previous and next images in set.
|
|
||||||
Lightbox.prototype.preloadNeighboringImages = function() {
|
|
||||||
if (this.album.length > this.currentImageIndex + 1 && this.album[this.currentImageIndex + 1].type == 'image') {
|
|
||||||
var preloadNext = new Image();
|
|
||||||
preloadNext.src = this.album[this.currentImageIndex + 1].link;
|
|
||||||
}
|
|
||||||
if (this.currentImageIndex > 0 && this.album[this.currentImageIndex - 1].type == 'image') {
|
|
||||||
var preloadPrev = new Image();
|
|
||||||
preloadPrev.src = this.album[this.currentImageIndex - 1].link;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Lightbox.prototype.enableKeyboardNav = function() {
|
|
||||||
this.disableKeyboardNav();
|
|
||||||
this.$lightbox.on('keyup.keyboard', $.proxy(this.keyboardAction, this));
|
|
||||||
this.$overlay.on('keyup.keyboard', $.proxy(this.keyboardAction, this));
|
|
||||||
};
|
|
||||||
|
|
||||||
Lightbox.prototype.disableKeyboardNav = function() {
|
|
||||||
this.$lightbox.off('.keyboard');
|
|
||||||
this.$overlay.off('.keyboard');
|
|
||||||
};
|
|
||||||
|
|
||||||
Lightbox.prototype.keyboardAction = function(event) {
|
|
||||||
var KEYCODE_ESC = 27;
|
|
||||||
var KEYCODE_LEFTARROW = 37;
|
|
||||||
var KEYCODE_RIGHTARROW = 39;
|
|
||||||
|
|
||||||
var keycode = event.keyCode;
|
|
||||||
if (keycode === KEYCODE_ESC) {
|
|
||||||
// Prevent bubbling so as to not affect other components on the page.
|
|
||||||
event.stopPropagation();
|
|
||||||
this.end();
|
|
||||||
} else if (keycode === KEYCODE_LEFTARROW) {
|
|
||||||
if (this.currentImageIndex !== 0) {
|
|
||||||
this.changeImage(this.currentImageIndex - 1);
|
|
||||||
} else if (this.options.wrapAround && this.album.length > 1) {
|
|
||||||
this.changeImage(this.album.length - 1);
|
|
||||||
}
|
|
||||||
} else if (keycode === KEYCODE_RIGHTARROW) {
|
|
||||||
if (this.currentImageIndex !== this.album.length - 1) {
|
|
||||||
this.changeImage(this.currentImageIndex + 1);
|
|
||||||
} else if (this.options.wrapAround && this.album.length > 1) {
|
|
||||||
this.changeImage(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Closing time. :-(
|
|
||||||
Lightbox.prototype.end = function() {
|
|
||||||
this.disableKeyboardNav();
|
|
||||||
|
|
||||||
if(this.options.hasVideo) {
|
|
||||||
var $lbContainer = this.$lightbox.find('.lb-container');
|
|
||||||
var $hasVideoNav = $lbContainer.hasClass('lb-video-nav');
|
|
||||||
this.$video.attr('src', '');
|
|
||||||
|
|
||||||
if($hasVideoNav) $lbContainer.removeClass('lb-video-nav');
|
|
||||||
}
|
|
||||||
oSpot.flushHash();
|
|
||||||
|
|
||||||
$(window).off('resize', this.sizeOverlay);
|
|
||||||
this.$nav.off('mousewheel');
|
|
||||||
this.$lightbox.fadeOut(this.options.fadeDuration);
|
|
||||||
this.$overlay.fadeOut(this.options.fadeDuration);
|
|
||||||
|
|
||||||
if (this.options.disableScrolling) {
|
|
||||||
$('body').removeClass('lb-disable-scrolling');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Lightbox();
|
|
||||||
}));
|
|
||||||
468
script/spot.js
@@ -1,468 +0,0 @@
|
|||||||
function Spot(asGlobals)
|
|
||||||
{
|
|
||||||
self = this;
|
|
||||||
this.consts = asGlobals.consts;
|
|
||||||
this.consts.hash_sep = '-';
|
|
||||||
this.consts.title = 'Spotty';
|
|
||||||
this.consts.default_page = 'project';
|
|
||||||
this.consts.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || this.consts.default_timezone;
|
|
||||||
|
|
||||||
/* Initialization */
|
|
||||||
|
|
||||||
this.init = function()
|
|
||||||
{
|
|
||||||
//Variables & constants from php
|
|
||||||
self.vars('tmp', 'object');
|
|
||||||
self.vars('page', 'string');
|
|
||||||
self.updateVars(asGlobals.vars);
|
|
||||||
|
|
||||||
//page elem
|
|
||||||
self.elem = {};
|
|
||||||
self.elem.container = $('#container');
|
|
||||||
self.elem.main = $('#main');
|
|
||||||
|
|
||||||
self.resetTmpFunctions();
|
|
||||||
|
|
||||||
//On Key down
|
|
||||||
$('html').on('keydown', function(oEvent){self.onKeydown(oEvent);});
|
|
||||||
|
|
||||||
//on window resize
|
|
||||||
$(window).on('resize', function(){self.onResize();});
|
|
||||||
|
|
||||||
//Setup menu
|
|
||||||
//self.initMenu();
|
|
||||||
|
|
||||||
//Hash management
|
|
||||||
$(window)
|
|
||||||
.bind('hashchange', self.onHashChange)
|
|
||||||
.trigger('hashchange');
|
|
||||||
};
|
|
||||||
|
|
||||||
this.updateVars = function(asVars)
|
|
||||||
{
|
|
||||||
$.each(asVars, function(sKey, oValue){self.vars(sKey, oValue)});
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Variable Management */
|
|
||||||
|
|
||||||
this.vars = function(oVarName, oValue)
|
|
||||||
{
|
|
||||||
var asVarName = (typeof oVarName == 'object')?oVarName:[oVarName];
|
|
||||||
|
|
||||||
//Set, name & type / default value (init)
|
|
||||||
if(typeof oValue !== 'undefined') setElem(self.vars, copyArray(asVarName), oValue);
|
|
||||||
|
|
||||||
//Get, only name parameter
|
|
||||||
return getElem(self.vars, asVarName);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.tmp = function(sVarName, oValue)
|
|
||||||
{
|
|
||||||
var asVarName = (typeof sVarName == 'object')?sVarName:[sVarName];
|
|
||||||
asVarName.unshift('tmp');
|
|
||||||
return self.vars(asVarName, oValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Interface with server */
|
|
||||||
|
|
||||||
this.get = function(sAction, fOnSuccess, oVars, fOnError, fonProgress)
|
|
||||||
{
|
|
||||||
if(!oVars) oVars = {};
|
|
||||||
fOnError = fOnError || function(sError) {console.log(sError);};
|
|
||||||
fonProgress = fonProgress || function(sState){};
|
|
||||||
fonProgress('start');
|
|
||||||
|
|
||||||
oVars['a'] = sAction;
|
|
||||||
oVars['t'] = self.consts.timezone;
|
|
||||||
return $.ajax(
|
|
||||||
{
|
|
||||||
url: self.consts.process_page,
|
|
||||||
data: oVars,
|
|
||||||
dataType: 'json'
|
|
||||||
})
|
|
||||||
.done(function(oData)
|
|
||||||
{
|
|
||||||
fonProgress('done');
|
|
||||||
if(oData.desc.substr(0, self.consts.lang_prefix.length)==self.consts.lang_prefix) oData.desc = self.lang(oData.desc.substr(5));
|
|
||||||
|
|
||||||
if(oData.result==self.consts.error) fOnError(oData.desc);
|
|
||||||
else fOnSuccess(oData.data, oData.desc);
|
|
||||||
})
|
|
||||||
.fail(function(jqXHR, textStatus, errorThrown)
|
|
||||||
{
|
|
||||||
fonProgress('fail');
|
|
||||||
fOnError(textStatus+' '+errorThrown);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.lang = function(sKey, asParams) {
|
|
||||||
var sParamType = $.type(asParams);
|
|
||||||
if(sParamType == 'undefined') asParams = [];
|
|
||||||
else if($.type(asParams) != 'array') asParams = [asParams];
|
|
||||||
var sLang = '';
|
|
||||||
|
|
||||||
if(sKey in self.consts.lang) {
|
|
||||||
sLang = self.consts.lang[sKey];
|
|
||||||
for(i in asParams) sLang = sLang.replace('$'+i, asParams[i]);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.log('missing translation: '+sKey);
|
|
||||||
sLang = sKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sLang;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Page Switch - Trigger & Event catching */
|
|
||||||
|
|
||||||
this.onHashChange = function()
|
|
||||||
{
|
|
||||||
var asHash = self.getHash();
|
|
||||||
if(asHash.hash !='' && asHash.page != '') self.switchPage(asHash); //page switching
|
|
||||||
else if(self.vars('page')=='') self.setHash(self.consts.default_page); //first page
|
|
||||||
};
|
|
||||||
|
|
||||||
this.getHash = function()
|
|
||||||
{
|
|
||||||
var sHash = self.hash();
|
|
||||||
var asHash = sHash.split(self.consts.hash_sep);
|
|
||||||
var sPage = asHash.shift() || '';
|
|
||||||
return {hash:sHash, page:sPage, items:asHash};
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setHash = function(sPage, asItems, bReboot)
|
|
||||||
{
|
|
||||||
bReboot = bReboot || false;
|
|
||||||
sPage = sPage || '';
|
|
||||||
asItems = asItems || [];
|
|
||||||
if(typeof asItems == 'string') asItems = [asItems];
|
|
||||||
if(sPage != '')
|
|
||||||
{
|
|
||||||
var sItems = (asItems.length > 0)?self.consts.hash_sep+asItems.join(self.consts.hash_sep):'';
|
|
||||||
self.hash(sPage+sItems, bReboot);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.hash = function(hash, bReboot)
|
|
||||||
{
|
|
||||||
bReboot = bReboot || false;
|
|
||||||
if(!hash) return window.location.hash.slice(1);
|
|
||||||
else window.location.hash = '#'+hash;
|
|
||||||
|
|
||||||
if(bReboot) location.reload();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.updateHash = function(sType, iId) {
|
|
||||||
sType = sType || '';
|
|
||||||
iId = iId || 0;
|
|
||||||
|
|
||||||
var asHash = self.getHash();
|
|
||||||
if(iId) self.setHash(asHash.page, [asHash.items[0], sType, iId]);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.flushHash = function(asTypes) {
|
|
||||||
asTypes = asTypes || [];
|
|
||||||
var asHash = self.getHash();
|
|
||||||
if(asHash.items.length > 1 && (asTypes.length == 0 || asTypes.indexOf(asHash.items[1]) != -1)) self.setHash(asHash.page, [asHash.items[0]]);
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Page Switch - DOM Replacement */
|
|
||||||
|
|
||||||
this.getActionLink = function(sAction, oVars)
|
|
||||||
{
|
|
||||||
if(!oVars) oVars = {};
|
|
||||||
sVars = '';
|
|
||||||
for(i in oVars)
|
|
||||||
{
|
|
||||||
sVars += '&'+i+'='+oVars[i];
|
|
||||||
}
|
|
||||||
return self.consts.process_page+'?a='+sAction+sVars;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.resetTmpFunctions = function()
|
|
||||||
{
|
|
||||||
self.pageInit = function(asHash){console.log('no init for the page: '+asHash.page)};
|
|
||||||
self.onSamePageMove = function(asHash){return false};
|
|
||||||
self.onQuitPage = function(){return true};
|
|
||||||
self.onResize = function(){};
|
|
||||||
self.onFeedback = function(sType, sMsg){};
|
|
||||||
self.onKeydown = function(oEvent){};
|
|
||||||
};
|
|
||||||
|
|
||||||
this.switchPage = function(asHash)
|
|
||||||
{
|
|
||||||
var sPageName = asHash.page;
|
|
||||||
var bSamePage = (self.vars('page') == sPageName);
|
|
||||||
var bFirstPage = (self.vars('page') == '');
|
|
||||||
|
|
||||||
if(!self.consts.pages[sPageName]) { //Page does not exist
|
|
||||||
if(bFirstPage) self.setHash(self.consts.default_page);
|
|
||||||
else self.setHash(self.vars('page'), self.vars(['hash', 'items']));
|
|
||||||
}
|
|
||||||
else if(self.onQuitPage(bSamePage) && !bSamePage || self.onSamePageMove(asHash))
|
|
||||||
{
|
|
||||||
//Delete tmp variables
|
|
||||||
self.vars('tmp', {});
|
|
||||||
|
|
||||||
//disable tmp functions
|
|
||||||
self.resetTmpFunctions();
|
|
||||||
|
|
||||||
//Officially a new page
|
|
||||||
self.vars('page', sPageName);
|
|
||||||
self.vars('hash', asHash);
|
|
||||||
|
|
||||||
//Update Page Title
|
|
||||||
this.setPageTitle(sPageName+' '+(asHash.items[0] || ''));
|
|
||||||
|
|
||||||
//Replacing DOM
|
|
||||||
var $Dom = $(self.consts.pages[sPageName]);
|
|
||||||
if(bFirstPage)
|
|
||||||
{
|
|
||||||
self.splash($Dom, asHash, bFirstPage); //first page
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
self.elem.main.stop().fadeTo('fast', 0, function(){self.splash($Dom, asHash, bFirstPage);}); //Switching page
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if(bSamePage) self.vars('hash', asHash);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setPageTitle = function(sTitle) {
|
|
||||||
document.title = self.consts.title+' - '+sTitle;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.splash = function($Dom, asHash, bFirstPage)
|
|
||||||
{
|
|
||||||
//Switch main content
|
|
||||||
self.elem.main.empty().html($Dom);
|
|
||||||
|
|
||||||
//Page Bootstrap
|
|
||||||
self.pageInit(asHash, bFirstPage);
|
|
||||||
|
|
||||||
//Show main
|
|
||||||
var $FadeInElem = bFirstPage?self.elem.container:self.elem.main;
|
|
||||||
$FadeInElem.hide().fadeTo('slow', 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.getNaturalDuration = function(iHours) {
|
|
||||||
var iTimeMinutes = 0, iTimeHours = 0, iTimeDays = Math.floor(iHours/8); //8 hours a day
|
|
||||||
if(iTimeDays > 1) iTimeDays = Math.round(iTimeDays * 2) / 2; //Round down to the closest half day
|
|
||||||
else {
|
|
||||||
iTimeDays = 0;
|
|
||||||
iTimeHours = Math.floor(iHours);
|
|
||||||
iHours -= iTimeHours;
|
|
||||||
|
|
||||||
iTimeMinutes = Math.floor(iHours * 4) * 15; //Round down to the closest 15 minutes
|
|
||||||
}
|
|
||||||
return '~ '
|
|
||||||
+(iTimeDays>0?(iTimeDays+(iTimeDays%2==0?'':'½')+' '+self.lang(iTimeDays>1?'unit_days':'unit_day')):'') //Days
|
|
||||||
+((iTimeHours>0 || iTimeDays==0)?iTimeHours+self.lang('unit_hour'):'') //Hours
|
|
||||||
+((iTimeDays>0 || iTimeMinutes==0)?'':iTimeMinutes) //Minutes
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
this.checkClearance = function(sClearance) {
|
|
||||||
return (self.vars(['user', 'clearance']) >= sClearance);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Common Functions */
|
|
||||||
|
|
||||||
function copyArray(asArray)
|
|
||||||
{
|
|
||||||
return asArray.slice(0); //trick to copy array
|
|
||||||
}
|
|
||||||
|
|
||||||
function getElem(aoAnchor, asPath)
|
|
||||||
{
|
|
||||||
return (typeof asPath == 'object' && asPath.length > 1)?getElem(aoAnchor[asPath.shift()], asPath):aoAnchor[(typeof asPath == 'object')?asPath.shift():asPath];
|
|
||||||
}
|
|
||||||
|
|
||||||
function setElem(aoAnchor, asPath, oValue)
|
|
||||||
{
|
|
||||||
var asTypes = {boolean:false, string:'', integer:0, int:0, array:[], object:{}};
|
|
||||||
if(typeof asPath == 'object' && asPath.length > 1)
|
|
||||||
{
|
|
||||||
var nextlevel = asPath.shift();
|
|
||||||
if(!(nextlevel in aoAnchor)) aoAnchor[nextlevel] = {}; //Creating a new level
|
|
||||||
if(typeof aoAnchor[nextlevel] !== 'object') debug('Error - setElem() : Already existing path at level "'+nextlevel+'". Cancelling setElem() action');
|
|
||||||
return setElem(aoAnchor[nextlevel], asPath, oValue);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var sKey = (typeof asPath == 'object')?asPath.shift():asPath;
|
|
||||||
return aoAnchor[sKey] = (!(sKey in aoAnchor) && (oValue in asTypes))?asTypes[oValue]:oValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$.prototype.addInput = function(sType, sName, sValue, aoEvents)
|
|
||||||
{
|
|
||||||
aoEvents = aoEvents || [];
|
|
||||||
var $Input = $('<input>', {type: sType, name: sName, value: sValue}).data('old_value', sValue);
|
|
||||||
$.each(aoEvents, function(iIndex, aoEvent) {
|
|
||||||
$Input.on(aoEvent.on, aoEvent.callback);
|
|
||||||
});
|
|
||||||
return $(this).append($Input);
|
|
||||||
};
|
|
||||||
|
|
||||||
$.prototype.addButton = function(sIcon, sText, sName, fOnClick, sClass)
|
|
||||||
{
|
|
||||||
sText = sText || '';
|
|
||||||
sClass = sClass || '';
|
|
||||||
var $Btn = $('<button>', {name: sName, 'class':sClass})
|
|
||||||
.addIcon('fa-'+sIcon, (sText != ''))
|
|
||||||
.append(sText)
|
|
||||||
.click(fOnClick);
|
|
||||||
|
|
||||||
return $(this).append($Btn);
|
|
||||||
};
|
|
||||||
|
|
||||||
$.prototype.addIcon = function(sIcon, bMargin, sStyle)
|
|
||||||
{
|
|
||||||
bMargin = bMargin || false;
|
|
||||||
sStyle = sStyle || '';
|
|
||||||
return $(this).append($('<i>', {'class':'fa'+sStyle+' '+sIcon+(bMargin?' push':'')}));
|
|
||||||
};
|
|
||||||
|
|
||||||
$.prototype.defaultVal = function(sDefaultValue)
|
|
||||||
{
|
|
||||||
$(this)
|
|
||||||
.data('default_value', sDefaultValue)
|
|
||||||
.val(sDefaultValue)
|
|
||||||
.addClass('defaultText')
|
|
||||||
.focus(function()
|
|
||||||
{
|
|
||||||
var $This = $(this);
|
|
||||||
if($This.val() == $This.data('default_value')) $This.val('').removeClass('defaultText');
|
|
||||||
})
|
|
||||||
.blur(function()
|
|
||||||
{
|
|
||||||
var $This = $(this);
|
|
||||||
if($This.val() == '') $This.val($This.data('default_value')).addClass('defaultText');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$.prototype.checkForm = function(sSelector)
|
|
||||||
{
|
|
||||||
sSelector = sSelector || 'input[type="text"], textarea';
|
|
||||||
var $This = $(this);
|
|
||||||
var bOk = true;
|
|
||||||
$This.find(sSelector).each(function()
|
|
||||||
{
|
|
||||||
$This = $(this);
|
|
||||||
bOk = bOk && $This.val()!='' && $This.val()!=$This.data('default_value');
|
|
||||||
});
|
|
||||||
return bOk;
|
|
||||||
};
|
|
||||||
|
|
||||||
$.prototype.cascadingDown = function(sDuration)
|
|
||||||
{
|
|
||||||
return $(this).slideDown(sDuration, function(){$(this).next().cascadingDown(sDuration);});
|
|
||||||
};
|
|
||||||
|
|
||||||
$.prototype.hoverSwap = function(sDefault, sHover)
|
|
||||||
{
|
|
||||||
return $(this)
|
|
||||||
.data('default', sDefault)
|
|
||||||
.data('hover', sHover)
|
|
||||||
.hover(function(){
|
|
||||||
var $This = $(this),
|
|
||||||
sHover = $This.data('hover');
|
|
||||||
sDefault = $This.data('default');
|
|
||||||
|
|
||||||
if(sDefault!='' && sHover != '') {
|
|
||||||
$This.fadeOut('fast', function() {
|
|
||||||
var $This = $(this);
|
|
||||||
$This.text((sDefault==$This.text())?sHover:sDefault).fadeIn('fast');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.text(sDefault);
|
|
||||||
};
|
|
||||||
|
|
||||||
$.prototype.onSwipe = function(fOnStart, fOnMove, fOnEnd){
|
|
||||||
return $(this)
|
|
||||||
.on('dragstart', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
})
|
|
||||||
.on('mousedown touchstart', (e) => {
|
|
||||||
var $This = $(this);
|
|
||||||
var oPos = getDragPosition(e);
|
|
||||||
$This.data('x-start', oPos.x);
|
|
||||||
$This.data('y-start', oPos.y);
|
|
||||||
$This.data('x-move', oPos.x);
|
|
||||||
$This.data('y-move', oPos.y);
|
|
||||||
$This.data('moving', true).addClass('moving');
|
|
||||||
fOnStart({
|
|
||||||
xStart: $This.data('x-start'),
|
|
||||||
yStart: $This.data('y-start')
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.on('touchmove mousemove', (e) => {
|
|
||||||
var $This = $(this);
|
|
||||||
if($This.data('moving')) {
|
|
||||||
var oPos = getDragPosition(e);
|
|
||||||
$This.data('x-move', oPos.x);
|
|
||||||
$This.data('y-move', oPos.y);
|
|
||||||
fOnMove({
|
|
||||||
xStart: $This.data('x-start'),
|
|
||||||
yStart: $This.data('y-start'),
|
|
||||||
xMove: $This.data('x-move'),
|
|
||||||
yMove: $This.data('y-move')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on('mouseup mouseleave touchend', (e) => {
|
|
||||||
var $This = $(this);
|
|
||||||
if($This.data('moving')) {
|
|
||||||
$This.data('moving', false).removeClass('moving');
|
|
||||||
fOnEnd({
|
|
||||||
xStart: $This.data('x-start'),
|
|
||||||
yStart: $This.data('y-start'),
|
|
||||||
xEnd: $This.data('x-move'),
|
|
||||||
yEnd: $This.data('y-move')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function getDragPosition(oEvent) {
|
|
||||||
let bMouse = oEvent.type.includes('mouse');
|
|
||||||
return {
|
|
||||||
x: bMouse?oEvent.pageX:oEvent.touches[0].clientX,
|
|
||||||
y: bMouse?oEvent.pageY:oEvent.touches[0].clientY
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyTextToClipboard(text) {
|
|
||||||
if(!navigator.clipboard) {
|
|
||||||
var textArea = document.createElement('textarea');
|
|
||||||
textArea.value = text;
|
|
||||||
|
|
||||||
// Avoid scrolling to bottom
|
|
||||||
textArea.style.top = '0';
|
|
||||||
textArea.style.left = '0';
|
|
||||||
textArea.style.position = 'fixed';
|
|
||||||
|
|
||||||
document.body.appendChild(textArea);
|
|
||||||
textArea.focus();
|
|
||||||
textArea.select();
|
|
||||||
|
|
||||||
try {
|
|
||||||
var successful = document.execCommand('copy');
|
|
||||||
if(!successful) console.error('Fallback: Oops, unable to copy', text);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Fallback: Oops, unable to copy', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.removeChild(textArea);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
navigator.clipboard.writeText(text).then(
|
|
||||||
function() {},
|
|
||||||
function(err) {
|
|
||||||
console.error('Async: Could not copy text: ', err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
114
src/App.vue
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
<script>
|
||||||
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
import Project from '@components/project';
|
||||||
|
|
||||||
|
const aoRoutes = {
|
||||||
|
'project': Project, //Merge app.js and project.js calls to avoid extra http request on inital page
|
||||||
|
'admin': defineAsyncComponent(() => import(/* webpackChunkName: "admin" */ '@components/admin')),
|
||||||
|
'upload': defineAsyncComponent(() => import(/* webpackChunkName: "upload" */ '@components/upload'))
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hash: {page: '', items: [], prev: {}},
|
||||||
|
consts: this.appConfig.consts,
|
||||||
|
mobile: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
provide() {
|
||||||
|
return {
|
||||||
|
hash: this.hash,
|
||||||
|
consts: this.consts,
|
||||||
|
isMobile: () => this.isMobile(),
|
||||||
|
getAnchor: this.getAnchor,
|
||||||
|
getPrevAnchor: () => this.getPrevAnchor(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
route() {
|
||||||
|
return aoRoutes[this.hash.page];
|
||||||
|
},
|
||||||
|
hashSnapshot() {
|
||||||
|
const { prev, ...asHash } = this.hash;
|
||||||
|
return JSON.stringify(asHash);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
inject: ['appConfig'],
|
||||||
|
created() {
|
||||||
|
this.mobileMediaQuery = window.matchMedia('only screen and (max-width: 800px)');
|
||||||
|
this.mobileMediaQuery.addEventListener('change', this.updateMobile);
|
||||||
|
this.updateMobile();
|
||||||
|
|
||||||
|
//Set initial page
|
||||||
|
this.setVarHash(this.validateRoute(this.getBrowserHash()));
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
//Catch browser hash change
|
||||||
|
window.addEventListener('hashchange', this.onBrowserHashChange);
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
hashSnapshot(jNewHash, jOldHash) {
|
||||||
|
const asNewHash = this.validateRoute(JSON.parse(jNewHash));
|
||||||
|
|
||||||
|
//Sync variable -> #hash
|
||||||
|
if(asNewHash != this.getBrowserHash()) {
|
||||||
|
this.setBrowserHash(asNewHash.page, asNewHash.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Store previous value
|
||||||
|
this.hash.prev = JSON.parse(jOldHash);
|
||||||
|
|
||||||
|
this.setPageTitle(asNewHash.page);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isMobile() {
|
||||||
|
return this.mobile;
|
||||||
|
},
|
||||||
|
updateMobile() {
|
||||||
|
this.mobile = this.mobileMediaQuery.matches;
|
||||||
|
},
|
||||||
|
setPageTitle(sTitle) {
|
||||||
|
document.title = this.consts.title + ' - ' + sTitle.trim();
|
||||||
|
},
|
||||||
|
setVarHash(asHash) {
|
||||||
|
this.hash.page = asHash.page || '';
|
||||||
|
this.hash.items = Array.isArray(asHash.items) ? [...asHash.items.filter(n => n)] : [];
|
||||||
|
},
|
||||||
|
onBrowserHashChange() { //Sync #hash -> variable
|
||||||
|
let asHash = this.getBrowserHash();
|
||||||
|
if(asHash != this.hash) this.setVarHash(asHash);
|
||||||
|
},
|
||||||
|
getBrowserHash() {
|
||||||
|
let sHash = window.location.hash.slice(1);
|
||||||
|
let asHash = sHash.split(this.consts.hash_sep).filter(n => n);
|
||||||
|
let sPage = asHash.shift() || '';
|
||||||
|
return {page: sPage, items: asHash};
|
||||||
|
},
|
||||||
|
setBrowserHash(sPage = '', asItems = []) {
|
||||||
|
if(typeof asItems == 'string' && asItems != '') asItems = [asItems];
|
||||||
|
window.location.hash = this.getAnchor([sPage, ...asItems]);
|
||||||
|
},
|
||||||
|
getAnchor(asBreadCrumbs) {
|
||||||
|
return '#' + asBreadCrumbs.filter(Boolean).join(this.consts.hash_sep);
|
||||||
|
},
|
||||||
|
getPrevAnchor() {
|
||||||
|
return this.getAnchor([this.hash.prev.page, ...this.hash.prev.items]);
|
||||||
|
},
|
||||||
|
validateRoute(asHash) {
|
||||||
|
if(!Object.keys(aoRoutes).includes(asHash.page)) asHash.page = this.consts.default_page;
|
||||||
|
return asHash;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
window.removeEventListener('hashchange', this.onBrowserHashChange);
|
||||||
|
this.mobileMediaQuery.removeEventListener('change', this.updateMobile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div id="main">
|
||||||
|
<component :is="route" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
37
src/app.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
//Librairies
|
||||||
|
import Api from '@scripts/api';
|
||||||
|
import Lang from '@scripts/lang';
|
||||||
|
import Projects from '@scripts/projects';
|
||||||
|
import User from '@scripts/user';
|
||||||
|
import { createApp, reactive } from 'vue';
|
||||||
|
|
||||||
|
//Main template
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
//Style
|
||||||
|
import Css from '@styles/spot';
|
||||||
|
|
||||||
|
//App Configuration from PHP
|
||||||
|
const appConfig = JSON.parse(document.getElementById('app-config').textContent);
|
||||||
|
|
||||||
|
//Instances
|
||||||
|
const oProjects = new Projects(appConfig.projects);
|
||||||
|
const oUser = reactive(new User(appConfig.user, appConfig.consts.default_timezone));
|
||||||
|
const oLang = new Lang({translations: appConfig.consts.lang, prefix: appConfig.consts.lang_prefix});
|
||||||
|
const oApi = new Api({
|
||||||
|
server: appConfig.consts.server,
|
||||||
|
processPage: appConfig.consts.process_page,
|
||||||
|
timezone: oUser.timezone,
|
||||||
|
csrfToken: appConfig.consts.csrf_token,
|
||||||
|
errorCode: appConfig.consts.error,
|
||||||
|
lang: oLang
|
||||||
|
});
|
||||||
|
|
||||||
|
//Mount app
|
||||||
|
const oApp = createApp(App);
|
||||||
|
oApp.provide('appConfig', appConfig);
|
||||||
|
oApp.provide('api', oApi);
|
||||||
|
oApp.provide('lang', oLang);
|
||||||
|
oApp.provide('projects', oProjects);
|
||||||
|
oApp.provide('user', oUser);
|
||||||
|
oApp.mount('#container');
|
||||||
231
src/components/admin.vue
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
<script>
|
||||||
|
import SpotIcon from '@components/spotIcon';
|
||||||
|
import SpotButton from '@components/spotButton';
|
||||||
|
import AdminInput from '@components/adminInput';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
SpotIcon,
|
||||||
|
SpotButton,
|
||||||
|
AdminInput
|
||||||
|
},
|
||||||
|
inject: ['api', 'lang', 'getPrevAnchor'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
elems: {},
|
||||||
|
feedbacks: [],
|
||||||
|
saveTimer: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
if(this.saveTimer) clearTimeout(this.saveTimer);
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.setProjects();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
l(id) {
|
||||||
|
return this.lang.get(id);
|
||||||
|
},
|
||||||
|
addFeedback(sType, sMsg, asContext = {}) {
|
||||||
|
delete asContext.a;
|
||||||
|
delete asContext.t;
|
||||||
|
sMsg += ' (';
|
||||||
|
for(const [sKey, sElem] of Object.entries(asContext)) {
|
||||||
|
sMsg += sKey+'='+sElem+' / ' ;
|
||||||
|
}
|
||||||
|
sMsg = sMsg.slice(0, -3)+')';
|
||||||
|
|
||||||
|
this.feedbacks.push({type:sType, msg:sMsg});
|
||||||
|
},
|
||||||
|
async setProjects() {
|
||||||
|
let aoElemTypes = await this.api.get('admin_get');
|
||||||
|
|
||||||
|
for(const [sType, aoElems] of Object.entries(aoElemTypes)) {
|
||||||
|
this.elems[sType] = {};
|
||||||
|
for(const [iKey, oElem] of Object.entries(aoElems)) {
|
||||||
|
oElem.type = sType;
|
||||||
|
this.elems[sType][oElem.id] = oElem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createElem(sType) {
|
||||||
|
this.api.post('admin_create', {type: sType})
|
||||||
|
.then((aoNewElemTypes) => {
|
||||||
|
for(const [sType, aoNewElems] of Object.entries(aoNewElemTypes)) {
|
||||||
|
for(const [iKey, oNewElem] of Object.entries(aoNewElems)) {
|
||||||
|
oNewElem.type = sType;
|
||||||
|
this.elems[sType][oNewElem.id] = oNewElem;
|
||||||
|
this.addFeedback('success', this.l('admin.create_success'), {'create':sType});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((sMsg) => {this.addFeedback('error', sMsg, {'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) {
|
||||||
|
if(this.saveTimer) clearTimeout(this.saveTimer);
|
||||||
|
|
||||||
|
let sOldVal = this.elems[oElem.type][oElem.id][oEvent.target.name];
|
||||||
|
let sNewVal = oEvent.target.value;
|
||||||
|
if(sOldVal != sNewVal) {
|
||||||
|
let asInputs = {
|
||||||
|
type: oElem.type,
|
||||||
|
id: oElem.id,
|
||||||
|
field: oEvent.target.name,
|
||||||
|
value: sNewVal
|
||||||
|
};
|
||||||
|
|
||||||
|
this.api.post('admin_set', asInputs)
|
||||||
|
.then((asData) => {
|
||||||
|
this.elems[oElem.type][oElem.id][oEvent.target.name] = sNewVal;
|
||||||
|
this.addFeedback('success', this.l('admin.save_success'), asInputs);
|
||||||
|
})
|
||||||
|
.catch((sError) => {
|
||||||
|
oEvent.target.value = sOldVal;
|
||||||
|
this.addFeedback('error', sError, asInputs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
queue(oElem, oEvent) {
|
||||||
|
if(this.saveTimer) clearTimeout(this.saveTimer);
|
||||||
|
this.saveTimer = setTimeout(() => {this.updateElem(oElem, oEvent);}, 2000);
|
||||||
|
},
|
||||||
|
updateProject() {
|
||||||
|
this.api.post('update_project')
|
||||||
|
.then((asData, sMsg) => {this.addFeedback('success', sMsg, {'update':'project'});})
|
||||||
|
.catch((sMsg) => {this.addFeedback('error', sMsg, {'update':'project'});});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div id="admin">
|
||||||
|
<a name="back" class="button" :href="getPrevAnchor()"><SpotIcon :icon="'back'" :text="l('action.back')" /></a>
|
||||||
|
<h1>{{ l('project.plural') }}</h1>
|
||||||
|
<div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ l('project.id') }}</th>
|
||||||
|
<th>{{ l('project.single') }}</th>
|
||||||
|
<th>{{ l('project.mode') }}</th>
|
||||||
|
<th>{{ l('project.code_name') }}</th>
|
||||||
|
<th>{{ l('project.start') }}</th>
|
||||||
|
<th>{{ l('project.end') }}</th>
|
||||||
|
<th>{{ l('action.delete') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="project in elems.project">
|
||||||
|
<td>{{ project.id }}</td>
|
||||||
|
<td><AdminInput :type="'text'" :name="'name'" :elem="project" /></td>
|
||||||
|
<td>{{ project.mode }}</td>
|
||||||
|
<td><AdminInput :type="'text'" :name="'codename'" :elem="project" /></td>
|
||||||
|
<td><AdminInput :type="'date'" :name="'active_from'" :elem="project" /></td>
|
||||||
|
<td><AdminInput :type="'date'" :name="'active_to'" :elem="project" /></td>
|
||||||
|
<td><SpotButton :icon="'close'" iconSize="lg" @click="deleteElem(project)" /></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<SpotButton :classes="'new'" :text="l('project.new')" :icon="'new'" @click="createElem('project')" />
|
||||||
|
</div>
|
||||||
|
<h1>{{ l('feed.plural') }}</h1>
|
||||||
|
<div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ l('feed.id') }}</th>
|
||||||
|
<th>{{ l('feed.ref_id') }}</th>
|
||||||
|
<th>{{ l('spot.id') }}</th>
|
||||||
|
<th>{{ l('project.id') }}</th>
|
||||||
|
<th>{{ l('feed.name') }}</th>
|
||||||
|
<th>{{ l('feed.status') }}</th>
|
||||||
|
<th>{{ l('feed.last_update') }}</th>
|
||||||
|
<th>{{ l('action.delete') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="feed in elems.feed">
|
||||||
|
<td>{{ feed.id }}</td>
|
||||||
|
<td><AdminInput :type="'text'" :name="'ref_feed_id'" :elem="feed" /></td>
|
||||||
|
<td><AdminInput :type="'number'" :name="'id_spot'" :elem="feed" /></td>
|
||||||
|
<td><AdminInput :type="'number'" :name="'id_project'" :elem="feed" /></td>
|
||||||
|
<td>{{ feed.name }}</td>
|
||||||
|
<td>{{ feed.status }}</td>
|
||||||
|
<td>{{ feed.last_update }}</td>
|
||||||
|
<td><SpotButton :icon="'close'" iconSize="lg" @click="deleteElem(feed)" /></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<SpotButton :classes="'new'" :text="l('feed.new')" :icon="'new'" @click="createElem('feed')" />
|
||||||
|
</div>
|
||||||
|
<h1>{{ l('spot.plural') }}</h1>
|
||||||
|
<div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ l('spot.id') }}</th>
|
||||||
|
<th>{{ l('spot.ref_id') }}</th>
|
||||||
|
<th>{{ l('spot.name') }}</th>
|
||||||
|
<th>{{ l('spot.model') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="spot in elems.spot">
|
||||||
|
<td>{{ spot.id }}</td>
|
||||||
|
<td>{{ spot.ref_spot_id }}</td>
|
||||||
|
<td>{{ spot.name }}</td>
|
||||||
|
<td>{{ spot.model }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<h1>{{ l('user.active') }}</h1>
|
||||||
|
<div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ l('user.id') }}</th>
|
||||||
|
<th>{{ l('user.name') }}</th>
|
||||||
|
<th>{{ l('user.language') }}</th>
|
||||||
|
<th>{{ l('time.zone') }}</th>
|
||||||
|
<th>{{ l('user.clearance') }}</th>
|
||||||
|
<th>{{ l('action.delete') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="user in elems.user">
|
||||||
|
<td>{{ user.id }}</td>
|
||||||
|
<td>{{ user.name }}</td>
|
||||||
|
<td>{{ user.language }}</td>
|
||||||
|
<td>{{ user.timezone }}</td>
|
||||||
|
<td><AdminInput :type="'number'" :name="'clearance'" :elem="user" /></td>
|
||||||
|
<td><SpotButton :icon="'close'" iconSize="lg" @click="deleteElem(user)" /></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<h1>{{ l('admin.toolbox') }}</h1>
|
||||||
|
<div id="toolbox">
|
||||||
|
<SpotButton :classes="'refresh'" :text="l('project.update_messages')" :icon="'refresh'" @click="updateProject" />
|
||||||
|
</div>
|
||||||
|
<div id="feedback" class="feedback">
|
||||||
|
<p v-for="feedback in feedbacks" :class="feedback.type">{{ feedback.msg }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
18
src/components/adminInput.vue
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
type: String,
|
||||||
|
name: String,
|
||||||
|
elem: Object
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
value() {
|
||||||
|
return this.elem[this.name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<input :type="type" :name="name" :value="value" @change="$parent.updateElem(elem, $event)" @keyup="$parent.queue(elem, $event)" />
|
||||||
|
</template>
|
||||||
650
src/components/project.vue
Normal file
@@ -0,0 +1,650 @@
|
|||||||
|
<script>
|
||||||
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||||
|
import { Map, Marker, LngLatBounds, LngLat, Popup, ScaleControl, NavigationControl } from 'maplibre-gl';
|
||||||
|
import { createApp } from 'vue';
|
||||||
|
|
||||||
|
import Lightbox from '@scripts/lightbox';
|
||||||
|
|
||||||
|
import SpotIcon from '@components/spotIcon';
|
||||||
|
import SpotIconStack from '@components/spotIconStack';
|
||||||
|
import ProjectPopup from '@components/projectPopup';
|
||||||
|
import ProjectFeed from '@components/projectFeed';
|
||||||
|
import ProjectSettings from '@components/projectSettings';
|
||||||
|
|
||||||
|
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 {
|
||||||
|
components: {
|
||||||
|
SpotIcon,
|
||||||
|
ProjectFeed,
|
||||||
|
ProjectSettings
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
panels: {
|
||||||
|
leftOpen: false,
|
||||||
|
rightOpen: false
|
||||||
|
},
|
||||||
|
feed: 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,
|
||||||
|
modeHisto: null,
|
||||||
|
baseMaps: [],
|
||||||
|
baseMap: null,
|
||||||
|
map: null,
|
||||||
|
mapInitializing: false,
|
||||||
|
markerHeight: 32, //FIXME
|
||||||
|
mapPadding: 16 + 32, //1rem + marker height
|
||||||
|
maxZoom: 15,
|
||||||
|
initialPitch: 45,
|
||||||
|
lightbox: null,
|
||||||
|
hikes: {
|
||||||
|
colors: {},
|
||||||
|
width: null
|
||||||
|
},
|
||||||
|
popup: {content: null, element: null},
|
||||||
|
overview: {id: 0, codename:'overview', name: this.lang.get('project.overview')},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
projectOptions() {
|
||||||
|
return [
|
||||||
|
this.overview,
|
||||||
|
...Object.values(this.projects)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'hash.items.0'(newProjectCodename, oldProjectCodename) { //hash.items.0 = Project Code Name
|
||||||
|
if(newProjectCodename != oldProjectCodename) {
|
||||||
|
this.hash.items = [newProjectCodename]; //Force removal of direct link
|
||||||
|
this.settings.toggle(false, 0);
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
provide() {
|
||||||
|
return {
|
||||||
|
map: {
|
||||||
|
panToBetweenPanels: this.panToBetweenPanels,
|
||||||
|
openMarkerPopup: this.openMarkerPopup,
|
||||||
|
closePopup: this.closePopup,
|
||||||
|
isMarkerVisible: this.isMarkerVisible
|
||||||
|
},
|
||||||
|
project: this
|
||||||
|
};
|
||||||
|
},
|
||||||
|
inject: ['api', 'lang', 'hash', 'projects', 'user', 'consts', 'isMobile'],
|
||||||
|
mounted() {
|
||||||
|
//Starts default project init() through watcher
|
||||||
|
if(this.hash.items.length == 0) {
|
||||||
|
this.hash.items[0] = (this.projects.getDefaultProject().mode == this.consts.modes.blog)?this.projects.getDefaultCodeName():this.overview.codename;
|
||||||
|
}
|
||||||
|
else this.init();
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
this.quit();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async init() {
|
||||||
|
this.initLightbox();
|
||||||
|
this.hikes.colors = {
|
||||||
|
'main': this.getStyleProperty('--track-main'),
|
||||||
|
'off-track': this.getStyleProperty('--track-off-track'),
|
||||||
|
'hitchhiking': this.getStyleProperty('--track-hitchhiking')
|
||||||
|
};
|
||||||
|
this.hikes.width = parseFloat(this.getStyleProperty('--track-width'));
|
||||||
|
|
||||||
|
//Reset values
|
||||||
|
this.track = null;
|
||||||
|
this.project = null;
|
||||||
|
this.removeMapContent();
|
||||||
|
|
||||||
|
//Build Map
|
||||||
|
this.mapInitializing = true;
|
||||||
|
if(this.hash.items[0] && this.projects[this.hash.items[0]]) await this.initProject(this.hash.items[0]);
|
||||||
|
else await this.initOverview();
|
||||||
|
this.mapInitializing = false;
|
||||||
|
},
|
||||||
|
quit() {
|
||||||
|
this.lightbox.end(true);
|
||||||
|
this.lightbox = null;
|
||||||
|
this.removeMap();
|
||||||
|
},
|
||||||
|
async initOverview() {
|
||||||
|
this.modeHisto = true;
|
||||||
|
this.hash.items = [this.overview.codename];
|
||||||
|
this.feed.toggle(false, 0);
|
||||||
|
|
||||||
|
await this.initOverviewMap();
|
||||||
|
},
|
||||||
|
async initProject(sProjectCodeName) {
|
||||||
|
this.project = this.projects[sProjectCodeName];
|
||||||
|
this.modeHisto = (this.project.mode == this.consts.modes.histo);
|
||||||
|
|
||||||
|
await this.$nextTick();
|
||||||
|
const pMapReady = this.initProjectMap();
|
||||||
|
await this.feed.init(pMapReady);
|
||||||
|
},
|
||||||
|
initLightbox() {
|
||||||
|
if(!this.lightbox) {
|
||||||
|
this.lightbox = new Lightbox({
|
||||||
|
alwaysShowNavOnTouchDevices: true,
|
||||||
|
fadeDuration: 300,
|
||||||
|
imageFadeDuration: 400,
|
||||||
|
positionFromTop: 0,
|
||||||
|
resizeDuration: 400,
|
||||||
|
hasVideo: true,
|
||||||
|
onMediaChange: async (oMedia) => {
|
||||||
|
this.hash.items = [this.project.codename, 'media', oMedia.id];
|
||||||
|
if(oMedia.set == 'post-medias') {
|
||||||
|
(await this.feed.goToPost('media', oMedia.id))?.panMapToMarker();
|
||||||
|
if(!this.lightbox.hasMediaAfterCurrent()) {
|
||||||
|
await this.feed.getNextFeed();
|
||||||
|
await this.$nextTick();
|
||||||
|
this.lightbox.refreshAlbum();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClosing: () => {this.hash.items = [this.hash.items[0]];}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async initProjectMap() {
|
||||||
|
[
|
||||||
|
{maps: this.baseMaps, markers: this.markers},
|
||||||
|
this.track
|
||||||
|
] = await Promise.all([
|
||||||
|
this.api.get('markers', {id_project: this.project.id}),
|
||||||
|
this.api.get('geojson', {id_project: this.project.id})
|
||||||
|
]);
|
||||||
|
|
||||||
|
await this.initMap();
|
||||||
|
},
|
||||||
|
async initOverviewMap() {
|
||||||
|
this.baseMaps = this.consts.default_maps;
|
||||||
|
this.markers = Object.values(this.projects).map((asProject) => ({
|
||||||
|
type: 'project',
|
||||||
|
subtype: 'project',
|
||||||
|
...asProject,
|
||||||
|
opacityWhenCovered: 0.3
|
||||||
|
}));
|
||||||
|
|
||||||
|
await this.initMap();
|
||||||
|
},
|
||||||
|
async initMap() {
|
||||||
|
//Build map
|
||||||
|
if(!this.map) this.addMap();
|
||||||
|
this.updateMapPadding();
|
||||||
|
|
||||||
|
//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: '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);
|
||||||
|
this.addTrack();
|
||||||
|
this.markers.forEach(this.addMarker);
|
||||||
|
},
|
||||||
|
removeMapContent() {
|
||||||
|
if(!this.map) return;
|
||||||
|
|
||||||
|
this.closePopup();
|
||||||
|
this.removeTrack();
|
||||||
|
this.markers.forEach(this.removeMarker);
|
||||||
|
this.baseMaps.forEach(this.removeBaseMap);
|
||||||
|
},
|
||||||
|
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(SpotIconStack, 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]
|
||||||
|
});
|
||||||
|
},
|
||||||
|
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 / this.map.transform.tileSize),
|
||||||
|
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) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if(!this.map) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.map.once('moveend', resolve);
|
||||||
|
this.map.easeTo({
|
||||||
|
center: oLngLat,
|
||||||
|
zoom: iZoom,
|
||||||
|
padding: this.getMapPadding(),
|
||||||
|
duration: iAnimDuration
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getMapPadding() {
|
||||||
|
let bIsMobile = this.isMobile();
|
||||||
|
return {
|
||||||
|
top: this.mapPadding,
|
||||||
|
bottom: this.mapPadding,
|
||||||
|
left: this.mapPadding + ((!bIsMobile && this.panels.leftOpen && this.settings)?this.settings.getWidth():0),
|
||||||
|
right: this.mapPadding + ((!bIsMobile && this.panels.rightOpen && this.feed)?this.feed.getWidth():0)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
updateMapPadding(iDuration=0) {
|
||||||
|
const asPadding = this.getMapPadding();
|
||||||
|
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) {
|
||||||
|
const sPanelKey = sPanel + 'Open';
|
||||||
|
let bOldValue = this.panels[sPanelKey];
|
||||||
|
this.panels[sPanelKey] = bNewValue;
|
||||||
|
|
||||||
|
if(bOldValue != bNewValue) {
|
||||||
|
//Adjust map center
|
||||||
|
if(!this.isMobile() && this.map) this.updateMapPadding(iAnimDuration);
|
||||||
|
|
||||||
|
//Open Close panels
|
||||||
|
this.$el.classList.toggle('with-'+sPanel+'-panel');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setFeed(vPanel) {
|
||||||
|
this.feed = vPanel;
|
||||||
|
},
|
||||||
|
setSettings(vPanel) {
|
||||||
|
this.settings = vPanel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="projects">
|
||||||
|
<div id="space"></div>
|
||||||
|
<div id="submap">
|
||||||
|
<div class="loader">
|
||||||
|
<SpotIcon :icon="'map'" :classes="'flicker'" width="fixed" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="map"></div>
|
||||||
|
<ProjectSettings
|
||||||
|
:ref="setSettings"
|
||||||
|
:projects="projectOptions"
|
||||||
|
v-model:project-code-name="hash.items[0]"
|
||||||
|
:base-maps="baseMaps"
|
||||||
|
v-model:base-map="baseMap"
|
||||||
|
:map-initializing="mapInitializing"
|
||||||
|
:hikes="hikes"
|
||||||
|
@toggle="(bIsOpen, iAnimDuration) => onPanelToggle('left', bIsOpen, iAnimDuration)"
|
||||||
|
/>
|
||||||
|
<ProjectFeed
|
||||||
|
:ref="setFeed"
|
||||||
|
:project="project"
|
||||||
|
:mode-histo="modeHisto"
|
||||||
|
@request-last-update="settings?.setLastUpdate"
|
||||||
|
@new-markers="addNewMarkers"
|
||||||
|
@toggle="(bIsOpen, iAnimDuration) => onPanelToggle('right', bIsOpen, iAnimDuration)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||