Separate project / feed / settings
All checks were successful
Deploy Spot / deploy (push) Successful in 49s
All checks were successful
Deploy Spot / deploy (push) Successful in 49s
This commit is contained in:
@@ -54,6 +54,7 @@ jobs:
|
||||
--exclude "/.npm-cache/" \
|
||||
--exclude "/node_modules/" \
|
||||
--exclude "/config/settings.php" \
|
||||
--exclude "/log.html" \
|
||||
--exclude "/files/" \
|
||||
--exclude "/geo/" \
|
||||
--exclude "/gaia/" \
|
||||
|
||||
13
lib/Spot.php
13
lib/Spot.php
@@ -325,19 +325,20 @@ class Spot extends Main
|
||||
$asMarkers = [...$asMessages, ...$asGeoMedias];
|
||||
usort($asMarkers, function($a, $b){return $a['unix_time'] > $b['unix_time'];});
|
||||
|
||||
//Spot Last Update
|
||||
$asLastUpdate = array();
|
||||
$this->addTimeStamp($asLastUpdate, $this->oProject->getLastUpdate());
|
||||
|
||||
$asResult = array(
|
||||
'markers' => $asMarkers,
|
||||
'maps' => $this->oMap->getProjectMaps($this->oProject->getProjectId()),
|
||||
'last_update' => $asLastUpdate
|
||||
'maps' => $this->oMap->getProjectMaps($this->oProject->getProjectId())
|
||||
);
|
||||
|
||||
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) {
|
||||
$asResult = $this->oUser->addUser($sEmail, $this->oLang->getLanguage(), date_default_timezone_get(), $sNickName);
|
||||
$asUserInfo = $this->oUser->getUserInfo();
|
||||
|
||||
@@ -42,6 +42,9 @@ if($sAction!='')
|
||||
case 'markers':
|
||||
$sResult = $oSpot->getMarkers();
|
||||
break;
|
||||
case 'last_update':
|
||||
$sResult = $oSpot->getLastUpdate();
|
||||
break;
|
||||
case 'geojson':
|
||||
$sResult = $oSpot->getProjectGeoJson();
|
||||
break;
|
||||
|
||||
115
package-lock.json
generated
115
package-lock.json
generated
@@ -2131,12 +2131,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz",
|
||||
"integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==",
|
||||
"version": "25.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
|
||||
"integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.19.0"
|
||||
"undici-types": ">=7.24.0 <7.24.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/retry": {
|
||||
@@ -2680,9 +2680,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.10.29",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz",
|
||||
"integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==",
|
||||
"version": "2.10.31",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz",
|
||||
"integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"baseline-browser-mapping": "dist/cli.cjs"
|
||||
@@ -2760,9 +2760,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001792",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz",
|
||||
"integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==",
|
||||
"version": "1.0.30001793",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
|
||||
"integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -2797,15 +2797,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
||||
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
"readdirp": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.16.0"
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
@@ -2992,9 +2992,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/css-loader/node_modules/semver": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
|
||||
"integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
|
||||
"integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -3084,9 +3084,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.353",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz",
|
||||
"integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==",
|
||||
"version": "1.5.361",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz",
|
||||
"integrity": "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/emojis-list": {
|
||||
@@ -3099,9 +3099,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.21.5",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.5.tgz",
|
||||
"integrity": "sha512-mLCNbrQli11K1ySUmuNt4ZUB3OpGIDq4q2vTBTf5cL2lpsRjI9QKqSD0ndjW8FyvcW/Jj46gMe9syyHAsvMa/A==",
|
||||
"version": "5.21.6",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz",
|
||||
"integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
@@ -3758,9 +3758,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/kdbush": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
|
||||
"integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.1.0.tgz",
|
||||
"integrity": "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/kind-of": {
|
||||
@@ -4015,10 +4015,13 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.38",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
|
||||
"integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
|
||||
"license": "MIT"
|
||||
"version": "2.0.45",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.45.tgz",
|
||||
"integrity": "sha512-iIbHXV9eBB2nB0wa7oTsrrXq+qQt+9SIlx9AX3T96YgobtEQfis5n6TJ6vV+3QP8DwdriEAcGhARaFCu37peBg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
@@ -4331,9 +4334,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.14",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
|
||||
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
|
||||
"version": "8.5.15",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -4350,7 +4353,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"nanoid": "^3.3.12",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
@@ -4461,9 +4464,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/preact": {
|
||||
"version": "10.29.1",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz",
|
||||
"integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==",
|
||||
"version": "10.29.2",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.29.2.tgz",
|
||||
"integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -4483,12 +4486,12 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
|
||||
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
@@ -4661,12 +4664,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.99.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.99.0.tgz",
|
||||
"integrity": "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==",
|
||||
"version": "1.100.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.100.0.tgz",
|
||||
"integrity": "sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"chokidar": "^5.0.0",
|
||||
"immutable": "^5.1.5",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
},
|
||||
@@ -4674,7 +4677,7 @@
|
||||
"sass": "sass.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@parcel/watcher": "^2.4.1"
|
||||
@@ -4923,9 +4926,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.47.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.47.1.tgz",
|
||||
"integrity": "sha512-tPbLXTI6ohPASb/1YViL428oEHu6/qv1OxqYnfaonVCFHqx4+wCd95pHrQWsL5X4pl90CTyW9piSAsS2L0VoMw==",
|
||||
"version": "5.48.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz",
|
||||
"integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
@@ -5035,9 +5038,9 @@
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.19.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
|
||||
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
|
||||
"version": "7.24.6",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
|
||||
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||
@@ -5204,9 +5207,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
"version": "5.107.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.107.0.tgz",
|
||||
"integrity": "sha512-PSxeHk/dmLYZlnTU+vL1Gej6Evg5RNtl3flhxBresfznFnzxinHMzHKloHnywM/3ouQv7/AlZCswWDIkNSggUA==",
|
||||
"version": "5.107.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.107.1.tgz",
|
||||
"integrity": "sha512-mvdIWxj/H6QsfgDdH9djne3a5dYcmEmtsXGESkypaGN5jXjF/b+9KDlmTDQ2TKlFUeA2fI9Y65kihD30JOdB+Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.8",
|
||||
|
||||
@@ -2,31 +2,29 @@
|
||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
import { Map, Marker, LngLatBounds, LngLat, Popup, ScaleControl } from 'maplibre-gl';
|
||||
import { createApp } from 'vue';
|
||||
import Simplebar from 'simplebar-vue';
|
||||
|
||||
import Lightbox from '@scripts/lightbox';
|
||||
|
||||
import SpotIcon from '@components/spotIcon';
|
||||
import SpotIconStack from '@components/spotIconStack';
|
||||
import ProjectPost from '@components/projectPost';
|
||||
import ProjectPopup from '@components/projectPopup';
|
||||
import ProjectNewsletter from '@components/projectNewsletter';
|
||||
import ProjectFeed from '@components/projectFeed';
|
||||
import ProjectSettings from '@components/projectSettings';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SpotIcon,
|
||||
ProjectPost,
|
||||
ProjectNewsletter,
|
||||
Simplebar
|
||||
ProjectFeed,
|
||||
ProjectSettings
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
feed: {loading:false, updatable:true, outOfData:false, refIdFirst:0, refIdLast:0, firstChunk:true},
|
||||
refreshRate: 60,
|
||||
lastUpdate: { unix_time: 0, relative_time: '', formatted_time: ''},
|
||||
feedPanelOpen: false,
|
||||
feedSwipe: {x: null, y: null},
|
||||
settingsPanelOpen: false,
|
||||
panels: {
|
||||
feedOpen: false,
|
||||
settingsOpen: false
|
||||
},
|
||||
feed: null,
|
||||
settings: null,
|
||||
track: null,
|
||||
markers: [],
|
||||
markerProps: {
|
||||
@@ -35,29 +33,24 @@ export default {
|
||||
video: {mainClasses: 'media', iconMain: 'marker', iconSub: 'video'},
|
||||
message: {mainClasses: 'message', iconMain: 'marker', iconSub: 'footprint', iconSubTransform: 'rotate-270'}
|
||||
},
|
||||
currProject: null,
|
||||
project: null,
|
||||
modeHisto: null,
|
||||
posts: [],
|
||||
baseMaps: [],
|
||||
baseMap: null,
|
||||
map: null,
|
||||
mapInitializing: false,
|
||||
markerHeight: 32, //FIXME
|
||||
mapPadding: 16 + 32, //1rem + marker height
|
||||
lightbox: null,
|
||||
hikes: {
|
||||
colors: {},
|
||||
width: 4
|
||||
},
|
||||
popup: {content: null, element: null},
|
||||
overview: {id: 0, codename:'overview', name: this.lang.get('project.overview')}
|
||||
overview: {id: 0, codename:'overview', name: this.lang.get('project.overview')},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
projectClasses() {
|
||||
return [
|
||||
this.feedPanelOpen?'with-feed':'',
|
||||
this.settingsPanelOpen?'with-settings':''
|
||||
].filter(n => n).join(' ');
|
||||
},
|
||||
projectOptions() {
|
||||
return [
|
||||
this.overview,
|
||||
@@ -75,7 +68,7 @@ export default {
|
||||
'hash.items.0'(newProjectCodename, oldProjectCodename) {
|
||||
if(newProjectCodename != oldProjectCodename) {
|
||||
this.hash.items = [newProjectCodename];
|
||||
this.toggleSettingsPanel(false, 'none');
|
||||
this.settings.toggle(false, 0);
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
@@ -113,11 +106,8 @@ export default {
|
||||
};
|
||||
|
||||
//Reset values
|
||||
this.setFeedUpdateTimer(-1);
|
||||
this.feed = {loading:false, updatable:true, outOfData:false, refIdFirst:0, refIdLast:0, firstChunk:true};
|
||||
this.posts = [];
|
||||
this.track = null;
|
||||
this.currProject = null;
|
||||
this.project = null;
|
||||
this.removeMapContent();
|
||||
|
||||
//Build Map
|
||||
@@ -128,28 +118,27 @@ export default {
|
||||
},
|
||||
quit() {
|
||||
this.lightbox.end();
|
||||
this.$refs.feedSimpleBar?.scrollElement.removeEventListener('scroll', this.onFeedScroll);
|
||||
this.setFeedUpdateTimer(-1);
|
||||
this.removeMap();
|
||||
},
|
||||
async initOverview() {
|
||||
this.modeHisto = true;
|
||||
this.hash.items = [this.overview.codename];
|
||||
this.toggleFeedPanel(false, 'none');
|
||||
this.feed.toggle(false, 0);
|
||||
|
||||
await this.initOverviewMap();
|
||||
},
|
||||
async initProject(iProjectId) {
|
||||
this.currProject = this.projects[iProjectId];
|
||||
this.modeHisto = (this.currProject.mode == this.consts.modes.histo);
|
||||
async initProject(sProjectCodeName) {
|
||||
this.project = this.projects[sProjectCodeName];
|
||||
this.modeHisto = (this.project.mode == this.consts.modes.histo);
|
||||
|
||||
await this.$nextTick();
|
||||
await Promise.all([
|
||||
this.initFeed(),
|
||||
this.feed.init(),
|
||||
this.initProjectMap()
|
||||
]);
|
||||
|
||||
//Direct link post action
|
||||
if(this.hash.items.length == 3) await this.findPost(this.hash.items[1], this.hash.items[2]);
|
||||
if(this.hash.items.length == 3) await this.feed.findPost(this.hash.items[1], this.hash.items[2]);
|
||||
},
|
||||
initLightbox() {
|
||||
if(!this.lightbox) {
|
||||
@@ -162,11 +151,11 @@ export default {
|
||||
resizeDuration: 400,
|
||||
hasVideo: true,
|
||||
onMediaChange: async (oMedia) => {
|
||||
this.hash.items = [this.currProject.codename, 'media', oMedia.id];
|
||||
this.hash.items = [this.project.codename, 'media', oMedia.id];
|
||||
if(oMedia.set == 'post-medias') {
|
||||
this.goToPost('media', oMedia.id)?.panMapToMarker();
|
||||
(await this.feed.goToPost('media', oMedia.id))?.panMapToMarker();
|
||||
if(!this.lightbox.hasMediaAfterCurrent()) {
|
||||
await this.getNextFeed();
|
||||
await this.feed.getNextFeed();
|
||||
await this.$nextTick();
|
||||
this.lightbox.refreshAlbum();
|
||||
}
|
||||
@@ -176,39 +165,19 @@ export default {
|
||||
});
|
||||
}
|
||||
},
|
||||
async initFeed() {
|
||||
await this.$nextTick();
|
||||
|
||||
//Simplebar event
|
||||
this.$refs.feedSimpleBar?.scrollElement.removeEventListener('scroll', this.onFeedScroll);
|
||||
this.$refs.feedSimpleBar?.scrollElement.addEventListener('scroll', this.onFeedScroll);
|
||||
|
||||
this.toggleFeedPanel(!this.isMobile(), 'none');
|
||||
|
||||
//Get first posts batch
|
||||
await this.getNextFeed();
|
||||
if(this.$refs.feedSimpleBar) this.$refs.feedSimpleBar.scrollElement.scrollTop = 0;
|
||||
|
||||
//Start auto-update
|
||||
if(!this.modeHisto) this.setFeedUpdateTimer(this.refreshRate);
|
||||
},
|
||||
async initProjectMap() {
|
||||
[
|
||||
{
|
||||
maps: this.baseMaps,
|
||||
markers: this.markers,
|
||||
last_update: this.lastUpdate
|
||||
},
|
||||
{maps: this.baseMaps, markers: this.markers},
|
||||
this.track
|
||||
] = await Promise.all([
|
||||
this.api.get('markers', {id_project: this.currProject.id}),
|
||||
this.api.get('geojson', {id_project: this.currProject.id})
|
||||
this.api.get('markers', {id_project: this.project.id}),
|
||||
this.api.get('geojson', {id_project: this.project.id})
|
||||
]);
|
||||
|
||||
await this.initMap({
|
||||
setCamera: () => {
|
||||
this.map.fitBounds(this.getInitialMapBounds(), {
|
||||
padding: 20,
|
||||
padding: this.mapPadding,
|
||||
animate: false,
|
||||
maxZoom: 15
|
||||
});
|
||||
@@ -229,7 +198,7 @@ export default {
|
||||
//Center on default project
|
||||
const oDefaultProject = this.projects.getDefaultProject();
|
||||
|
||||
//Adapt zoom to see whole planet
|
||||
//Get Map / Canvas size
|
||||
const $Canvas = this.map.getCanvas();
|
||||
const oMapBounds = this.map.getContainer().getBoundingClientRect();
|
||||
|
||||
@@ -248,7 +217,7 @@ export default {
|
||||
//Build map
|
||||
if(!this.map) this.addMap();
|
||||
this.updateMapPadding();
|
||||
setCamera();
|
||||
setCamera();
|
||||
|
||||
//Force wait for load event
|
||||
await new Promise((resolve) => {
|
||||
@@ -363,7 +332,7 @@ export default {
|
||||
'source': 'track',
|
||||
'paint': {
|
||||
'line-opacity': 0,
|
||||
'line-width': this.hikes.width + 20
|
||||
'line-width': this.hikes.width + this.mapPadding
|
||||
}
|
||||
});
|
||||
this.map.on('click', 'track-hitbox', this.openTrackPopup);
|
||||
@@ -435,7 +404,7 @@ export default {
|
||||
this.openPopup({
|
||||
lnglat: [oProject.longitude, oProject.latitude],
|
||||
options: oProject,
|
||||
offset: [0, -32] //FIXME
|
||||
offset: [0, -1 * this.markerHeight]
|
||||
});
|
||||
},
|
||||
openMarkerPopup(iMarkerId, sMarkerType) {
|
||||
@@ -443,7 +412,7 @@ export default {
|
||||
this.openPopup({
|
||||
lnglat: [oMarker.longitude, oMarker.latitude],
|
||||
options: oMarker,
|
||||
offset: [0, -32] //FIXME
|
||||
offset: [0, -1 * this.markerHeight]
|
||||
});
|
||||
},
|
||||
openTrackPopup(oEvent) {
|
||||
@@ -466,7 +435,7 @@ export default {
|
||||
|
||||
this.popup.content = createApp(ProjectPopup, {
|
||||
options: options,
|
||||
project: this.currProject
|
||||
project: this.project
|
||||
});
|
||||
this.popup.content
|
||||
.provide('lang', this.lang)
|
||||
@@ -501,7 +470,7 @@ export default {
|
||||
oBounds.extend(new LngLat(oHashMarker.longitude, oHashMarker.latitude));
|
||||
}
|
||||
else if( //Blog Mode: Fit to last message
|
||||
this.currProject.mode == this.consts.modes.blog &&
|
||||
this.project.mode == this.consts.modes.blog &&
|
||||
this.markers.length > 0
|
||||
) {
|
||||
let oLastMsg = this.markers.at(-1);
|
||||
@@ -510,9 +479,7 @@ export default {
|
||||
else { //Pre/Histo Mode: Fit to track
|
||||
for(const iFeatureId in this.track.features) {
|
||||
oBounds = this.track.features[iFeatureId].geometry.coordinates.reduce(
|
||||
(bounds, coord) => {
|
||||
return bounds.extend(coord);
|
||||
},
|
||||
(bounds, coord) => bounds.extend(coord),
|
||||
oBounds
|
||||
);
|
||||
}
|
||||
@@ -520,110 +487,9 @@ export default {
|
||||
|
||||
return oBounds;
|
||||
},
|
||||
|
||||
async findPost(sPostType, iPostId) {
|
||||
let vPost = this.goToPost(sPostType, iPostId);
|
||||
if(vPost) {
|
||||
await vPost.executeMainAction(0);
|
||||
return vPost;
|
||||
}
|
||||
else if(!this.feed.outOfData) {
|
||||
await this.getNextFeed();
|
||||
return this.findPost(sPostType, iPostId);
|
||||
}
|
||||
else console.log('Missing element ID "'+iPostId+'" of type "'+sPostType+'"');
|
||||
return null;
|
||||
},
|
||||
goToPost(sPostType, iPostId) {
|
||||
let bFound = false;
|
||||
let avPosts = this.$refs.posts.filter((post) => {return post.postId == sPostType+'-'+iPostId;});
|
||||
if(avPosts.length > 0) {
|
||||
let vPost = avPosts[0];
|
||||
this.$refs.feedSimpleBar.scrollElement.scrollTop += Math.round(
|
||||
vPost.$el.getBoundingClientRect().top
|
||||
+ window.pageYOffset
|
||||
- parseFloat(getComputedStyle(this.$refs.feedSimpleBar.$el).paddingTop)
|
||||
);
|
||||
|
||||
return vPost;
|
||||
}
|
||||
},
|
||||
async getNextFeed() {
|
||||
if(!this.feed.outOfData && !this.feed.loading) {
|
||||
//Get next chunk
|
||||
this.feed.loading = true;
|
||||
let aoData = await this.api.get('next_feed', {id_project: this.currProject.id, id: this.feed.refIdLast});
|
||||
let iPostCount = Object.keys(aoData.feed).length;
|
||||
|
||||
//Update pointers
|
||||
this.feed.outOfData = (iPostCount < this.consts.chunk_size);
|
||||
if(iPostCount > 0) {
|
||||
this.feed.refIdLast = aoData.ref_id_last;
|
||||
if(this.feed.firstChunk) this.feed.refIdFirst = aoData.ref_id_first;
|
||||
}
|
||||
|
||||
//Add posts
|
||||
this.posts.push(...aoData.feed);
|
||||
|
||||
this.feed.loading = false;
|
||||
this.feed.firstChunk = false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
onFeedScroll(oEvent) {
|
||||
const box = oEvent.currentTarget
|
||||
const content = box.querySelector('.simplebar-content')
|
||||
|
||||
if ((box.scrollTop + box.clientHeight) / (content?.offsetHeight || 1) >= 0.8) this.getNextFeed();
|
||||
},
|
||||
onFeedTouchStart(oEvent) {
|
||||
if(!this.isMobile() || !this.feedPanelOpen || oEvent.touches.length != 1) return;
|
||||
|
||||
const oTouch = oEvent.touches[0];
|
||||
this.feedSwipe = {x: oTouch.clientX, y: oTouch.clientY};
|
||||
},
|
||||
onFeedTouchEnd(oEvent) {
|
||||
const oTouch = oEvent.changedTouches[0];
|
||||
if(!oTouch || this.feedSwipe.x === null) return;
|
||||
|
||||
const iDeltaX = oTouch.clientX - this.feedSwipe.x;
|
||||
const iDeltaY = oTouch.clientY - this.feedSwipe.y;
|
||||
|
||||
if(iDeltaX > 80 && Math.abs(iDeltaX) > Math.abs(iDeltaY) * 1.5) {
|
||||
this.toggleFeedPanel();
|
||||
}
|
||||
|
||||
this.feedSwipe = {x: null, y: null};
|
||||
},
|
||||
setFeedUpdateTimer(iSeconds) {
|
||||
if(typeof this.feedTimer != 'undefined') clearTimeout(this.feedTimer);
|
||||
if(iSeconds >= 0) this.feedTimer = setTimeout(this.checkNewFeed, iSeconds * 1000);
|
||||
},
|
||||
async checkNewFeed() {
|
||||
let aoData = await this.api.get('new_feed', {id_project: this.currProject.id, id: this.feed.refIdFirst});
|
||||
const aoFeed = aoData.feed || [];
|
||||
const aoMarkers = aoData.markers || [];
|
||||
|
||||
if(aoFeed.length > 0) {
|
||||
//Update pointer
|
||||
this.feed.refIdFirst = aoData.ref_id_first;
|
||||
|
||||
//Add new posts
|
||||
this.posts.unshift(...aoFeed);
|
||||
}
|
||||
|
||||
//Add new Markers
|
||||
if(aoMarkers.length > 0) {
|
||||
this.markers.push(...aoMarkers);
|
||||
aoMarkers.forEach(this.addMarker);
|
||||
}
|
||||
|
||||
//Message Last Update
|
||||
this.lastUpdate = aoData.last_update;
|
||||
|
||||
//Reschedule
|
||||
this.setFeedUpdateTimer(this.refreshRate);
|
||||
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) => {
|
||||
@@ -643,10 +509,10 @@ export default {
|
||||
getMapPadding() {
|
||||
let bIsMobile = this.isMobile();
|
||||
return {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: (!bIsMobile && this.settingsPanelOpen)?this.$refs.settings.getBoundingClientRect().width:0,
|
||||
right: (!bIsMobile && this.feedPanelOpen)?this.$refs.feed.getBoundingClientRect().width:0
|
||||
top: this.mapPadding,
|
||||
bottom: this.mapPadding,
|
||||
left: this.mapPadding + ((!bIsMobile && this.panels.settingsOpen && this.settings)?this.settings.getWidth():0),
|
||||
right: this.mapPadding + ((!bIsMobile && this.panels.feedOpen && this.feed)?this.feed.getWidth():0)
|
||||
};
|
||||
},
|
||||
updateMapPadding(iDuration=0) {
|
||||
@@ -660,58 +526,31 @@ export default {
|
||||
isMarkerVisible(oLngLat){
|
||||
return !!this.map && this.map.getBounds().contains(oLngLat);
|
||||
},
|
||||
getGoogleMapsLink(asInfo) {
|
||||
return $('<a>', {
|
||||
href:'https://www.google.com/maps/place/'+asInfo.lat_dms+'+'+asInfo.lon_dms+'/@'+asInfo.latitude+','+asInfo.longitude+',10z',
|
||||
title: this.lang.get('map.see_on_google'),
|
||||
target: '_blank',
|
||||
rel: 'noreferrer noopener'
|
||||
}).text(asInfo.lat_dms+' '+asInfo.lon_dms);
|
||||
},
|
||||
toggleFeedPanel(bShow, sMapAction) {
|
||||
let bOldValue = this.feedPanelOpen;
|
||||
this.feedPanelOpen = (typeof bShow === 'object' || typeof bShow === 'undefined')?(!this.feedPanelOpen):bShow;
|
||||
onPanelToggle(sPanel, bNewValue, iAnimDuration=500) {
|
||||
const sPanelKey = sPanel + 'Open';
|
||||
let bOldValue = this.panels[sPanelKey];
|
||||
this.panels[sPanelKey] = bNewValue;
|
||||
|
||||
if(bOldValue != this.feedPanelOpen && !this.isMobile() && this.map) {
|
||||
sMapAction = sMapAction || 'panTo';
|
||||
switch(sMapAction) {
|
||||
case 'none':
|
||||
this.updateMapPadding();
|
||||
break;
|
||||
case 'panTo':
|
||||
this.updateMapPadding(500);
|
||||
break;
|
||||
case 'panToInstant':
|
||||
this.updateMapPadding();
|
||||
break;
|
||||
}
|
||||
if(bOldValue != bNewValue) {
|
||||
//Adjust map center
|
||||
if(!this.isMobile() && this.map) this.updateMapPadding(iAnimDuration);
|
||||
|
||||
//Open Close panels
|
||||
this.$el.classList.toggle('with-'+sPanel);
|
||||
}
|
||||
},
|
||||
toggleSettingsPanel(bShow, sMapAction) {
|
||||
let bOldValue = this.settingsPanelOpen;
|
||||
this.settingsPanelOpen = (typeof bShow === 'object' || typeof bShow === 'undefined')?(!this.settingsPanelOpen):bShow;
|
||||
|
||||
if(bOldValue != this.settingsPanelOpen && !this.isMobile() && this.map) {
|
||||
sMapAction = sMapAction || 'panTo';
|
||||
switch(sMapAction) {
|
||||
case 'none':
|
||||
this.updateMapPadding();
|
||||
break;
|
||||
case 'panTo':
|
||||
this.updateMapPadding(500);
|
||||
break;
|
||||
case 'panToInstant':
|
||||
this.updateMapPadding();
|
||||
break;
|
||||
}
|
||||
}
|
||||
setFeed(vPanel) {
|
||||
this.feed = vPanel;
|
||||
},
|
||||
setSettings(vPanel) {
|
||||
this.settings = vPanel;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="projects" :class="projectClasses">
|
||||
<div class="projects">
|
||||
<div id="background"></div>
|
||||
<div id="submap">
|
||||
<div class="loader">
|
||||
@@ -719,87 +558,23 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
<div id="map"></div>
|
||||
<div id="settings" class="map-container map-container-left" ref="settings">
|
||||
<div id="settings-panel" class="map-panel">
|
||||
<div class="settings-header">
|
||||
<div class="logo"><img width="289" height="72" src="images/logo_black.png" alt="Spotty" /></div>
|
||||
<div id="last_update" v-if="this.currProject && this.currProject.mode == this.consts.modes.blog && lastUpdate.unix_time > 0">
|
||||
<p><span><img src="images/spot-logo-only.svg" alt="" /></span><abbr :title="lastUpdate.formatted_time">{{ lang.get('feed.last_update')+' '+lastUpdate.relative_time }}</abbr></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-sections">
|
||||
<Simplebar id="settings-sections-scrollbox">
|
||||
<div class="settings-section">
|
||||
<h1><SpotIcon :icon="'project'" width="fixed" :text="lang.get('project.hikes')" /></h1>
|
||||
<div class="settings-section-body">
|
||||
<div class="radio" v-for="project in projectOptions" :key="'project-'+project.id">
|
||||
<input type="radio" :id="'project-'+project.id" :value="project.codename" v-model="hash.items[0]" :disabled="mapInitializing" />
|
||||
<label :for="'project-'+project.id">
|
||||
<span>{{ project.name }}</span>
|
||||
<a v-if="project.gpxfilepath" class="download" :href="project.gpxfilepath" :download="project.codename + '.gpx'" :title="lang.get('track.download')" @click.stop="()=>{}">
|
||||
<SpotIcon :icon="'download'" margin="left" />
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<h1><SpotIcon :icon="'map'" width="fixed" :text="lang.get('map.title')" /></h1>
|
||||
<div class="settings-section-body">
|
||||
<div class="radio" v-for="bm in baseMaps" :key="'map-'+bm.id_map">
|
||||
<input type="radio" :id="'map-'+bm.id_map" :value="bm.codename" v-model="baseMap" :disabled="mapInitializing" />
|
||||
<label :for="'map-'+bm.id_map">{{ lang.get('map.'+bm.codename) }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-section newsletter">
|
||||
<ProjectNewsletter />
|
||||
</div>
|
||||
<div class="settings-section admin" v-if="user.hasClearance(consts.clearances.admin)">
|
||||
<h1><SpotIcon :icon="'admin'" width="fixed" :text="lang.get('admin.title')" /></h1>
|
||||
<div class="admin-actions">
|
||||
<a class="button" href="#admin"><SpotIcon :icon="'config'" :text="lang.get('admin.config')" /></a>
|
||||
<a class="button" href="#upload"><SpotIcon :icon="'upload'" :text="lang.get('admin.upload')" /></a>
|
||||
</div>
|
||||
</div>
|
||||
</Simplebar>
|
||||
</div>
|
||||
<div class="settings-footer">
|
||||
<a href="https://git.lutran.fr/franzz/spot" :title="lang.get('credits.git')" target="_blank" rel="noopener">
|
||||
<SpotIcon :icon="'credits'" :text="lang.get('credits.project')" />
|
||||
</a>
|
||||
<span> {{ lang.get('credits.license') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="'map-control map-control-icon settings-control map-control-'+(isMobile()?'bottom':'top')" @click="toggleSettingsPanel">
|
||||
<SpotIcon :icon="settingsPanelOpen?'prev':'menu'" />
|
||||
</div>
|
||||
<div v-if="currProject && !isMobile()" id="legend" class="map-control settings-control map-control-bottom">
|
||||
<div v-for="(color, hikeType) in hikes.colors" class="track">
|
||||
<span class="line" :style="'background-color:'+color+'; height:'+hikes.width+'px;'"></span>
|
||||
<span class="desc">{{ lang.get('track.'+hikeType) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="currProject" id="title" :class="'map-control settings-control map-control-'+(isMobile()?'bottom':'top')">
|
||||
<span>{{ currProject.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="feed" class="map-container map-container-right" ref="feed" @touchstart.passive="onFeedTouchStart" @touchend.passive="onFeedTouchEnd">
|
||||
<Simplebar id="feed-panel" class="map-panel" ref="feedSimpleBar">
|
||||
<div id="feed-header">
|
||||
<ProjectPost v-if="modeHisto" :options="{type: 'archived', headerless: true}" />
|
||||
<ProjectPost v-else :options="{type: 'poster', relative_time: lang.get('post.new_message')}" />
|
||||
</div>
|
||||
<div v-if="currProject" id="feed-posts">
|
||||
<ProjectPost v-for="post in posts" :options="post" ref="posts" />
|
||||
</div>
|
||||
<div id="feed-footer" v-if="feed.loading">
|
||||
<ProjectPost :options="{type: 'loading', headerless: true}" />
|
||||
</div>
|
||||
</Simplebar>
|
||||
<div v-if="currProject" :class="'map-control map-control-icon feed-control map-control-'+(isMobile()?'bottom':'top')" @click="toggleFeedPanel">
|
||||
<SpotIcon :icon="feedPanelOpen?'next':'post'" />
|
||||
</div>
|
||||
</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('settings', bIsOpen, iAnimDuration)"
|
||||
/>
|
||||
<ProjectFeed
|
||||
:ref="setFeed"
|
||||
:project="project"
|
||||
:mode-histo="modeHisto"
|
||||
@request-last-update="settings?.setLastUpdate"
|
||||
@new-markers="addNewMarkers"
|
||||
@toggle="(bIsOpen, iAnimDuration) => onPanelToggle('feed', bIsOpen, iAnimDuration)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
216
src/components/projectFeed.vue
Normal file
216
src/components/projectFeed.vue
Normal file
@@ -0,0 +1,216 @@
|
||||
<script>
|
||||
import Simplebar from 'simplebar-vue';
|
||||
|
||||
import SpotIcon from '@components/spotIcon';
|
||||
import ProjectPost from '@components/projectPost';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SpotIcon,
|
||||
ProjectPost,
|
||||
Simplebar
|
||||
},
|
||||
props: {
|
||||
project: Object,
|
||||
modeHisto: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
updatable: true,
|
||||
outOfData: false,
|
||||
refIdFirst: 0,
|
||||
refIdLast: 0,
|
||||
firstChunk: true,
|
||||
isOpen: false,
|
||||
posts: [],
|
||||
refreshRate: 60,
|
||||
swipe: {x: null, y: null}
|
||||
};
|
||||
},
|
||||
emits: ['request-last-update', 'new-markers', 'toggle'],
|
||||
inject: ['api', 'lang', 'consts', 'isMobile'],
|
||||
provide() {
|
||||
return {
|
||||
feed: {
|
||||
checkNewFeed: this.checkNewFeed
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
project() {
|
||||
this.syncUpdateTimer();
|
||||
},
|
||||
modeHisto() {
|
||||
this.syncUpdateTimer();
|
||||
},
|
||||
firstChunk() {
|
||||
this.syncUpdateTimer();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getScrollElement()?.addEventListener('scroll', this.onFeedScroll);
|
||||
this.syncUpdateTimer();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.getScrollElement()?.removeEventListener('scroll', this.onFeedScroll);
|
||||
this.setUpdateTimer(-1);
|
||||
},
|
||||
methods: {
|
||||
async init() {
|
||||
this.setUpdateTimer(-1);
|
||||
this.loading = false;
|
||||
this.updatable = true;
|
||||
this.outOfData = false;
|
||||
this.refIdFirst = 0;
|
||||
this.refIdLast = 0;
|
||||
this.firstChunk = true;
|
||||
this.posts = [];
|
||||
this.swipe = {x: null, y: null};
|
||||
|
||||
await this.$nextTick();
|
||||
this.toggle(!this.isMobile(), 0);
|
||||
|
||||
await this.getNextFeed();
|
||||
this.getScrollElement().scrollTop = 0;
|
||||
this.syncUpdateTimer();
|
||||
},
|
||||
getScrollElement() {
|
||||
return this.$refs.feedSimpleBar?.scrollElement;
|
||||
},
|
||||
async findPost(sPostType, iPostId) {
|
||||
let vPost = await this.goToPost(sPostType, iPostId);
|
||||
if(vPost) {
|
||||
await vPost.executeMainAction(0);
|
||||
return vPost;
|
||||
}
|
||||
else if(!this.outOfData) {
|
||||
await this.getNextFeed();
|
||||
await this.$nextTick();
|
||||
return this.findPost(sPostType, iPostId);
|
||||
}
|
||||
else console.log('Missing element ID "'+iPostId+'" of type "'+sPostType+'"');
|
||||
return null;
|
||||
},
|
||||
async goToPost(sPostType, iPostId) {
|
||||
let avPosts = this.$refs.posts.filter((post) => {return post.postId == sPostType+'-'+iPostId;});
|
||||
if(avPosts.length == 0) return null;
|
||||
|
||||
//Force next update to have enough subsequent elements to position the post on top of the page
|
||||
await this.getNextFeed();
|
||||
|
||||
let vPost = avPosts[0];
|
||||
this.getScrollElement().scrollTop += Math.round(
|
||||
vPost.$el.getBoundingClientRect().top
|
||||
+ window.pageYOffset
|
||||
- parseFloat(getComputedStyle(this.$refs.feedSimpleBar.$el).paddingTop)
|
||||
);
|
||||
|
||||
return vPost;
|
||||
},
|
||||
async getNextFeed() {
|
||||
if(!this.project || this.outOfData || this.loading) return true;
|
||||
|
||||
//Get next chunk
|
||||
this.loading = true;
|
||||
let aoData = await this.api.get('next_feed', {id_project: this.project.id, id: this.refIdLast});
|
||||
let iPostCount = Object.keys(aoData.feed).length;
|
||||
|
||||
//Update pointers
|
||||
this.outOfData = (iPostCount < this.consts.chunk_size);
|
||||
if(iPostCount > 0) {
|
||||
this.refIdLast = aoData.ref_id_last;
|
||||
if(this.firstChunk) this.refIdFirst = aoData.ref_id_first;
|
||||
}
|
||||
|
||||
//Add posts
|
||||
this.posts.push(...aoData.feed);
|
||||
|
||||
this.loading = false;
|
||||
this.firstChunk = false;
|
||||
|
||||
return true;
|
||||
},
|
||||
onFeedScroll(oEvent) {
|
||||
const box = oEvent.currentTarget;
|
||||
const content = box.querySelector('.simplebar-content');
|
||||
|
||||
if((box.scrollTop + box.clientHeight) / (content?.offsetHeight || 1) >= 0.8) this.getNextFeed();
|
||||
},
|
||||
onTouchStart(oEvent) {
|
||||
if(!this.isMobile() || !this.isOpen || oEvent.touches.length != 1) return;
|
||||
|
||||
const oTouch = oEvent.touches[0];
|
||||
this.swipe = {x: oTouch.clientX, y: oTouch.clientY};
|
||||
},
|
||||
onTouchEnd(oEvent) {
|
||||
const oTouch = oEvent.changedTouches[0];
|
||||
if(!oTouch || this.swipe.x === null) return;
|
||||
|
||||
const iDeltaX = oTouch.clientX - this.swipe.x;
|
||||
const iDeltaY = oTouch.clientY - this.swipe.y;
|
||||
|
||||
if(iDeltaX > 80 && Math.abs(iDeltaX) > Math.abs(iDeltaY) * 1.5) this.toggle();
|
||||
|
||||
this.swipe = {x: null, y: null};
|
||||
},
|
||||
setUpdateTimer(iSeconds) {
|
||||
if(typeof this.feedTimer != 'undefined') clearTimeout(this.feedTimer);
|
||||
if(iSeconds >= 0) this.feedTimer = setTimeout(this.onUpdateTimer, iSeconds * 1000);
|
||||
},
|
||||
syncUpdateTimer() {
|
||||
this.setUpdateTimer((!!this.project && !this.modeHisto && !this.firstChunk)?this.refreshRate:-1);
|
||||
},
|
||||
async onUpdateTimer() {
|
||||
await this.checkNewFeed();
|
||||
this.syncUpdateTimer();
|
||||
},
|
||||
async checkNewFeed() {
|
||||
if(!this.project) return;
|
||||
|
||||
let aoData = await this.api.get('new_feed', {id_project: this.project.id, id: this.refIdFirst});
|
||||
const aoFeed = aoData.feed || [];
|
||||
const aoMarkers = aoData.markers || [];
|
||||
|
||||
if(aoFeed.length > 0) {
|
||||
//Update pointer
|
||||
this.refIdFirst = aoData.ref_id_first;
|
||||
|
||||
//Add new posts
|
||||
this.posts.unshift(...aoFeed);
|
||||
}
|
||||
|
||||
if(aoMarkers.length > 0) this.$emit('new-markers', aoMarkers);
|
||||
this.$emit('request-last-update');
|
||||
},
|
||||
toggle(bShow, iAnimDuration=500) {
|
||||
this.isOpen = (typeof bShow == 'boolean')?bShow:(!this.isOpen);
|
||||
this.$emit('toggle', this.isOpen, iAnimDuration);
|
||||
return this.isOpen;
|
||||
},
|
||||
getWidth() {
|
||||
return this.$el.getBoundingClientRect().width;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="feed" class="map-container map-container-right" @touchstart.passive="onTouchStart" @touchend.passive="onTouchEnd">
|
||||
<Simplebar id="feed-panel" class="map-panel" ref="feedSimpleBar">
|
||||
<div id="feed-header">
|
||||
<ProjectPost v-if="modeHisto" :options="{type: 'archived', headerless: true}" />
|
||||
<ProjectPost v-else :options="{type: 'poster', relative_time: lang.get('post.new_message')}" />
|
||||
</div>
|
||||
<div v-if="project" id="feed-posts">
|
||||
<ProjectPost v-for="post in posts" :options="post" ref="posts" />
|
||||
</div>
|
||||
<div id="feed-footer" v-if="loading">
|
||||
<ProjectPost :options="{type: 'loading', headerless: true}" />
|
||||
</div>
|
||||
</Simplebar>
|
||||
<div v-if="project" :class="'map-control map-control-icon feed-control map-control-'+(isMobile()?'bottom':'top')" @click="toggle">
|
||||
<SpotIcon :icon="isOpen?'next':'post'" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -38,6 +38,7 @@
|
||||
focusZoomLevel: 15
|
||||
};
|
||||
},
|
||||
inject: ['api', 'lang', 'project', 'feed', 'user', 'map', 'hash', 'consts', 'isMobile'],
|
||||
computed: {
|
||||
postClass() {
|
||||
let sHeaderLess = this.options.headerless?' headerless':'';
|
||||
@@ -59,10 +60,10 @@
|
||||
return this.mouseOverDrill?null:'footprint';
|
||||
},
|
||||
anchorLink() {
|
||||
return '#'+[this.hash.page, this.project.currProject.codename, this.options.type, this.options.id].join(this.consts.hash_sep);
|
||||
return '#'+[this.hash.page, this.project.project.codename, this.options.type, this.options.id].join(this.consts.hash_sep);
|
||||
},
|
||||
modeHisto() {
|
||||
return (this.project?.currProject?.mode == this.consts.modes.histo);
|
||||
return (this.project?.project?.mode == this.consts.modes.histo);
|
||||
},
|
||||
relTime() {
|
||||
return this.modeHisto?(this.options.formatted_time || '').substr(0, 10):this.options.relative_time;
|
||||
@@ -90,7 +91,6 @@
|
||||
return new LngLat(oRelatedMarker.longitude, oRelatedMarker.latitude);
|
||||
}
|
||||
},
|
||||
inject: ['api', 'lang', 'project', 'user', 'map', 'hash', 'consts', 'isMobile'],
|
||||
methods: {
|
||||
copyAnchor() {
|
||||
copyTextToClipboard(this.consts.server+this.anchorLink);
|
||||
@@ -106,8 +106,8 @@
|
||||
|
||||
this.popupRequested = true;
|
||||
|
||||
if(this.isMobile()) this.project.toggleFeedPanel(false, 'panToInstant');
|
||||
this.hash.items = [this.project.currProject.codename, this.options.type, this.options.id];
|
||||
if(this.isMobile()) this.feed.toggle(false);
|
||||
this.hash.items = [this.project.project.codename, this.options.type, this.options.id];
|
||||
|
||||
return this.map.panToBetweenPanels(this.relatedMarkerLatLng, this.focusZoomLevel, iAnimDuration).then(() => {
|
||||
this.openMarkerPopup();
|
||||
@@ -137,14 +137,14 @@
|
||||
this.api.get(
|
||||
'add_post',
|
||||
{
|
||||
id_project: this.project.currProject.id,
|
||||
id_project: this.project.project.id,
|
||||
name: this.user.name,
|
||||
content: this.postMessage
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
this.postMessage = '';
|
||||
this.project.checkNewFeed();
|
||||
this.feed.checkNewFeed();
|
||||
this.sending = false;
|
||||
})
|
||||
.catch((sDesc) => {
|
||||
|
||||
142
src/components/projectSettings.vue
Normal file
142
src/components/projectSettings.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<script>
|
||||
import Simplebar from 'simplebar-vue';
|
||||
|
||||
import SpotIcon from '@components/spotIcon';
|
||||
import ProjectNewsletter from '@components/projectNewsletter';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SpotIcon,
|
||||
ProjectNewsletter,
|
||||
Simplebar
|
||||
},
|
||||
props: {
|
||||
projects: Array,
|
||||
projectCodeName: String,
|
||||
baseMaps: Array,
|
||||
baseMap: String,
|
||||
mapInitializing: Boolean,
|
||||
hikes: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isOpen: false,
|
||||
lastUpdate: {unix_time: 0, relative_time: '', formatted_time: ''}
|
||||
};
|
||||
},
|
||||
emits: ['update:baseMap', 'update:projectCodeName', 'toggle'],
|
||||
inject: ['api', 'lang', 'user', 'consts', 'isMobile'],
|
||||
computed: {
|
||||
project() {
|
||||
return this.projects.find((project) => project.codename == this.projectCodeName);
|
||||
},
|
||||
projectCodeNameModel: {
|
||||
get() {
|
||||
return this.projectCodeName;
|
||||
},
|
||||
set(sProjectCodeName) {
|
||||
this.$emit('update:projectCodeName', sProjectCodeName);
|
||||
}
|
||||
},
|
||||
baseMapModel: {
|
||||
get() {
|
||||
return this.baseMap;
|
||||
},
|
||||
set(sBaseMap) {
|
||||
this.$emit('update:baseMap', sBaseMap);
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
projectCodeName() {
|
||||
this.setLastUpdate();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.setLastUpdate();
|
||||
},
|
||||
methods: {
|
||||
async setLastUpdate() {
|
||||
if(this.project?.mode == this.consts.modes.blog) {
|
||||
this.lastUpdate = await this.api.get('last_update', {id_project: this.project.id});
|
||||
}
|
||||
},
|
||||
toggle(bShow, iAnimDuration=500) {
|
||||
this.isOpen = (typeof bShow == 'boolean')?bShow:(!this.isOpen);
|
||||
this.$emit('toggle', this.isOpen, iAnimDuration);
|
||||
return this.isOpen;
|
||||
},
|
||||
getWidth() {
|
||||
return this.$el.getBoundingClientRect().width;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="settings" class="map-container map-container-left">
|
||||
<div id="settings-panel" class="map-panel">
|
||||
<div class="settings-header">
|
||||
<div class="logo"><img width="289" height="72" src="images/logo_black.png" alt="Spotty" /></div>
|
||||
<div id="last_update" v-if="project?.mode == consts.modes.blog && lastUpdate.unix_time > 0">
|
||||
<p><span><img src="images/spot-logo-only.svg" alt="" /></span><abbr :title="lastUpdate.formatted_time">{{ lang.get('feed.last_update')+' '+lastUpdate.relative_time }}</abbr></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-sections">
|
||||
<Simplebar id="settings-sections-scrollbox">
|
||||
<div class="settings-section">
|
||||
<h1><SpotIcon :icon="'project'" width="fixed" :text="lang.get('project.hikes')" /></h1>
|
||||
<div class="settings-section-body">
|
||||
<div class="radio" v-for="project in projects" :key="'project-'+project.id">
|
||||
<input type="radio" :id="'project-'+project.id" :value="project.codename" v-model="projectCodeNameModel" :disabled="mapInitializing" />
|
||||
<label :for="'project-'+project.id">
|
||||
<span>{{ project.name }}</span>
|
||||
<a v-if="project.gpxfilepath" class="download" :href="project.gpxfilepath" :download="project.codename + '.gpx'" :title="lang.get('track.download')" @click.stop="()=>{}">
|
||||
<SpotIcon :icon="'download'" margin="left" />
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<h1><SpotIcon :icon="'map'" width="fixed" :text="lang.get('map.title')" /></h1>
|
||||
<div class="settings-section-body">
|
||||
<div class="radio" v-for="bm in baseMaps" :key="'map-'+bm.id_map">
|
||||
<input type="radio" :id="'map-'+bm.id_map" :value="bm.codename" v-model="baseMapModel" :disabled="mapInitializing" />
|
||||
<label :for="'map-'+bm.id_map">{{ lang.get('map.'+bm.codename) }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-section newsletter">
|
||||
<ProjectNewsletter />
|
||||
</div>
|
||||
<div class="settings-section admin" v-if="user.hasClearance(consts.clearances.admin)">
|
||||
<h1><SpotIcon :icon="'admin'" width="fixed" :text="lang.get('admin.title')" /></h1>
|
||||
<div class="admin-actions">
|
||||
<a class="button" href="#admin"><SpotIcon :icon="'config'" :text="lang.get('admin.config')" /></a>
|
||||
<a class="button" href="#upload"><SpotIcon :icon="'upload'" :text="lang.get('admin.upload')" /></a>
|
||||
</div>
|
||||
</div>
|
||||
</Simplebar>
|
||||
</div>
|
||||
<div class="settings-footer">
|
||||
<a href="https://git.lutran.fr/franzz/spot" :title="lang.get('credits.git')" target="_blank" rel="noopener">
|
||||
<SpotIcon :icon="'credits'" :text="lang.get('credits.project')" />
|
||||
</a>
|
||||
<span> {{ lang.get('credits.license') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="'map-control map-control-icon settings-control map-control-'+(isMobile()?'bottom':'top')" @click="toggle">
|
||||
<SpotIcon :icon="isOpen?'prev':'menu'" />
|
||||
</div>
|
||||
<div v-if="project?.id && !isMobile()" id="legend" class="map-control settings-control map-control-bottom">
|
||||
<div v-for="(color, hikeType) in hikes.colors" class="track">
|
||||
<span class="line" :style="'background-color:'+color+'; height:'+hikes.width+'px;'"></span>
|
||||
<span class="desc">{{ lang.get('track.'+hikeType) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="project?.id" id="title" :class="'map-control settings-control map-control-'+(isMobile()?'bottom':'top')">
|
||||
<span>{{ project.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -10,7 +10,7 @@
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#projects {
|
||||
.projects {
|
||||
.map-container {
|
||||
width: calc(#{$panel-width});
|
||||
max-width: calc(#{$panel-width});
|
||||
|
||||
@@ -6,7 +6,7 @@ $panel-width: 30vw;
|
||||
$panel-width-max: "400px + 3 * #{var.$block-spacing}";
|
||||
$panel-actual-width: min($panel-width, #{$panel-width-max});
|
||||
|
||||
#projects {
|
||||
.projects {
|
||||
&.with-feed, &.with-settings {
|
||||
#title {
|
||||
max-width: calc(100vw - var.$block-spacing - $panel-actual-width - (var.$button-width + var.$block-spacing * 2) * 2);
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
|
||||
@use '@styles/page.project.map' as map;
|
||||
@use '@styles/page.project.panel' as panel;
|
||||
@use '@styles/page.project.feed' as feed;
|
||||
@use '@styles/page.project.settings' as settings;
|
||||
@use '@styles/page.project.panel.feed' as feed;
|
||||
@use '@styles/page.project.panel.settings' as settings;
|
||||
|
||||
#projects {
|
||||
.projects {
|
||||
--space: #{color.$space};
|
||||
--horizon: #{color.$horizon};
|
||||
--track-main: #{color.$main-track};
|
||||
|
||||
Reference in New Issue
Block a user