Compare commits
32 Commits
3611f2206f
...
vue
| Author | SHA1 | Date | |
|---|---|---|---|
| d9bc89b7f6 | |||
| 760f38374f | |||
| b9a4bd6d2d | |||
| ea14a1ef3e | |||
| 457bab2c18 | |||
| 3571f93e41 | |||
| e878b159bf | |||
| db70593852 | |||
| 73b8e6b04f | |||
| a49f73236b | |||
| c0b7ad8000 | |||
| 83bf47287c | |||
| 4ce96e7192 | |||
| 3169b8e83e | |||
| 205855acd8 | |||
| 356d8ccd7e | |||
| 25ff80ad7a | |||
| 0cd509a99d | |||
| 3063f8b904 | |||
| 59dea2917d | |||
| 6e614042d1 | |||
| 8c812f6b0a | |||
| abacab8206 | |||
| 869b084d70 | |||
| cab899e544 | |||
| b6fc305111 | |||
| 30a81b5341 | |||
| 683670f77a | |||
| c2956ac373 | |||
| 7853c6e285 | |||
| f674b0d934 | |||
| d767e335f9 |
@@ -4,6 +4,7 @@ var webpack = require("webpack");
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||
const SymlinkWebpackPlugin = require('symlink-webpack-plugin');
|
||||
const { VueLoaderPlugin } = require('vue-loader')
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
@@ -16,29 +17,28 @@ module.exports = {
|
||||
},
|
||||
devtool: "inline-source-map",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
rules: [{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader'
|
||||
}, {
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
exclude: file => (/node_modules/.test(file) && !/\.vue\.js/.test(file)),
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/preset-env'],
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
}, {
|
||||
test: /\.html$/i,
|
||||
loader: "html-loader",
|
||||
},
|
||||
{
|
||||
}, {
|
||||
test: /\.(woff|woff2|eot|ttf|otf)$/i,
|
||||
type: 'asset/resource'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
test: /\.s[ac]ss$/i,
|
||||
use: [
|
||||
'style-loader',
|
||||
'vue-style-loader',
|
||||
'css-loader',
|
||||
'resolve-url-loader',
|
||||
{
|
||||
@@ -49,12 +49,10 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
}, {
|
||||
test: /\.css$/i,
|
||||
use: ["style-loader", "css-loader"],
|
||||
},
|
||||
{
|
||||
use: ["vue-style-loader", "css-loader"],
|
||||
}, {
|
||||
test: /\.(png|svg|jpg|jpeg|gif)$/i,
|
||||
use: {
|
||||
loader: "url-loader",
|
||||
@@ -76,23 +74,32 @@ module.exports = {
|
||||
plugins: [
|
||||
new webpack.ProvidePlugin({
|
||||
$: require.resolve('jquery'),
|
||||
jQuery: require.resolve('jquery'),
|
||||
//L: require.resolve('leaflet')
|
||||
jQuery: require.resolve('jquery')
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [{
|
||||
patterns: [/*{
|
||||
from: 'geo/',
|
||||
to: path.resolve(DIST, 'geo')
|
||||
}, {
|
||||
from: path.resolve(LIB, 'index.php'),
|
||||
to: 'index.php'
|
||||
}, {
|
||||
from: path.resolve(SRC, 'images/icons'),
|
||||
to: 'images/icons'
|
||||
}, */{
|
||||
from: path.resolve(LIB, 'index.php'),
|
||||
to: 'index.php'
|
||||
}]
|
||||
}),
|
||||
new SymlinkWebpackPlugin({ origin: '../files/', symlink: 'files' }),
|
||||
new CleanWebpackPlugin()
|
||||
new SymlinkWebpackPlugin([
|
||||
{ origin: '../files/', symlink: 'files' },
|
||||
{ origin: '../geo/', symlink: 'geo' },
|
||||
{ origin: '../src/images/icons/', symlink: 'images/icons' }
|
||||
]),
|
||||
new CleanWebpackPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
__VUE_OPTIONS_API__: 'true',
|
||||
__VUE_PROD_DEVTOOLS__: 'false',
|
||||
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false'
|
||||
}),
|
||||
new VueLoaderPlugin()
|
||||
],
|
||||
resolve: {
|
||||
extensions: ['', '.js'],
|
||||
|
||||
21
composer.lock
generated
21
composer.lock
generated
@@ -12,7 +12,7 @@
|
||||
"dist": {
|
||||
"type": "path",
|
||||
"url": "../objects",
|
||||
"reference": "a9f8601384a0078cf8531576768e2c5bad4bbf95"
|
||||
"reference": "bcae723140735b1432caaf3070ef4e29ecb73a76"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
@@ -27,16 +27,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpmailer/phpmailer",
|
||||
"version": "v6.8.1",
|
||||
"version": "v6.10.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
||||
"reference": "e88da8d679acc3824ff231fdc553565b802ac016"
|
||||
"reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/e88da8d679acc3824ff231fdc553565b802ac016",
|
||||
"reference": "e88da8d679acc3824ff231fdc553565b802ac016",
|
||||
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
|
||||
"reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -56,6 +56,7 @@
|
||||
"yoast/phpunit-polyfills": "^1.0.4"
|
||||
},
|
||||
"suggest": {
|
||||
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
|
||||
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
|
||||
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
|
||||
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
|
||||
@@ -95,7 +96,7 @@
|
||||
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
|
||||
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.8.1"
|
||||
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.10.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -103,7 +104,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2023-08-29T08:26:30+00:00"
|
||||
"time": "2025-04-24T15:19:31+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
@@ -114,7 +115,7 @@
|
||||
},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": [],
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.3.0"
|
||||
"platform": {},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
||||
5
config/db/update_v21_to_v22.sql
Normal file
5
config/db/update_v21_to_v22.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE mappings ADD COLUMN default_map BOOLEAN DEFAULT 0 AFTER id_project;
|
||||
ALTER TABLE mappings ADD CONSTRAINT default_on_generic_map_only CHECK (default_map = 0 OR id_project IS NULL);
|
||||
UPDATE mappings SET default_map = 1 WHERE id_map = (select id_map from maps where codename = 'satellite');
|
||||
UPDATE maps SET token = substring(pattern, locate('token=', pattern) + 6) WHERE codename = 'static_marker';
|
||||
UPDATE maps SET pattern = replace(pattern, token, '{token}') WHERE codename = 'static_marker';
|
||||
@@ -8,7 +8,7 @@ class Settings
|
||||
const DB_NAME = 'spot';
|
||||
const DB_ENC = 'utf8mb4';
|
||||
const TEXT_ENC = 'UTF-8';
|
||||
const TIMEZONE = 'Europe/Paris';
|
||||
const TIMEZONE = 'Europe/Zurich';
|
||||
const MAIL_SERVER = '';
|
||||
const MAIL_FROM = '';
|
||||
const MAIL_USER = '';
|
||||
|
||||
69304
geo/snt.gpx
Executable file
69304
geo/snt.gpx
Executable file
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,9 @@ admin_config = Config
|
||||
admin_upload = Upload
|
||||
save = Save
|
||||
admin_save_success = Saved
|
||||
admin_create_success= Created
|
||||
admin_delete_success= Deleted
|
||||
no_auth = No authorization
|
||||
|
||||
track_main = Main track
|
||||
track_off-track = Off-track
|
||||
@@ -21,6 +24,7 @@ upload_title = Picture & Video Uploads
|
||||
upload_mode_archived= Project "$0" is archived. No upload allowed
|
||||
upload_success = $0 uploaded successfully
|
||||
upload_media_exist = Picture $0 already exists
|
||||
new_position = New Position
|
||||
|
||||
post_message = Message
|
||||
post_name = Name
|
||||
@@ -64,7 +68,7 @@ id_project = Project ID
|
||||
project = Project
|
||||
projects = Projects
|
||||
new_project = New Project
|
||||
update_project = Update Project
|
||||
update_project = Update project messages
|
||||
hikes = Hikes
|
||||
mode = Mode
|
||||
mode_previz = Project in preparation
|
||||
@@ -76,6 +80,7 @@ end = End
|
||||
feeds = Feeds
|
||||
id_feed = Feed ID
|
||||
ref_feed_id = Ref. Feed ID
|
||||
new_feed = New feed
|
||||
id_spot = Spot ID
|
||||
name = Name
|
||||
status = Status
|
||||
|
||||
@@ -11,6 +11,9 @@ admin_config = Configuración
|
||||
admin_upload = Cargar
|
||||
save = Guardar
|
||||
admin_save_success = Guardado
|
||||
admin_create_success= Creado
|
||||
admin_delete_success= Eliminado
|
||||
no_auth = No autorización
|
||||
|
||||
track_main = Camino principal
|
||||
track_off-track = Variante
|
||||
@@ -21,6 +24,7 @@ upload_title = Cargar fotos y videos
|
||||
upload_mode_archived= El proyecto "$0" esta archivado. No se puede cargar
|
||||
upload_success = $0 ha sido subido
|
||||
upload_media_exist = La imagen $0 ya existe
|
||||
new_position = Nueva posición
|
||||
|
||||
post_message = Mensaje
|
||||
post_name = Nombre
|
||||
@@ -64,7 +68,7 @@ id_project = Proyecto ID
|
||||
project = Proyecto
|
||||
projects = Proyectos
|
||||
new_project = Nuevo proyecto
|
||||
update_project = Actualizar el proyecto
|
||||
update_project = Actualizar los mensajes del proyecto
|
||||
hikes = Senderos
|
||||
mode = Modo
|
||||
mode_previz = Proyecto en preparación
|
||||
@@ -76,6 +80,7 @@ end = Fin
|
||||
feeds = Feeds
|
||||
id_feed = ID Feed
|
||||
ref_feed_id = ID Feed ref.
|
||||
new_feed = Nuevo feed
|
||||
id_spot = ID Spot
|
||||
name = Descripción
|
||||
status = Estado
|
||||
|
||||
@@ -11,6 +11,9 @@ admin_config = Paramètres
|
||||
admin_upload = Uploader
|
||||
save = Sauvegarder
|
||||
admin_save_success = Sauvegardé
|
||||
admin_create_success= Créé
|
||||
admin_delete_success= Supprimé
|
||||
no_auth = Pas d'authorisation
|
||||
|
||||
track_main = Trajet principal
|
||||
track_off-track = Variante
|
||||
@@ -21,6 +24,7 @@ upload_title = Uploader photos & vidéos
|
||||
upload_mode_archived= Le projet "$0" a été archivé. Aucun upload possible
|
||||
upload_success = $0 a été uploadé
|
||||
upload_media_exist = l'image $0 existe déjà
|
||||
new_position = Nouvelle position
|
||||
|
||||
post_message = Message
|
||||
post_name = Nom
|
||||
@@ -64,7 +68,7 @@ id_project = ID projet
|
||||
project = Projet
|
||||
projects = Projets
|
||||
new_project = Nouveau projet
|
||||
update_project = Mettre à jour le projet
|
||||
update_project = Mettre à jour les messages du projet
|
||||
hikes = Randonnées
|
||||
mode = Mode
|
||||
mode_previz = Projet en cours de préparation
|
||||
@@ -76,6 +80,7 @@ end = Arrivée
|
||||
feeds = Feeds
|
||||
id_feed = ID Feed
|
||||
ref_feed_id = ID Feed ref.
|
||||
new_feed = Nouveau feed
|
||||
id_spot = ID Spot
|
||||
name = Description
|
||||
status = Statut
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
namespace Franzz\Spot;
|
||||
use Franzz\Objects\PhpObject;
|
||||
use Franzz\Objects\ToolBox;
|
||||
use \Settings;
|
||||
|
||||
/**
|
||||
* GPX to GeoJSON Converter
|
||||
@@ -35,298 +33,10 @@ class Converter extends PhpObject {
|
||||
}
|
||||
|
||||
public static function isGeoJsonValid($sCodeName) {
|
||||
$bResult = false;
|
||||
$sGpxFilePath = Gpx::getFilePath($sCodeName);
|
||||
$sGeoJsonFilePath = GeoJson::getFilePath($sCodeName);
|
||||
|
||||
//No need to generate if gpx is missing
|
||||
if(!file_exists($sGpxFilePath) || file_exists($sGeoJsonFilePath) && filemtime($sGeoJsonFilePath) > filemtime(Gpx::getFilePath($sCodeName))) $bResult = true;
|
||||
return $bResult;
|
||||
return !file_exists($sGpxFilePath) || file_exists($sGeoJsonFilePath) && filemtime($sGeoJsonFilePath) >= filemtime($sGpxFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
class Geo extends PhpObject {
|
||||
|
||||
const GEO_FOLDER = '../geo/';
|
||||
const OPT_SIMPLE = 'simplification';
|
||||
|
||||
protected $asTracks;
|
||||
protected $sFilePath;
|
||||
|
||||
public function __construct($sCodeName) {
|
||||
parent::__construct(get_class($this), Settings::DEBUG, PhpObject::MODE_HTML);
|
||||
$this->sFilePath = self::getFilePath($sCodeName);
|
||||
$this->asTracks = array();
|
||||
}
|
||||
|
||||
public static function getFilePath($sCodeName) {
|
||||
return self::GEO_FOLDER.$sCodeName.static::EXT;
|
||||
}
|
||||
|
||||
public static function getDistFilePath($sCodeName) {
|
||||
return 'geo/'.$sCodeName.static::EXT;
|
||||
}
|
||||
|
||||
public function getLog() {
|
||||
return $this->getCleanMessageStack(PhpObject::NOTICE_TAB);
|
||||
}
|
||||
}
|
||||
|
||||
class Gpx extends Geo {
|
||||
|
||||
const EXT = '.gpx';
|
||||
|
||||
public function __construct($sCodeName) {
|
||||
parent::__construct($sCodeName);
|
||||
$this->parseFile();
|
||||
}
|
||||
|
||||
public function getTracks() {
|
||||
return $this->asTracks;
|
||||
}
|
||||
|
||||
private function parseFile() {
|
||||
$this->addNotice('Parsing: '.$this->sFilePath);
|
||||
if(!file_exists($this->sFilePath)) $this->addError($this->sFilePath.' file missing');
|
||||
else {
|
||||
$oXml = simplexml_load_file($this->sFilePath);
|
||||
|
||||
//Tracks
|
||||
$this->addNotice('Converting '.count($oXml->trk).' tracks');
|
||||
foreach($oXml->trk as $aoTrack) {
|
||||
$asTrack = array(
|
||||
'name' => (string) $aoTrack->name,
|
||||
'desc' => str_replace("\n", '', ToolBox::fixEOL((strip_tags($aoTrack->desc)))),
|
||||
'cmt' => ToolBox::fixEOL((strip_tags($aoTrack->cmt))),
|
||||
'color' => (string) $aoTrack->extensions->children('gpxx', true)->TrackExtension->DisplayColor,
|
||||
'points'=> array()
|
||||
);
|
||||
|
||||
foreach($aoTrack->trkseg as $asSegment) {
|
||||
foreach($asSegment as $asPoint) {
|
||||
$asTrack['points'][] = array(
|
||||
'lon' => (float) $asPoint['lon'],
|
||||
'lat' => (float) $asPoint['lat'],
|
||||
'ele' => (int) $asPoint->ele
|
||||
);
|
||||
}
|
||||
}
|
||||
$this->asTracks[] = $asTrack;
|
||||
}
|
||||
|
||||
//Waypoints
|
||||
$this->addNotice('Ignoring '.count($oXml->wpt).' waypoints');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GeoJson extends Geo {
|
||||
|
||||
const EXT = '.geojson';
|
||||
const MAX_FILESIZE = 2; //MB
|
||||
const MAX_DEVIATION_FLAT = 0.1; //10%
|
||||
const MAX_DEVIATION_ELEV = 0.1; //10%
|
||||
|
||||
public function __construct($sCodeName) {
|
||||
parent::__construct($sCodeName);
|
||||
}
|
||||
|
||||
public function saveFile() {
|
||||
$this->addNotice('Saving '.$this->sFilePath);
|
||||
file_put_contents($this->sFilePath, $this->buildGeoJson());
|
||||
}
|
||||
|
||||
public function isSimplicationRequired() {
|
||||
//Size in bytes
|
||||
$iFileSize = strlen($this->buildGeoJson());
|
||||
|
||||
//Convert to MB
|
||||
$iFileSize = round($iFileSize / pow(1024, 2), 2);
|
||||
|
||||
//Compare with max allowed size
|
||||
$bFileTooLarge = ($iFileSize > self::MAX_FILESIZE);
|
||||
if($bFileTooLarge) $this->addNotice('Output file is too large ('.$iFileSize.'MB > '.self::MAX_FILESIZE.'MB)');
|
||||
|
||||
return $bFileTooLarge;
|
||||
}
|
||||
|
||||
public function buildTracks($asTracks, $bSimplify=false) {
|
||||
$this->addNotice('Creating '.($bSimplify?'Simplified ':'').'GeoJson Tracks');
|
||||
|
||||
$iGlobalInvalidPointCount = 0;
|
||||
$iGlobalPointCount = 0;
|
||||
|
||||
$this->asTracks = array();
|
||||
foreach($asTracks as $asTrackProps) {
|
||||
$asOptions = $this->parseOptions($asTrackProps['cmt']);
|
||||
|
||||
//Color mapping
|
||||
switch($asTrackProps['color']) {
|
||||
case 'DarkBlue':
|
||||
$sType = 'main';
|
||||
break;
|
||||
case 'Magenta':
|
||||
if($bSimplify && $asOptions[self::OPT_SIMPLE]!='keep') {
|
||||
$this->addNotice('Ignoring Track "'.$asTrackProps['name'].' (off-track)');
|
||||
continue 2; //discard tracks
|
||||
}
|
||||
else {
|
||||
$sType = 'off-track';
|
||||
break;
|
||||
}
|
||||
case 'Red':
|
||||
$sType = 'hitchhiking';
|
||||
break;
|
||||
default:
|
||||
$this->addNotice('Ignoring Track "'.$asTrackProps['name'].' (unknown color "'.$asTrackProps['color'].'")');
|
||||
continue 2; //discard tracks
|
||||
}
|
||||
|
||||
$asTrack = array(
|
||||
'type' => 'Feature',
|
||||
'properties' => array(
|
||||
'name' => $asTrackProps['name'],
|
||||
'type' => $sType,
|
||||
'description' => $asTrackProps['desc']
|
||||
),
|
||||
'geometry' => array(
|
||||
'type' => 'LineString',
|
||||
'coordinates' => array()
|
||||
)
|
||||
);
|
||||
|
||||
//Track points
|
||||
$asTrackPoints = $asTrackProps['points'];
|
||||
$iPointCount = count($asTrackPoints);
|
||||
$iInvalidPointCount = 0;
|
||||
$asPrevPoint = array();
|
||||
foreach($asTrackPoints as $iIndex=>$asPoint) {
|
||||
$asNextPoint = ($iIndex < ($iPointCount - 1))?$asTrackPoints[$iIndex + 1]:array();
|
||||
if($bSimplify && !empty($asPrevPoint) && !empty($asNextPoint)) {
|
||||
if(!$this->isPointValid($asPrevPoint, $asPoint, $asNextPoint)) {
|
||||
$iInvalidPointCount++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$asTrack['geometry']['coordinates'][] = array_values($asPoint);
|
||||
$asPrevPoint = $asPoint;
|
||||
}
|
||||
$this->asTracks[] = $asTrack;
|
||||
|
||||
$iGlobalInvalidPointCount += $iInvalidPointCount;
|
||||
$iGlobalPointCount += $iPointCount;
|
||||
if($iInvalidPointCount > 0) $this->addNotice('Removing '.$iInvalidPointCount.'/'.$iPointCount.' points ('.round($iInvalidPointCount / $iPointCount * 100, 1).'%) from '.$asTrackProps['name']);
|
||||
}
|
||||
|
||||
if($bSimplify) $this->addNotice('Total: '.$iGlobalInvalidPointCount.'/'.$iGlobalPointCount.' points removed ('.round($iGlobalInvalidPointCount / $iGlobalPointCount * 100, 1).'%)');
|
||||
}
|
||||
|
||||
|
||||
public function sortOffTracks() {
|
||||
$this->addNotice('Sorting off-tracks');
|
||||
|
||||
//Find first & last track points
|
||||
$asTracksEnds = array();
|
||||
$asTracks = array();
|
||||
foreach($this->asTracks as $iTrackId=>$asTrack) {
|
||||
$sTrackId = 't'.$iTrackId;
|
||||
$asTracksEnds[$sTrackId] = array('first'=>reset($asTrack['geometry']['coordinates']), 'last'=>end($asTrack['geometry']['coordinates']));
|
||||
$asTracks[$sTrackId] = $asTrack;
|
||||
}
|
||||
|
||||
//Find variants close-by tracks
|
||||
$asClonedTracks = $asTracks;
|
||||
foreach($asClonedTracks as $sTrackId=>$asTrack) {
|
||||
if($asTrack['properties']['type'] != 'off-track') continue;
|
||||
|
||||
$iMinDistance = INF;
|
||||
$sConnectedTrackId = 0;
|
||||
$iPosition = 0;
|
||||
|
||||
//Test all track ending points to find the closest
|
||||
foreach($asTracksEnds as $sTrackEndId=>$asTrackEnds) {
|
||||
if($sTrackEndId != $sTrackId) {
|
||||
//Calculate distance between the last point of the track and every starting point of other tracks
|
||||
$iDistance = self::getDistance($asTracksEnds[$sTrackId]['last'], $asTrackEnds['first']);
|
||||
if($iDistance < $iMinDistance) {
|
||||
$sConnectedTrackId = $sTrackEndId;
|
||||
$iPosition = 0; //Track before the Connected Track
|
||||
$iMinDistance = $iDistance;
|
||||
}
|
||||
|
||||
//Calculate distance between the first point of the track and every ending point of other tracks
|
||||
$iDistance = self::getDistance($asTracksEnds[$sTrackId]['first'], $asTrackEnds['last']);
|
||||
if($iDistance < $iMinDistance) {
|
||||
$sConnectedTrackId = $sTrackEndId;
|
||||
$iPosition = +1; //Track after the Connected Track
|
||||
$iMinDistance = $iDistance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Move track
|
||||
unset($asTracks[$sTrackId]);
|
||||
$iOffset = array_search($sConnectedTrackId, array_keys($asTracks)) + $iPosition;
|
||||
$asTracks = array_slice($asTracks, 0, $iOffset) + array($sTrackId => $asTrack) + array_slice($asTracks, $iOffset);
|
||||
}
|
||||
|
||||
$this->asTracks = array_values($asTracks);
|
||||
}
|
||||
|
||||
private function parseOptions($sComment){
|
||||
$sComment = strip_tags(html_entity_decode($sComment));
|
||||
$asOptions = array(self::OPT_SIMPLE=>'');
|
||||
foreach(explode("\n", $sComment) as $sLine) {
|
||||
$asOptions[mb_strtolower(trim(mb_strstr($sLine, ':', true)))] = mb_strtolower(trim(mb_substr(mb_strstr($sLine, ':'), 1)));
|
||||
}
|
||||
return $asOptions;
|
||||
}
|
||||
|
||||
private function isPointValid($asPointA, $asPointO, $asPointB) {
|
||||
/* A----O Calculate angle AO^OB
|
||||
* \ If angle is within [90% Pi ; 110% Pi], O can be discarded
|
||||
* \ O is valid otherwise
|
||||
* B
|
||||
*/
|
||||
|
||||
//Path Turn Check -> -> -> ->
|
||||
//Law of Cosines (vector): angle = arccos(OA.OB / ||OA||.||OB||)
|
||||
$fVectorOA = array('lon'=>($asPointA['lon'] - $asPointO['lon']), 'lat'=> ($asPointA['lat'] - $asPointO['lat']));
|
||||
$fVectorOB = array('lon'=>($asPointB['lon'] - $asPointO['lon']), 'lat'=> ($asPointB['lat'] - $asPointO['lat']));
|
||||
|
||||
$fLengthOA = sqrt(pow($asPointA['lon'] - $asPointO['lon'], 2) + pow($asPointA['lat'] - $asPointO['lat'], 2));
|
||||
$fLengthOB = sqrt(pow($asPointO['lon'] - $asPointB['lon'], 2) + pow($asPointO['lat'] - $asPointB['lat'], 2));
|
||||
|
||||
$fVectorOAxOB = $fVectorOA['lon'] * $fVectorOB['lon'] + $fVectorOA['lat'] * $fVectorOB['lat'];
|
||||
$fAngleAOB = ($fLengthOA != 0 && $fLengthOB != 0) ? acos($fVectorOAxOB/($fLengthOA * $fLengthOB)) : 0;
|
||||
|
||||
//Elevation Check
|
||||
//Law of Cosines: angle = arccos((OB² + AO² - AB²) / (2*OB*AO))
|
||||
$fLengthAB = sqrt(pow($asPointB['ele'] - $asPointA['ele'], 2) + pow($fLengthOA + $fLengthOB, 2));
|
||||
$fLengthAO = sqrt(pow($asPointO['ele'] - $asPointA['ele'], 2) + pow($fLengthOA, 2));
|
||||
$fLengthOB = sqrt(pow($asPointB['ele'] - $asPointO['ele'], 2) + pow($fLengthOB, 2));
|
||||
$fAngleAOBElev = ($fLengthOB != 0 && $fLengthAO != 0) ? (acos((pow($fLengthOB, 2) + pow($fLengthAO, 2) - pow($fLengthAB, 2)) / (2 * $fLengthOB * $fLengthAO))) : 0;
|
||||
|
||||
return ($fAngleAOB <= (1 - self::MAX_DEVIATION_FLAT) * M_PI || $fAngleAOB >= (1 + self::MAX_DEVIATION_FLAT) * M_PI ||
|
||||
$fAngleAOBElev <= (1 - self::MAX_DEVIATION_ELEV) * M_PI || $fAngleAOBElev >= (1 + self::MAX_DEVIATION_ELEV) * M_PI);
|
||||
}
|
||||
|
||||
private function buildGeoJson() {
|
||||
return json_encode(array('type'=>'FeatureCollection', 'features'=>$this->asTracks));
|
||||
}
|
||||
|
||||
private static function getDistance($asPointA, $asPointB) {
|
||||
$fLatFrom = $asPointA[1];
|
||||
$fLonFrom = $asPointA[0];
|
||||
$fLatTo = $asPointB[1];
|
||||
$fLonTo = $asPointB[0];
|
||||
|
||||
$fRad = M_PI / 180;
|
||||
|
||||
//Calculate distance from latitude and longitude
|
||||
$fTheta = $fLonFrom - $fLonTo;
|
||||
$fDistance = sin($fLatFrom * $fRad) * sin($fLatTo * $fRad) + cos($fLatFrom * $fRad) * cos($fLatTo * $fRad) * cos($fTheta * $fRad);
|
||||
|
||||
return acos($fDistance) / $fRad * 60 * 1.853;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
lib/Feed.php
25
lib/Feed.php
@@ -168,6 +168,31 @@ class Feed extends PhpObject {
|
||||
return $bNewMsg;
|
||||
}
|
||||
|
||||
public function addManualPosition($sLat, $sLng, $iTimestamp) {
|
||||
$sTimeZone = date_default_timezone_get();
|
||||
$oDateTime = new \DateTime('@'.$iTimestamp);
|
||||
$oDateTime->setTimezone(new \DateTimeZone($sTimeZone));
|
||||
$asWeather = $this->getWeather(array($sLat, $sLng), $iTimestamp);
|
||||
|
||||
$asMsg = [
|
||||
'ref_msg_id' => $iTimestamp.'/man',
|
||||
'id_feed' => $this->getFeedId(),
|
||||
'type' => 'OK',
|
||||
'latitude' => $sLat,
|
||||
'longitude' => $sLng,
|
||||
'iso_time' => $oDateTime->format("Y-m-d\TH:i:sO"), //Incorrect ISO 8601 format, but compliant with Spot data
|
||||
'site_time' => $oDateTime->format(Db::TIMESTAMP_FORMAT),
|
||||
'timezone' => $sTimeZone,
|
||||
'unix_time' => $iTimestamp,
|
||||
'content' => '',
|
||||
'battery_state' => '',
|
||||
'posted_on' => date(Db::TIMESTAMP_FORMAT),
|
||||
];
|
||||
|
||||
$iMessageId = $this->oDb->insertRow(self::MSG_TABLE, array_merge($asMsg, $asWeather));
|
||||
return $iMessageId;
|
||||
}
|
||||
|
||||
private function updateFeed() {
|
||||
$bNewMsg = false;
|
||||
$asData = $this->retrieveFeed();
|
||||
|
||||
32
lib/Geo.php
Normal file
32
lib/Geo.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Franzz\Spot;
|
||||
use Franzz\Objects\PhpObject;
|
||||
use \Settings;
|
||||
|
||||
class Geo extends PhpObject {
|
||||
|
||||
const GEO_FOLDER = '../geo/';
|
||||
const OPT_SIMPLE = 'simplification';
|
||||
|
||||
protected $asTracks;
|
||||
protected $sFilePath;
|
||||
|
||||
public function __construct($sCodeName) {
|
||||
parent::__construct(get_class($this), Settings::DEBUG, PhpObject::MODE_HTML);
|
||||
$this->sFilePath = self::getFilePath($sCodeName);
|
||||
$this->asTracks = array();
|
||||
}
|
||||
|
||||
public static function getFilePath($sCodeName) {
|
||||
return self::GEO_FOLDER.$sCodeName.static::EXT;
|
||||
}
|
||||
|
||||
public static function getDistFilePath($sCodeName) {
|
||||
return 'geo/'.$sCodeName.static::EXT;
|
||||
}
|
||||
|
||||
public function getLog() {
|
||||
return $this->getCleanMessageStack(PhpObject::NOTICE_TAB);
|
||||
}
|
||||
}
|
||||
214
lib/GeoJson.php
Normal file
214
lib/GeoJson.php
Normal file
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
|
||||
namespace Franzz\Spot;
|
||||
|
||||
class GeoJson extends Geo {
|
||||
|
||||
const EXT = '.geojson';
|
||||
const MAX_FILESIZE = 2; //MB
|
||||
const MAX_DEVIATION_FLAT = 0.1; //10%
|
||||
const MAX_DEVIATION_ELEV = 0.1; //10%
|
||||
|
||||
public function __construct($sCodeName) {
|
||||
parent::__construct($sCodeName);
|
||||
}
|
||||
|
||||
public function saveFile() {
|
||||
$this->addNotice('Saving '.$this->sFilePath);
|
||||
file_put_contents($this->sFilePath, $this->buildGeoJson());
|
||||
}
|
||||
|
||||
public function isSimplicationRequired() {
|
||||
//Size in bytes
|
||||
$iFileSize = strlen($this->buildGeoJson());
|
||||
|
||||
//Convert to MB
|
||||
$iFileSize = round($iFileSize / pow(1024, 2), 2);
|
||||
|
||||
//Compare with max allowed size
|
||||
$bFileTooLarge = ($iFileSize > self::MAX_FILESIZE);
|
||||
if($bFileTooLarge) $this->addNotice('Output file is too large ('.$iFileSize.'MB > '.self::MAX_FILESIZE.'MB)');
|
||||
|
||||
return $bFileTooLarge;
|
||||
}
|
||||
|
||||
public function buildTracks($asTracks, $bSimplify=false) {
|
||||
$this->addNotice('Creating '.($bSimplify?'Simplified ':'').'GeoJson Tracks');
|
||||
|
||||
$iGlobalInvalidPointCount = 0;
|
||||
$iGlobalPointCount = 0;
|
||||
|
||||
$this->asTracks = array();
|
||||
foreach($asTracks as $asTrackProps) {
|
||||
$asOptions = $this->parseOptions($asTrackProps['cmt']);
|
||||
|
||||
//Color mapping
|
||||
switch($asTrackProps['color']) {
|
||||
case 'DarkBlue':
|
||||
$sType = 'main';
|
||||
break;
|
||||
case 'Magenta':
|
||||
if($bSimplify && $asOptions[self::OPT_SIMPLE]!='keep') {
|
||||
$this->addNotice('Ignoring Track "'.$asTrackProps['name'].' (off-track)');
|
||||
continue 2; //discard tracks
|
||||
}
|
||||
else {
|
||||
$sType = 'off-track';
|
||||
break;
|
||||
}
|
||||
case 'Red':
|
||||
$sType = 'hitchhiking';
|
||||
break;
|
||||
default:
|
||||
$this->addNotice('Ignoring Track "'.$asTrackProps['name'].' (unknown color "'.$asTrackProps['color'].'")');
|
||||
continue 2; //discard tracks
|
||||
}
|
||||
|
||||
$asTrack = array(
|
||||
'type' => 'Feature',
|
||||
'properties' => array(
|
||||
'name' => $asTrackProps['name'],
|
||||
'type' => $sType,
|
||||
'description' => $asTrackProps['desc']
|
||||
),
|
||||
'geometry' => array(
|
||||
'type' => 'LineString',
|
||||
'coordinates' => array()
|
||||
)
|
||||
);
|
||||
|
||||
//Track points
|
||||
$asTrackPoints = $asTrackProps['points'];
|
||||
$iPointCount = count($asTrackPoints);
|
||||
$iInvalidPointCount = 0;
|
||||
$asPrevPoint = array();
|
||||
foreach($asTrackPoints as $iIndex=>$asPoint) {
|
||||
$asNextPoint = ($iIndex < ($iPointCount - 1))?$asTrackPoints[$iIndex + 1]:array();
|
||||
if($bSimplify && !empty($asPrevPoint) && !empty($asNextPoint)) {
|
||||
if(!$this->isPointValid($asPrevPoint, $asPoint, $asNextPoint)) {
|
||||
$iInvalidPointCount++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$asTrack['geometry']['coordinates'][] = array_values($asPoint);
|
||||
$asPrevPoint = $asPoint;
|
||||
}
|
||||
$this->asTracks[] = $asTrack;
|
||||
|
||||
$iGlobalInvalidPointCount += $iInvalidPointCount;
|
||||
$iGlobalPointCount += $iPointCount;
|
||||
if($iInvalidPointCount > 0) $this->addNotice('Removing '.$iInvalidPointCount.'/'.$iPointCount.' points ('.round($iInvalidPointCount / $iPointCount * 100, 1).'%) from '.$asTrackProps['name']);
|
||||
}
|
||||
|
||||
if($bSimplify) $this->addNotice('Total: '.$iGlobalInvalidPointCount.'/'.$iGlobalPointCount.' points removed ('.round($iGlobalInvalidPointCount / $iGlobalPointCount * 100, 1).'%)');
|
||||
}
|
||||
|
||||
|
||||
public function sortOffTracks() {
|
||||
$this->addNotice('Sorting off-tracks');
|
||||
|
||||
//Find first & last track points
|
||||
$asTracksEnds = array();
|
||||
$asTracks = array();
|
||||
foreach($this->asTracks as $iTrackId=>$asTrack) {
|
||||
$sTrackId = 't'.$iTrackId;
|
||||
$asTracksEnds[$sTrackId] = array('first'=>reset($asTrack['geometry']['coordinates']), 'last'=>end($asTrack['geometry']['coordinates']));
|
||||
$asTracks[$sTrackId] = $asTrack;
|
||||
}
|
||||
|
||||
//Find variants close-by tracks
|
||||
$asClonedTracks = $asTracks;
|
||||
foreach($asClonedTracks as $sTrackId=>$asTrack) {
|
||||
if($asTrack['properties']['type'] != 'off-track') continue;
|
||||
|
||||
$iMinDistance = INF;
|
||||
$sConnectedTrackId = 0;
|
||||
$iPosition = 0;
|
||||
|
||||
//Test all track ending points to find the closest
|
||||
foreach($asTracksEnds as $sTrackEndId=>$asTrackEnds) {
|
||||
if($sTrackEndId != $sTrackId) {
|
||||
//Calculate distance between the last point of the track and every starting point of other tracks
|
||||
$iDistance = self::getDistance($asTracksEnds[$sTrackId]['last'], $asTrackEnds['first']);
|
||||
if($iDistance < $iMinDistance) {
|
||||
$sConnectedTrackId = $sTrackEndId;
|
||||
$iPosition = 0; //Track before the Connected Track
|
||||
$iMinDistance = $iDistance;
|
||||
}
|
||||
|
||||
//Calculate distance between the first point of the track and every ending point of other tracks
|
||||
$iDistance = self::getDistance($asTracksEnds[$sTrackId]['first'], $asTrackEnds['last']);
|
||||
if($iDistance < $iMinDistance) {
|
||||
$sConnectedTrackId = $sTrackEndId;
|
||||
$iPosition = +1; //Track after the Connected Track
|
||||
$iMinDistance = $iDistance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Move track
|
||||
unset($asTracks[$sTrackId]);
|
||||
$iOffset = array_search($sConnectedTrackId, array_keys($asTracks)) + $iPosition;
|
||||
$asTracks = array_slice($asTracks, 0, $iOffset) + array($sTrackId => $asTrack) + array_slice($asTracks, $iOffset);
|
||||
}
|
||||
|
||||
$this->asTracks = array_values($asTracks);
|
||||
}
|
||||
|
||||
private function parseOptions($sComment){
|
||||
$sComment = strip_tags(html_entity_decode($sComment));
|
||||
$asOptions = array(self::OPT_SIMPLE=>'');
|
||||
foreach(explode("\n", $sComment) as $sLine) {
|
||||
$asOptions[mb_strtolower(trim(mb_strstr($sLine, ':', true)))] = mb_strtolower(trim(mb_substr(mb_strstr($sLine, ':'), 1)));
|
||||
}
|
||||
return $asOptions;
|
||||
}
|
||||
|
||||
private function isPointValid($asPointA, $asPointO, $asPointB) {
|
||||
/* A----O Calculate angle AO^OB
|
||||
* \ If angle is within [90% Pi ; 110% Pi], O can be discarded
|
||||
* \ O is valid otherwise
|
||||
* B
|
||||
*/
|
||||
|
||||
//Path Turn Check -> -> -> ->
|
||||
//Law of Cosines (vector): angle = arccos(OA.OB / ||OA||.||OB||)
|
||||
$fVectorOA = array('lon'=>($asPointA['lon'] - $asPointO['lon']), 'lat'=> ($asPointA['lat'] - $asPointO['lat']));
|
||||
$fVectorOB = array('lon'=>($asPointB['lon'] - $asPointO['lon']), 'lat'=> ($asPointB['lat'] - $asPointO['lat']));
|
||||
|
||||
$fLengthOA = sqrt(pow($asPointA['lon'] - $asPointO['lon'], 2) + pow($asPointA['lat'] - $asPointO['lat'], 2));
|
||||
$fLengthOB = sqrt(pow($asPointO['lon'] - $asPointB['lon'], 2) + pow($asPointO['lat'] - $asPointB['lat'], 2));
|
||||
|
||||
$fVectorOAxOB = $fVectorOA['lon'] * $fVectorOB['lon'] + $fVectorOA['lat'] * $fVectorOB['lat'];
|
||||
$fAngleAOB = ($fLengthOA != 0 && $fLengthOB != 0) ? acos($fVectorOAxOB/($fLengthOA * $fLengthOB)) : 0;
|
||||
|
||||
//Elevation Check
|
||||
//Law of Cosines: angle = arccos((OB² + AO² - AB²) / (2*OB*AO))
|
||||
$fLengthAB = sqrt(pow($asPointB['ele'] - $asPointA['ele'], 2) + pow($fLengthOA + $fLengthOB, 2));
|
||||
$fLengthAO = sqrt(pow($asPointO['ele'] - $asPointA['ele'], 2) + pow($fLengthOA, 2));
|
||||
$fLengthOB = sqrt(pow($asPointB['ele'] - $asPointO['ele'], 2) + pow($fLengthOB, 2));
|
||||
$fAngleAOBElev = ($fLengthOB != 0 && $fLengthAO != 0) ? (acos((pow($fLengthOB, 2) + pow($fLengthAO, 2) - pow($fLengthAB, 2)) / (2 * $fLengthOB * $fLengthAO))) : 0;
|
||||
|
||||
return ($fAngleAOB <= (1 - self::MAX_DEVIATION_FLAT) * M_PI || $fAngleAOB >= (1 + self::MAX_DEVIATION_FLAT) * M_PI ||
|
||||
$fAngleAOBElev <= (1 - self::MAX_DEVIATION_ELEV) * M_PI || $fAngleAOBElev >= (1 + self::MAX_DEVIATION_ELEV) * M_PI);
|
||||
}
|
||||
|
||||
private function buildGeoJson() {
|
||||
return json_encode(array('type'=>'FeatureCollection', 'features'=>$this->asTracks));
|
||||
}
|
||||
|
||||
private static function getDistance($asPointA, $asPointB) {
|
||||
$fLatFrom = $asPointA[1];
|
||||
$fLonFrom = $asPointA[0];
|
||||
$fLatTo = $asPointB[1];
|
||||
$fLonTo = $asPointB[0];
|
||||
|
||||
$fRad = M_PI / 180;
|
||||
|
||||
//Calculate distance from latitude and longitude
|
||||
$fTheta = $fLonFrom - $fLonTo;
|
||||
$fDistance = sin($fLatFrom * $fRad) * sin($fLatTo * $fRad) + cos($fLatFrom * $fRad) * cos($fLatTo * $fRad) * cos($fTheta * $fRad);
|
||||
|
||||
return acos($fDistance) / $fRad * 60 * 1.853;
|
||||
}
|
||||
}
|
||||
52
lib/Gpx.php
Normal file
52
lib/Gpx.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Franzz\Spot;
|
||||
use Franzz\Objects\ToolBox;
|
||||
|
||||
class Gpx extends Geo {
|
||||
|
||||
const EXT = '.gpx';
|
||||
|
||||
public function __construct($sCodeName) {
|
||||
parent::__construct($sCodeName);
|
||||
$this->parseFile();
|
||||
}
|
||||
|
||||
public function getTracks() {
|
||||
return $this->asTracks;
|
||||
}
|
||||
|
||||
private function parseFile() {
|
||||
$this->addNotice('Parsing: '.$this->sFilePath);
|
||||
if(!file_exists($this->sFilePath)) $this->addError($this->sFilePath.' file missing');
|
||||
else {
|
||||
$oXml = simplexml_load_file($this->sFilePath);
|
||||
|
||||
//Tracks
|
||||
$this->addNotice('Converting '.count($oXml->trk).' tracks');
|
||||
foreach($oXml->trk as $aoTrack) {
|
||||
$asTrack = array(
|
||||
'name' => (string) $aoTrack->name,
|
||||
'desc' => str_replace("\n", '', ToolBox::fixEOL((strip_tags($aoTrack->desc)))),
|
||||
'cmt' => ToolBox::fixEOL((strip_tags($aoTrack->cmt))),
|
||||
'color' => (string) $aoTrack->extensions->children('gpxx', true)->TrackExtension->DisplayColor,
|
||||
'points'=> array()
|
||||
);
|
||||
|
||||
foreach($aoTrack->trkseg as $asSegment) {
|
||||
foreach($asSegment as $asPoint) {
|
||||
$asTrack['points'][] = array(
|
||||
'lon' => (float) $asPoint['lon'],
|
||||
'lat' => (float) $asPoint['lat'],
|
||||
'ele' => (int) $asPoint->ele
|
||||
);
|
||||
}
|
||||
}
|
||||
$this->asTracks[] = $asTrack;
|
||||
}
|
||||
|
||||
//Waypoints
|
||||
$this->addNotice('Ignoring '.count($oXml->wpt).' waypoints');
|
||||
}
|
||||
}
|
||||
}
|
||||
33
lib/Map.php
33
lib/Map.php
@@ -11,13 +11,12 @@ class Map extends PhpObject {
|
||||
const MAPPING_TABLE = 'mappings';
|
||||
|
||||
private Db $oDb;
|
||||
|
||||
private $asMaps;
|
||||
|
||||
public function __construct(Db &$oDb) {
|
||||
parent::__construct(__CLASS__);
|
||||
$this->oDb = &$oDb;
|
||||
$this->setMaps();
|
||||
$this->asMaps = array();
|
||||
}
|
||||
|
||||
private function setMaps() {
|
||||
@@ -25,14 +24,36 @@ class Map extends PhpObject {
|
||||
foreach($asMaps as $asMap) $this->asMaps[$asMap['codename']] = $asMap;
|
||||
}
|
||||
|
||||
private function getMaps($sCodeName='') {
|
||||
if(empty($this->asMaps)) $this->setMaps();
|
||||
return ($sCodeName=='')?$this->asMaps:$this->asMaps[$sCodeName];
|
||||
}
|
||||
|
||||
public function getProjectMaps($iProjectId) {
|
||||
$asMappings = $this->oDb->getArrayQuery("SELECT id_map FROM mappings WHERE id_project = ".$iProjectId." OR id_project IS NULL", true);
|
||||
return array_filter($this->asMaps, function($asMap) use($asMappings) {return in_array($asMap['id_map'], $asMappings);});
|
||||
$asMappings = $this->oDb->selectRows(
|
||||
array(
|
||||
'select' => array(Db::getId(self::MAP_TABLE), 'default_map'),
|
||||
'from' => self::MAPPING_TABLE,
|
||||
'constraint'=> array("IFNULL(id_project, {$iProjectId})" => $iProjectId)
|
||||
),
|
||||
Db::getId(self::MAP_TABLE)
|
||||
);
|
||||
|
||||
$asProjectMaps = array();
|
||||
foreach($this->getMaps() as $asMap) {
|
||||
if(array_key_exists($asMap['id_map'], $asMappings)) {
|
||||
$asMap['default_map'] = $asMappings[$asMap['id_map']];
|
||||
$asProjectMaps[] = $asMap;
|
||||
}
|
||||
}
|
||||
|
||||
return $asProjectMaps;
|
||||
}
|
||||
|
||||
public function getMapUrl($sCodeName, $asParams) {
|
||||
$asParams['token'] = $this->asMaps[$sCodeName]['token'];
|
||||
return self::populateParams($this->asMaps[$sCodeName]['pattern'], $asParams);
|
||||
$asMap = $this->getMaps($sCodeName);
|
||||
$asParams['token'] = $asMap['token'];
|
||||
return self::populateParams($asMap['pattern'], $asParams);
|
||||
}
|
||||
|
||||
private static function populateParams($sUrl, $asParams) {
|
||||
|
||||
@@ -17,30 +17,21 @@ class Media extends PhpObject {
|
||||
|
||||
const THUMB_MAX_WIDTH = 400;
|
||||
|
||||
/**
|
||||
* Database Handle
|
||||
* @var Db
|
||||
*/
|
||||
private $oDb;
|
||||
|
||||
/**
|
||||
* Media Project
|
||||
* @var Project
|
||||
*/
|
||||
private $oProject;
|
||||
private Db $oDb;
|
||||
private Project $oProject;
|
||||
private $asMedia;
|
||||
private $asMedias;
|
||||
private $sSystemType;
|
||||
//private $sSystemType;
|
||||
|
||||
private $iMediaId;
|
||||
|
||||
public function __construct(Db &$oDb, &$oProject, $iMediaId=0) {
|
||||
public function __construct(Db &$oDb, Project &$oProject, $iMediaId=0) {
|
||||
parent::__construct(__CLASS__);
|
||||
$this->oDb = &$oDb;
|
||||
$this->oProject = &$oProject;
|
||||
$this->asMedia = array();
|
||||
$this->asMedias = array();
|
||||
$this->sSystemType = (substr(php_uname(), 0, 7) == "Windows")?'win':'unix';
|
||||
//$this->sSystemType = (substr(php_uname(), 0, 7) == "Windows")?'win':'unix';
|
||||
$this->setMediaId($iMediaId);
|
||||
}
|
||||
|
||||
|
||||
@@ -142,15 +142,19 @@ class Project extends PhpObject {
|
||||
}
|
||||
$asProject['editable'] = $this->isModeEditable($asProject['mode']);
|
||||
|
||||
if($sCodeName != '' && !Converter::isGeoJsonValid($sCodeName)) Converter::convertToGeoJson($sCodeName);
|
||||
|
||||
$asProject['geofilepath'] = Spot::addTimestampToFilePath(GeoJson::getDistFilePath($sCodeName));
|
||||
//$asProject['geofilepath'] = Spot::addTimestampToFilePath(GeoJson::getDistFilePath($sCodeName));
|
||||
$asProject['gpxfilepath'] = Spot::addTimestampToFilePath(Gpx::getDistFilePath($sCodeName));
|
||||
$asProject['codename'] = $sCodeName;
|
||||
}
|
||||
return $bSpecificProj?$asProject:$asProjects;
|
||||
}
|
||||
|
||||
public function getGeoJson() {
|
||||
if($this->sCodeName != '' && !Converter::isGeoJsonValid($this->sCodeName)) Converter::convertToGeoJson($this->sCodeName);
|
||||
|
||||
return json_decode(file_get_contents(GeoJson::getDistFilePath($this->sCodeName)), true);
|
||||
}
|
||||
|
||||
public function getProject() {
|
||||
return $this->getProjects($this->getProjectId());
|
||||
}
|
||||
@@ -185,7 +189,7 @@ class Project extends PhpObject {
|
||||
$this->sCodeName = $asProject['codename'];
|
||||
$this->sMode = $asProject['mode'];
|
||||
$this->asActive = array('from'=>$asProject['active_from'], 'to'=>$asProject['active_to']);
|
||||
$this->asGeo = array('geofile'=>$asProject['geofilepath'], 'gpxfile'=>$asProject['gpxfilepath']);
|
||||
$this->asGeo = array(/*'geofile'=>$asProject['geofilepath'], */'gpxfile'=>$asProject['gpxfilepath']);
|
||||
}
|
||||
else $this->addError('Error while setting project: no project ID');
|
||||
}
|
||||
|
||||
144
lib/Spot.php
144
lib/Spot.php
@@ -150,7 +150,8 @@ class Spot extends Main
|
||||
Project::PROJ_TABLE => "UNIQUE KEY `uni_proj_name` (`codename`)",
|
||||
Media::MEDIA_TABLE => "UNIQUE KEY `uni_file_name` (`filename`)",
|
||||
User::USER_TABLE => "UNIQUE KEY `uni_email` (`email`)",
|
||||
Map::MAP_TABLE => "UNIQUE KEY `uni_map_name` (`codename`)"
|
||||
Map::MAP_TABLE => "UNIQUE KEY `uni_map_name` (`codename`)",
|
||||
Map::MAPPING_TABLE => "default_on_generic_map_only CHECK (`default_map` = 0 OR `id_project` IS NULL)"
|
||||
),
|
||||
'cascading_delete' => array
|
||||
(
|
||||
@@ -173,7 +174,6 @@ class Spot extends Main
|
||||
return parent::getMainPage(
|
||||
array(
|
||||
'vars' => array(
|
||||
'chunk_size' => self::FEED_CHUNK_SIZE,
|
||||
'default_project_codename' => $this->oProject->getProjectCodeName(),
|
||||
'projects' => $this->oProject->getProjects(),
|
||||
'user' => $this->oUser->getUserInfo()
|
||||
@@ -181,7 +181,8 @@ class Spot extends Main
|
||||
'consts' => array(
|
||||
'modes' => Project::MODES,
|
||||
'clearances' => User::CLEARANCES,
|
||||
'default_timezone' => Settings::TIMEZONE
|
||||
'default_timezone' => Settings::TIMEZONE,
|
||||
'chunk_size' => self::FEED_CHUNK_SIZE
|
||||
)
|
||||
),
|
||||
self::MAIN_PAGE,
|
||||
@@ -204,6 +205,10 @@ class Spot extends Main
|
||||
$this->oProject->setProjectId($iProjectId);
|
||||
}
|
||||
|
||||
public function getProjectGeoJson() {
|
||||
return self::getJsonResult(true, '', $this->oProject->getGeoJson());
|
||||
}
|
||||
|
||||
public function updateProject() {
|
||||
$bNewMsg = false;
|
||||
$bSuccess = true;
|
||||
@@ -218,34 +223,7 @@ class Spot extends Main
|
||||
|
||||
//Send Update Email
|
||||
if($bNewMsg) {
|
||||
$oEmail = new Email($this->asContext['serv_name'], 'email_update');
|
||||
$oEmail->setDestInfo($this->oUser->getActiveUsersInfo());
|
||||
|
||||
//Add Position
|
||||
$asLastMessage = array_shift($this->getSpotMessages(array($this->oProject->getLastMessageId($this->getFeedConstraints(Feed::MSG_TABLE)))));
|
||||
$oEmail->oTemplate->setTags($asLastMessage);
|
||||
$oEmail->oTemplate->setTag('date_time', 'time:'.$asLastMessage['unix_time'], 'd/m/Y, H:i');
|
||||
|
||||
//Add latest news feed
|
||||
$asNews = $this->getNextFeed(0, true);
|
||||
$iPostCount = 0;
|
||||
foreach($asNews as $asPost) {
|
||||
if($asPost['type'] != 'message') {
|
||||
$oEmail->oTemplate->newInstance('news');
|
||||
$oEmail->oTemplate->setInstanceTags('news', array(
|
||||
'local_server' => $this->asContext['serv_name'],
|
||||
'project' => $this->oProject->getProjectCodeName(),
|
||||
'type' => $asPost['type'],
|
||||
'id' => $asPost['id_'.$asPost['type']])
|
||||
);
|
||||
$oEmail->oTemplate->addInstance($asPost['type'], $asPost);
|
||||
$oEmail->oTemplate->setInstanceTag($asPost['type'], 'local_server', $this->asContext['serv_name']);
|
||||
$iPostCount++;
|
||||
}
|
||||
if($iPostCount == self::MAIL_CHUNK_SIZE) break;
|
||||
}
|
||||
|
||||
$bSuccess = $oEmail->send();
|
||||
$bSuccess = $this->sendEmail();
|
||||
$sDesc = $bSuccess?'mail_sent':'mail_failure';
|
||||
}
|
||||
else $sDesc = 'no_new_msg';
|
||||
@@ -253,6 +231,37 @@ class Spot extends Main
|
||||
return self::getJsonResult($bSuccess, $sDesc);
|
||||
}
|
||||
|
||||
private function sendEmail() {
|
||||
$oEmail = new Email($this->asContext['serv_name'], 'email_update');
|
||||
$oEmail->setDestInfo($this->oUser->getActiveUsersInfo());
|
||||
|
||||
//Add Position
|
||||
$asLastMessage = array_shift($this->getSpotMessages(array($this->oProject->getLastMessageId($this->getFeedConstraints(Feed::MSG_TABLE)))));
|
||||
$oEmail->oTemplate->setTags($asLastMessage);
|
||||
$oEmail->oTemplate->setTag('date_time', 'time:'.$asLastMessage['unix_time'], 'd/m/Y, H:i');
|
||||
|
||||
//Add latest news feed
|
||||
$asNews = $this->getNextFeed(0, true);
|
||||
$iPostCount = 0;
|
||||
foreach($asNews as $asPost) {
|
||||
if($asPost['type'] != 'message') {
|
||||
$oEmail->oTemplate->newInstance('news');
|
||||
$oEmail->oTemplate->setInstanceTags('news', array(
|
||||
'local_server' => $this->asContext['serv_name'],
|
||||
'project' => $this->oProject->getProjectCodeName(),
|
||||
'type' => $asPost['type'],
|
||||
'id' => $asPost['id_'.$asPost['type']])
|
||||
);
|
||||
$oEmail->oTemplate->addInstance($asPost['type'], $asPost);
|
||||
$oEmail->oTemplate->setInstanceTag($asPost['type'], 'local_server', $this->asContext['serv_name']);
|
||||
$iPostCount++;
|
||||
}
|
||||
if($iPostCount == self::MAIL_CHUNK_SIZE) break;
|
||||
}
|
||||
|
||||
return $oEmail->send();
|
||||
}
|
||||
|
||||
public function genCronFile() {
|
||||
//$bSuccess = (file_put_contents('spot_cron.sh', '#!/bin/bash'."\n".'cd '.dirname($_SERVER['SCRIPT_FILENAME'])."\n".'php -f index.php a=update_feed')!==false);
|
||||
$sFileName = 'spot_cron.sh';
|
||||
@@ -627,6 +636,19 @@ class Spot extends Main
|
||||
return self::getJsonResult($asResult['result'], $asResult['desc'], $asResult['data']);
|
||||
}
|
||||
|
||||
public function addPosition($sLat, $sLng, $iTimestamp) {
|
||||
$oFeed = new Feed($this->oDb, $this->oProject->getFeedIds()[0]);
|
||||
$bSuccess = ($oFeed->addManualPosition($sLat, $sLng, $iTimestamp) > 0);
|
||||
|
||||
if($bSuccess) {
|
||||
$bSuccess = $this->sendEmail();
|
||||
$sDesc = $bSuccess?'mail_sent':'mail_failure';
|
||||
}
|
||||
else $sDesc = 'error_commit_db';
|
||||
|
||||
return self::getJsonResult($bSuccess, $sDesc);
|
||||
}
|
||||
|
||||
public function getAdminSettings($sType='') {
|
||||
$oFeed = new Feed($this->oDb);
|
||||
$asData = array(
|
||||
@@ -709,38 +731,68 @@ class Spot extends Main
|
||||
return self::getJsonResult($bSuccess, $sDesc, array($sType=>array($asResult)));
|
||||
}
|
||||
|
||||
public function delAdminSettings($sType, $iId) {
|
||||
public function createAdminSettings($sType) {
|
||||
$bSuccess = false;
|
||||
$sDesc = '';
|
||||
$asResult = array();
|
||||
|
||||
switch($sType) {
|
||||
case 'project':
|
||||
$oProject = new Project($this->oDb);
|
||||
$iNewProjectId = $oProject->createProjectId();
|
||||
|
||||
$oFeed = new Feed($this->oDb);
|
||||
$oFeed->createFeedId($iNewProjectId);
|
||||
|
||||
$bSuccess = $iNewProjectId > 0;
|
||||
$asResult = array(
|
||||
'project' => array($oProject->getProject()),
|
||||
'feed' => array($oFeed->getFeed())
|
||||
);
|
||||
break;
|
||||
case 'feed':
|
||||
$oFeed = new Feed($this->oDb);
|
||||
$iNewFeedId = $oFeed->createFeedId($this->oProject->getProjectId());
|
||||
$bSuccess = $iNewFeedId > 0;
|
||||
$asResult = array(
|
||||
'feed' => array($oFeed->getFeed())
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return self::getJsonResult($bSuccess, $sDesc, $asResult);
|
||||
}
|
||||
|
||||
public function deleteAdminSettings($sType, $iId) {
|
||||
$bSuccess = false;
|
||||
$sDesc = '';
|
||||
$asResult = array();
|
||||
|
||||
switch($sType) {
|
||||
case 'project':
|
||||
$oProject = new Project($this->oDb, $iId);
|
||||
$asResult = $oProject->delete();
|
||||
$sDesc = $asResult['project'][0]['desc'];
|
||||
$bSuccess = $asResult['project'][0]['del'];
|
||||
break;
|
||||
case 'feed':
|
||||
$oFeed = new Feed($this->oDb, $iId);
|
||||
$asResult = array('feed'=>array($oFeed->delete()));
|
||||
$asResult = array('feed' => array($oFeed->delete()));
|
||||
$sDesc = $asResult['feed'][0]['desc'];
|
||||
$bSuccess = $asResult['feed'][0]['del'];
|
||||
break;
|
||||
case 'user':
|
||||
$asResult = array('user' => array($this->oUser->removeUser($iId)));
|
||||
$sDesc = $asResult['user'][0]['desc'];
|
||||
$bSuccess = $asResult['user'][0]['result'];
|
||||
break;
|
||||
}
|
||||
$bSuccess = ($sDesc=='');
|
||||
|
||||
|
||||
return self::getJsonResult($bSuccess, $sDesc, $asResult);
|
||||
}
|
||||
|
||||
public function createProject() {
|
||||
$oProject = new Project($this->oDb);
|
||||
$iNewProjectId = $oProject->createProjectId();
|
||||
|
||||
$oFeed = new Feed($this->oDb);
|
||||
$oFeed->createFeedId($iNewProjectId);
|
||||
|
||||
return self::getJsonResult($iNewProjectId>0, '', array(
|
||||
'project' => array($oProject->getProject()),
|
||||
'feed' => array($oFeed->getFeed())
|
||||
));
|
||||
public function buildGeoJSON($sCodeName) {
|
||||
return Converter::convertToGeoJson($sCodeName);
|
||||
}
|
||||
|
||||
public static function decToDms($dValue, $sType) {
|
||||
|
||||
114
lib/User.php
114
lib/User.php
@@ -20,6 +20,7 @@ class User extends PhpObject {
|
||||
//Cookie
|
||||
const COOKIE_ID_USER = 'subscriber';
|
||||
const COOKIE_DURATION = 60 * 60 * 24 * 365; //1 year
|
||||
|
||||
/**
|
||||
* Database Handle
|
||||
* @var Db
|
||||
@@ -33,7 +34,7 @@ class User extends PhpObject {
|
||||
public function __construct(Db &$oDb) {
|
||||
parent::__construct(__CLASS__);
|
||||
$this->oDb = &$oDb;
|
||||
$this->iUserId = 0;
|
||||
$this->setUserId(0);
|
||||
$this->asUserInfo = array(
|
||||
'id' => 0,
|
||||
Db::getId(self::USER_TABLE) => 0,
|
||||
@@ -47,6 +48,51 @@ class User extends PhpObject {
|
||||
$this->checkUserCookie();
|
||||
}
|
||||
|
||||
public function getUserId() {
|
||||
return $this->iUserId;
|
||||
}
|
||||
|
||||
public function setUserId($iUserId) {
|
||||
$this->iUserId = 0;
|
||||
|
||||
if($iUserId > 0) {
|
||||
$asUser = $this->getActiveUserInfo($iUserId);
|
||||
if(!empty($asUser)) {
|
||||
$this->iUserId = $iUserId;
|
||||
$this->asUserInfo = $asUser;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getUserInfo() {
|
||||
return $this->asUserInfo;
|
||||
}
|
||||
|
||||
public function getActiveUserInfo($iUserId) {
|
||||
$asUsersInfo = array();
|
||||
if($iUserId > 0) $asUsersInfo = $this->getActiveUsersInfo($iUserId);
|
||||
return empty($asUsersInfo)?array():array_shift($asUsersInfo);
|
||||
}
|
||||
|
||||
public function getActiveUsersInfo($iUserId=-1) {
|
||||
|
||||
//Mapping between user fields and DB fields
|
||||
$asSelect = array_keys($this->asUserInfo);
|
||||
$asSelect[array_search('id', $asSelect)] = Db::getId(self::USER_TABLE)." AS id";
|
||||
|
||||
//Non-admin cannot access clearance info
|
||||
if(!$this->checkUserClearance(self::CLEARANCE_ADMIN)) unset($asSelect['clearance']);
|
||||
|
||||
$asInfo = array(
|
||||
'select' => $asSelect,
|
||||
'from' => self::USER_TABLE,
|
||||
'constraint'=> array('active'=>self::USER_ACTIVE)
|
||||
);
|
||||
if($iUserId != -1) $asInfo['constraint'][Db::getId(self::USER_TABLE)] = $iUserId;
|
||||
|
||||
return $this->oDb->selectRows($asInfo);
|
||||
}
|
||||
|
||||
public function getLang() {
|
||||
return $this->asUserInfo['language'];
|
||||
}
|
||||
@@ -95,20 +141,25 @@ class User extends PhpObject {
|
||||
return Spot::getResult($bSuccess, $sDesc);
|
||||
}
|
||||
|
||||
public function removeUser() {
|
||||
public function removeUser($iUserId=0) {
|
||||
$iUserId = ($iUserId > 0)?$iUserId:$this->getUserId();
|
||||
$bSelf = ($iUserId == $this->getUserId());
|
||||
$bSuccess = false;
|
||||
$sDesc = '';
|
||||
|
||||
if($this->iUserId > 0) {
|
||||
$iUserId = $this->oDb->updateRow(self::USER_TABLE, $this->getUserId(), array('active'=>self::USER_INACTIVE));
|
||||
if($iUserId==0) $sDesc = 'lang:error_commit_db';
|
||||
else {
|
||||
$sDesc = 'lang:nl_unsubscribed';
|
||||
$this->updateCookie(-60 * 60); //Set Cookie in the past, deleting it
|
||||
$bSuccess = true;
|
||||
if($bSelf || $this->checkUserClearance(self::CLEARANCE_ADMIN)) {
|
||||
if($this->getUserId() > 0) {
|
||||
$iUserId = $this->oDb->updateRow(self::USER_TABLE, $iUserId, array('active' => self::USER_INACTIVE));
|
||||
if($iUserId==0) $sDesc = 'lang:error_commit_db';
|
||||
else {
|
||||
$sDesc = 'lang:nl_unsubscribed';
|
||||
if($bSelf) $this->updateCookie(-60 * 60); //Set Cookie in the past, deleting it
|
||||
$bSuccess = true;
|
||||
}
|
||||
}
|
||||
else $sDesc = 'lang:nl_unknown_email';
|
||||
}
|
||||
else $sDesc = 'lang:nl_unknown_email';
|
||||
else $sDesc = 'lang:no_auth';
|
||||
|
||||
return Spot::getResult($bSuccess, $sDesc);
|
||||
}
|
||||
@@ -131,49 +182,6 @@ class User extends PhpObject {
|
||||
}
|
||||
}
|
||||
|
||||
public function getUserId() {
|
||||
return $this->iUserId;
|
||||
}
|
||||
|
||||
public function setUserId($iUserId) {
|
||||
$this->iUserId = 0;
|
||||
|
||||
$asUser = $this->getActiveUserInfo($iUserId);
|
||||
if(!empty($asUser)) {
|
||||
$this->iUserId = $iUserId;
|
||||
$this->asUserInfo = $asUser;
|
||||
}
|
||||
}
|
||||
|
||||
public function getUserInfo() {
|
||||
return $this->asUserInfo;
|
||||
}
|
||||
|
||||
public function getActiveUserInfo($iUserId) {
|
||||
$asUsersInfo = array();
|
||||
if($iUserId > 0) $asUsersInfo = $this->getActiveUsersInfo($iUserId);
|
||||
return empty($asUsersInfo)?array():array_shift($asUsersInfo);
|
||||
}
|
||||
|
||||
public function getActiveUsersInfo($iUserId=-1) {
|
||||
|
||||
//Mapping between user fields and DB fields
|
||||
$asSelect = array_keys($this->asUserInfo);
|
||||
$asSelect[array_search('id', $asSelect)] = Db::getId(self::USER_TABLE)." AS id";
|
||||
|
||||
//Non-admin cannot access clearance info
|
||||
if(!$this->checkUserClearance(self::CLEARANCE_ADMIN)) unset($asSelect['clearance']);
|
||||
|
||||
$asInfo = array(
|
||||
'select' => $asSelect,
|
||||
'from' => self::USER_TABLE,
|
||||
'constraint'=> array('active'=>self::USER_ACTIVE)
|
||||
);
|
||||
if($iUserId != -1) $asInfo['constraint'][Db::getId(self::USER_TABLE)] = $iUserId;
|
||||
|
||||
return $this->oDb->selectRows($asInfo);
|
||||
}
|
||||
|
||||
public function checkUserClearance($iClearance)
|
||||
{
|
||||
return ($this->asUserInfo['clearance'] >= $iClearance);
|
||||
|
||||
@@ -26,6 +26,9 @@ $oValue = $_REQUEST['value'] ?? '';
|
||||
$iId = $_REQUEST['id'] ?? 0 ;
|
||||
$sType = $_REQUEST['type'] ?? '';
|
||||
$sEmail = $_REQUEST['email'] ?? '';
|
||||
$sLat = $_REQUEST['latitude'] ?? '';
|
||||
$sLng = $_REQUEST['longitude'] ?? '';
|
||||
$iTimestamp = $_REQUEST['timestamp'] ?? 0;
|
||||
|
||||
//Initiate class
|
||||
$oSpot = new Spot(__FILE__, $sTimezone);
|
||||
@@ -39,6 +42,9 @@ if($sAction!='')
|
||||
case 'markers':
|
||||
$sResult = $oSpot->getMarkers();
|
||||
break;
|
||||
case 'geojson':
|
||||
$sResult = $oSpot->getProjectGeoJson();
|
||||
break;
|
||||
case 'next_feed':
|
||||
$sResult = $oSpot->getNextFeed($iId);
|
||||
break;
|
||||
@@ -71,8 +77,8 @@ if($sAction!='')
|
||||
case 'add_comment':
|
||||
$sResult = $oSpot->addComment($iId, $sContent);
|
||||
break;
|
||||
case 'admin_new':
|
||||
$sResult = $oSpot->createProject();
|
||||
case 'add_position':
|
||||
$sResult = $oSpot->addPosition($sLat, $sLng, $iTimestamp);
|
||||
break;
|
||||
case 'admin_get':
|
||||
$sResult = $oSpot->getAdminSettings();
|
||||
@@ -80,8 +86,11 @@ if($sAction!='')
|
||||
case 'admin_set':
|
||||
$sResult = $oSpot->setAdminSettings($sType, $iId, $sField, $oValue);
|
||||
break;
|
||||
case 'admin_del':
|
||||
$sResult = $oSpot->delAdminSettings($sType, $iId);
|
||||
case 'admin_create':
|
||||
$sResult = $oSpot->createAdminSettings($sType);
|
||||
break;
|
||||
case 'admin_delete':
|
||||
$sResult = $oSpot->deleteAdminSettings($sType, $iId);
|
||||
break;
|
||||
case 'generate_cron':
|
||||
$sResult = $oSpot->genCronFile();
|
||||
@@ -89,6 +98,9 @@ if($sAction!='')
|
||||
case 'sql':
|
||||
$sResult = $oSpot->getDbBuildScript();
|
||||
break;
|
||||
case 'build_geojson':
|
||||
$sResult = $oSpot->buildGeoJSON($sName);
|
||||
break;
|
||||
default:
|
||||
$sResult = Main::getJsonResult(false, Main::NOT_FOUND);
|
||||
}
|
||||
|
||||
4392
package-lock.json
generated
4392
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@@ -1,12 +1,14 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.23.2",
|
||||
"@babel/preset-env": "^7.23.2",
|
||||
"babel-loader": "^9.1.3",
|
||||
"@babel/core": "^7.23.9",
|
||||
"@babel/preset-env": "^7.23.9",
|
||||
"babel-loader": "^10.0.0",
|
||||
"resolve-url-loader": "^5.0.0",
|
||||
"symlink-webpack-plugin": "^1.1.0",
|
||||
"webpack": "^5.89.0",
|
||||
"webpack-cli": "^5.1.4"
|
||||
"vue-loader": "^17.4.2",
|
||||
"vue-template-compiler": "^2.7.16",
|
||||
"webpack": "^5.99.7",
|
||||
"webpack-cli": "^6.0.1"
|
||||
},
|
||||
"name": "spot",
|
||||
"description": "FindMeSpot & GPX integration",
|
||||
@@ -23,23 +25,23 @@
|
||||
"autosize": "^6.0.1",
|
||||
"blueimp-file-upload": "^10.32.0",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"css-loader": "^6.8.1",
|
||||
"copy-webpack-plugin": "^13.0.0",
|
||||
"css-loader": "^7.1.2",
|
||||
"d3": "^7.8.5",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-loader": "^4.2.0",
|
||||
"html-loader": "^5.0.0",
|
||||
"jquery": "^3.7.1",
|
||||
"jquery-mousewheel": "^3.1.13",
|
||||
"jquery.waitforimages": "^2.4.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet-geometryutil": "^0.10.2",
|
||||
"leaflet.heightgraph": "^1.4.0",
|
||||
"lightbox2": "^2.11.4",
|
||||
"maplibre-gl": "^5.4.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"sass": "^1.69.4",
|
||||
"sass-loader": "^13.3.2",
|
||||
"simplebar": "^6.2.5",
|
||||
"style-loader": "^3.3.3",
|
||||
"url-loader": "^4.1.1"
|
||||
"sass": "^1.70.0",
|
||||
"sass-loader": "^16.0.5",
|
||||
"simplebar-vue": "^2.3.3",
|
||||
"style-loader": "^4.0.0",
|
||||
"url-loader": "^4.1.1",
|
||||
"vue": "^3.3.8",
|
||||
"vue-style-loader": "^4.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
26
readme.md
26
readme.md
@@ -1,6 +1,8 @@
|
||||
# Spot Project
|
||||
[Spot](https://www.findmespot.com) & GPX integration
|
||||
|
||||
## Dependencies
|
||||
|
||||
* npm 18+
|
||||
* composer
|
||||
* php-mbstring
|
||||
@@ -11,25 +13,31 @@
|
||||
* ffprobe & ffmpeg
|
||||
* STARTTLS Email Server (use Gmail if none available)
|
||||
* Optional: Geo Caching Server (WMTS Caching Service)
|
||||
|
||||
## PHP Configuration
|
||||
|
||||
* max_execution_time = 300
|
||||
* memory_limit = 500M
|
||||
* post_max_size = 4G
|
||||
* upload_max_filesize = 4G
|
||||
* max_file_uploads = 50
|
||||
|
||||
## Getting started
|
||||
|
||||
1. Clone Git onto web server
|
||||
2. composer install
|
||||
3. npm run dev
|
||||
4. Update php.ini parameters
|
||||
5. Copy timezone data: mariadb_tzinfo_to_sql /usr/share/zoneinfo | mariadb -u root mysql
|
||||
6. Copy settings-sample.php to settings.php and populate
|
||||
7. Go to #admin and create a new project, feed & maps
|
||||
8. Add a GPX file named <project_codename>.gpx to /geo/
|
||||
3. npm install webpack
|
||||
4. npm run dev
|
||||
5. Update php.ini parameters
|
||||
6. Copy timezone data: mariadb-tzinfo-to-sql /usr/share/zoneinfo | mariadb -u root mysql
|
||||
7. Copy settings-sample.php to settings.php and populate
|
||||
8. Go to #admin and create a new project, feed & maps
|
||||
9. Add a GPX file named <project_codename>.gpx to /geo/
|
||||
|
||||
## To Do List
|
||||
* ECMA import/export
|
||||
|
||||
* Add mail frequency slider
|
||||
* Use WMTS servers directly when not using Geo Caching Server
|
||||
* Allow HEIF picture format
|
||||
* Vector tiles support (https://www.arcgis.com/home/item.html?id=7dc6cea0b1764a1f9af2e679f642f0f5) + Use of GL library. Use Mapbox GL JS / Maplibre GL JS / ESRI-Leaflet-vector?
|
||||
* Fix .MOV playback on windows firefox
|
||||
* Fix .MOV playback on windows firefox
|
||||
* Garmin InReach Integration
|
||||
|
||||
84
src/Spot.vue
Normal file
84
src/Spot.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<script>
|
||||
import Project from './components/project.vue';
|
||||
import Admin from './components/admin.vue';
|
||||
import Upload from './components/upload.vue';
|
||||
|
||||
const aoRoutes = {
|
||||
'project': Project,
|
||||
'admin': Admin,
|
||||
'upload': Upload
|
||||
};
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
hash: {},
|
||||
consts: this.spot.consts,
|
||||
user: this.spot.vars('user')
|
||||
};
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
projects: this.spot.vars('projects'),
|
||||
consts: this.consts,
|
||||
user: this.user
|
||||
};
|
||||
},
|
||||
inject: ['spot'],
|
||||
computed: {
|
||||
page() {
|
||||
this.spot.vars('page', this.hash.page);
|
||||
return aoRoutes[this.hash.page];
|
||||
}
|
||||
},
|
||||
created() {
|
||||
//User
|
||||
this.user.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || this.consts.default_timezone;
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('hashchange', () => {this.onHashChange();});
|
||||
var oEvent = new Event('hashchange');
|
||||
window.dispatchEvent(oEvent);
|
||||
},
|
||||
methods: {
|
||||
_hash(hash, bReboot) {
|
||||
bReboot = bReboot || false;
|
||||
if(!hash) return window.location.hash.slice(1);
|
||||
else window.location.hash = '#'+hash;
|
||||
|
||||
if(bReboot) location.reload();
|
||||
},
|
||||
onHashChange() {
|
||||
let asHash = this.getHash();
|
||||
if(asHash.hash !='' && asHash.page != '') {
|
||||
if(asHash.page == this.hash.page) this.spot.onSamePageMove(asHash);
|
||||
this.hash = asHash;
|
||||
}
|
||||
else if(!this.hash.page) this.setHash(this.spot.consts.default_page);
|
||||
},
|
||||
getHash() {
|
||||
let sHash = this._hash();
|
||||
let asHash = sHash.split(this.spot.consts.hash_sep);
|
||||
let sPage = asHash.shift() || '';
|
||||
return {hash:sHash, page:sPage, items:asHash};
|
||||
},
|
||||
setHash(sPage, asItems, bReboot) {
|
||||
bReboot = bReboot || false;
|
||||
sPage = sPage || '';
|
||||
asItems = asItems || [];
|
||||
if(typeof asItems == 'string') asItems = [asItems];
|
||||
|
||||
if(sPage != '') {
|
||||
let sItems = (asItems.length > 0)?this.spot.consts.hash_sep+asItems.join(this.spot.consts.hash_sep):'';
|
||||
this._hash(sPage+sItems, bReboot);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div id="main">
|
||||
<component :is="page" />
|
||||
</div>
|
||||
<div id="mobile"></div>
|
||||
</template>
|
||||
235
src/components/admin.vue
Normal file
235
src/components/admin.vue
Normal file
@@ -0,0 +1,235 @@
|
||||
<script>
|
||||
import SpotIcon from './spotIcon.vue';
|
||||
import SpotButton from './spotButton.vue';
|
||||
import AdminInput from './adminInput.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SpotIcon,
|
||||
SpotButton,
|
||||
AdminInput
|
||||
},
|
||||
inject: ['spot'],
|
||||
data() {
|
||||
return {
|
||||
elems: {},
|
||||
feedbacks: []
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.setEvents();
|
||||
this.setProjects();
|
||||
},
|
||||
methods: {
|
||||
l(id) {
|
||||
return this.spot.lang(id);
|
||||
},
|
||||
setEvents() {
|
||||
this.spot.addPage('admin', {
|
||||
onFeedback: (sType, sMsg, asContext) => {
|
||||
delete asContext.a;
|
||||
delete asContext.t;
|
||||
sMsg += ' (';
|
||||
for(const [sKey, sElem] of Object.entries(asContext)) {
|
||||
sMsg += sKey+'='+sElem+' / ' ;
|
||||
}
|
||||
sMsg = sMsg.slice(0, -3)+')';
|
||||
|
||||
this.feedbacks.push({type:sType, msg:sMsg});
|
||||
}
|
||||
});
|
||||
},
|
||||
async setProjects() {
|
||||
let aoElemTypes = await this.spot.get2('admin_get');
|
||||
|
||||
for(const [sType, aoElems] of Object.entries(aoElemTypes)) {
|
||||
this.elems[sType] = {};
|
||||
for(const [iKey, oElem] of Object.entries(aoElems)) {
|
||||
oElem.type = sType;
|
||||
this.elems[sType][oElem.id] = oElem;
|
||||
}
|
||||
}
|
||||
},
|
||||
createElem(sType) {
|
||||
this.spot.get2('admin_create', {type: sType})
|
||||
.then((aoNewElemTypes) => {
|
||||
for(const [sType, aoNewElems] of Object.entries(aoNewElemTypes)) {
|
||||
for(const [iKey, oNewElem] of Object.entries(aoNewElems)) {
|
||||
oNewElem.type = sType;
|
||||
this.elems[sType][oNewElem.id] = oNewElem;
|
||||
this.spot.onFeedback('success', this.spot.lang('admin_create_success'), {'create':sType});
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((sMsg) => {this.spot.onFeedback('error', sMsg, {'create':sType});});
|
||||
},
|
||||
deleteElem(oElem) {
|
||||
const asInputs = {
|
||||
type: oElem.type,
|
||||
id: oElem.id
|
||||
};
|
||||
|
||||
this.spot.get(
|
||||
'admin_delete',
|
||||
(asData) => {
|
||||
delete this.elems[asInputs.type][asInputs.id];
|
||||
this.spot.onFeedback('success', this.spot.lang('admin_delete_success'), asInputs);
|
||||
},
|
||||
asInputs,
|
||||
(sError) => {
|
||||
this.spot.onFeedback('error', sError, asInputs);
|
||||
}
|
||||
);
|
||||
},
|
||||
updateElem(oElem, oEvent) {
|
||||
if(typeof this.spot.tmp('wait') != 'undefined') clearTimeout(this.spot.tmp('wait'));
|
||||
|
||||
let sOldVal = this.elems[oElem.type][oElem.id][oEvent.target.name];
|
||||
let sNewVal = oEvent.target.value;
|
||||
if(sOldVal != sNewVal) {
|
||||
let asInputs = {
|
||||
type: oElem.type,
|
||||
id: oElem.id,
|
||||
field: oEvent.target.name,
|
||||
value: sNewVal
|
||||
};
|
||||
|
||||
this.spot.get2('admin_set', asInputs)
|
||||
.then((asData) => {
|
||||
this.elems[oElem.type][oElem.id][oEvent.target.name] = sNewVal;
|
||||
this.spot.onFeedback('success', this.spot.lang('admin_save_success'), asInputs);
|
||||
})
|
||||
.catch((sError) => {
|
||||
oEvent.target.value = sOldVal;
|
||||
this.spot.onFeedback('error', sError, asInputs);
|
||||
});
|
||||
}
|
||||
},
|
||||
queue(oElem, oEvent) {
|
||||
if(typeof this.spot.tmp('wait') != 'undefined') clearTimeout(this.spot.tmp('wait'));
|
||||
this.spot.tmp('wait', setTimeout(() => {this.updateElem(oElem, oEvent);}, 2000));
|
||||
},
|
||||
updateProject() {
|
||||
this.spot.get2('update_project')
|
||||
.then((asData, sMsg) => {this.spot.onFeedback('success', sMsg, {'update':'project'});})
|
||||
.catch((sMsg) => {this.spot.onFeedback('error', sMsg, {'update':'project'});});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div id="admin">
|
||||
<a name="back" class="button" href="#project"><SpotIcon :icon="'back'" :text="l('nav_back')" /></a>
|
||||
<h1>{{ l('projects') }}</h1>
|
||||
<div id="project_section">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ l('id_project') }}</th>
|
||||
<th>{{ l('project') }}</th>
|
||||
<th>{{ l('mode') }}</th>
|
||||
<th>{{ l('code_name') }}</th>
|
||||
<th>{{ l('start') }}</th>
|
||||
<th>{{ l('end') }}</th>
|
||||
<th>{{ l('delete') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="project in elems.project">
|
||||
<td>{{ project.id }}</td>
|
||||
<td><AdminInput :type="'text'" :name="'name'" :elem="project" /></td>
|
||||
<td>{{ project.mode }}</td>
|
||||
<td><AdminInput :type="'text'" :name="'codename'" :elem="project" /></td>
|
||||
<td><AdminInput :type="'date'" :name="'active_from'" :elem="project" /></td>
|
||||
<td><AdminInput :type="'date'" :name="'active_to'" :elem="project" /></td>
|
||||
<td><SpotButton :icon="'close fa-lg'" @click="deleteElem(project)" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<SpotButton :classes="'new'" :text="l('new_project')" :icon="'new'" @click="createElem('project')" />
|
||||
</div>
|
||||
<h1>{{ l('feeds') }}</h1>
|
||||
<div id="feed_section">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ l('id_feed') }}</th>
|
||||
<th>{{ l('ref_feed_id') }}</th>
|
||||
<th>{{ l('id_spot') }}</th>
|
||||
<th>{{ l('id_project') }}</th>
|
||||
<th>{{ l('name') }}</th>
|
||||
<th>{{ l('status') }}</th>
|
||||
<th>{{ l('last_update') }}</th>
|
||||
<th>{{ l('delete') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="feed in elems.feed">
|
||||
<td>{{ feed.id }}</td>
|
||||
<td><AdminInput :type="'text'" :name="'ref_feed_id'" :elem="feed" /></td>
|
||||
<td><AdminInput :type="'number'" :name="'id_spot'" :elem="feed" /></td>
|
||||
<td><AdminInput :type="'number'" :name="'id_project'" :elem="feed" /></td>
|
||||
<td>{{ feed.name }}</td>
|
||||
<td>{{ feed.status }}</td>
|
||||
<td>{{ feed.last_update }}</td>
|
||||
<td><SpotButton :icon="'close fa-lg'" @click="deleteElem(feed)" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<SpotButton :classes="'new'" :text="l('new_feed')" :icon="'new'" @click="createElem('feed')" />
|
||||
</div>
|
||||
<h1>Spots</h1>
|
||||
<div id="spot_section">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ l('id_spot') }}</th>
|
||||
<th>{{ l('ref_spot_id') }}</th>
|
||||
<th>{{ l('name') }}</th>
|
||||
<th>{{ l('model') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="spot in elems.spot">
|
||||
<td>{{ spot.id }}</td>
|
||||
<td>{{ spot.ref_spot_id }}</td>
|
||||
<td>{{ spot.name }}</td>
|
||||
<td>{{ spot.model }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<h1>{{ l('active_users') }}</h1>
|
||||
<div id="user_section">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ l('id_user') }}</th>
|
||||
<th>{{ l('user_name') }}</th>
|
||||
<th>{{ l('language') }}</th>
|
||||
<th>{{ l('time_zone') }}</th>
|
||||
<th>{{ l('clearance') }}</th>
|
||||
<th>{{ l('delete') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="user in elems.user">
|
||||
<td>{{ user.id }}</td>
|
||||
<td>{{ user.name }}</td>
|
||||
<td>{{ user.language }}</td>
|
||||
<td>{{ user.timezone }}</td>
|
||||
<td><AdminInput :type="'number'" :name="'clearance'" :elem="user" /></td>
|
||||
<td><SpotButton :icon="'close fa-lg'" @click="deleteElem(user)" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<h1>{{ l('toolbox') }}</h1>
|
||||
<div id="toolbox">
|
||||
<SpotButton :classes="'refresh'" :text="l('update_project')" :icon="'refresh'" @click="updateProject" />
|
||||
</div>
|
||||
<div id="feedback" class="feedback">
|
||||
<p v-for="feedback in feedbacks" :class="feedback.type">{{ feedback.msg }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
18
src/components/adminInput.vue
Normal file
18
src/components/adminInput.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
type: String,
|
||||
name: String,
|
||||
elem: Object
|
||||
},
|
||||
computed: {
|
||||
value() {
|
||||
return this.elem[this.name];
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input :type="type" :name="name" :value="value" @change="$parent.updateElem(elem, $event)" @keyup="$parent.queue(elem, $event)" />
|
||||
</template>
|
||||
590
src/components/project.vue
Normal file
590
src/components/project.vue
Normal file
@@ -0,0 +1,590 @@
|
||||
<script>
|
||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
import { Map, NavigationControl, Marker, LngLatBounds, LngLat, Popup } from 'maplibre-gl';
|
||||
import { createApp, defineComponent, nextTick, ref, defineCustomElement, provide, inject } from 'vue';
|
||||
import simplebar from 'simplebar-vue';
|
||||
|
||||
import autosize from 'autosize';
|
||||
import mousewheel from 'jquery-mousewheel';
|
||||
import waitforimages from 'jquery.waitforimages';
|
||||
import lightbox from '../scripts/lightbox.js';
|
||||
|
||||
//import SimpleBar from 'simplebar';
|
||||
|
||||
import SpotIcon from './spotIcon.vue';
|
||||
import SpotButton from './spotButton.vue';
|
||||
import ProjectPost from './projectPost.vue';
|
||||
import ProjectPopup from './projectPopup.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SpotIcon,
|
||||
SpotButton,
|
||||
ProjectPost,
|
||||
ProjectPopup,
|
||||
simplebar
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
server: this.spot.consts.server,
|
||||
feed: {loading:false, updatable:true, outOfData:false, refIdFirst:0, refIdLast:0, firstChunk:true},
|
||||
feedPanelOpen: false,
|
||||
feedSimpleBar: null,
|
||||
settingsPanelOpen: false,
|
||||
markerSize: {width: 32, height: 32},
|
||||
project: {},
|
||||
projectCodename: null,
|
||||
modeHisto: false,
|
||||
posts: [],
|
||||
nlFeedbacks: [],
|
||||
nlLoading: false,
|
||||
baseMaps: {},
|
||||
baseMap: null,
|
||||
messages: null,
|
||||
map: null,
|
||||
hikes: {
|
||||
colors:{'main':'#00ff78', 'off-track':'#0000ff', 'hitchhiking':'#FF7814'},
|
||||
width: 4
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
projectClasses() {
|
||||
return [
|
||||
this.feedPanelOpen?'with-feed':'',
|
||||
this.settingsPanelOpen?'with-settings':''
|
||||
].filter(n => n).join(' ');
|
||||
},
|
||||
nlClasses() {
|
||||
return [
|
||||
this.nlAction,
|
||||
this.nlLoading?'loading':''
|
||||
].filter(n => n).join(' ');
|
||||
},
|
||||
subscribed() {
|
||||
return this.user.id_user > 0;
|
||||
},
|
||||
nlAction() {
|
||||
return this.subscribed?'unsubscribe':'subscribe';
|
||||
},
|
||||
mobile() {
|
||||
return this.spot.isMobile();
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
baseMap(sNewBaseMap, sOldBaseMap) {
|
||||
if(sOldBaseMap) this.map.setLayoutProperty(sOldBaseMap, 'visibility', 'none');
|
||||
if(sNewBaseMap) this.map.setLayoutProperty(sNewBaseMap, 'visibility', 'visible');
|
||||
},
|
||||
projectCodename(sNewCodeName, sOldCodeName) {
|
||||
console.log('change in projectCodename: '+sNewCodeName);
|
||||
//this.toggleSettingsPanel(false);
|
||||
this.$parent.setHash(this.$parent.hash.page, [sNewCodeName]);
|
||||
this.init();
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
project: this.project
|
||||
};
|
||||
},
|
||||
inject: ['spot', 'projects', 'user'],
|
||||
mounted() {
|
||||
this.spot.addPage('project', {
|
||||
onResize: () => {
|
||||
//this.spot.tmp('map_offset', -1 * (this.feedPanelOpen?getOuterWidth(this.$refs.feed):0) / getOuterWidth(window));
|
||||
|
||||
/* TODO
|
||||
if(typeof this.spot.tmp('elev') != 'undefined' && this.spot.tmp('elev')._showState) {
|
||||
this.spot.tmp('elev').resize({width:this.getElevWidth()});
|
||||
}
|
||||
*/
|
||||
}
|
||||
});
|
||||
|
||||
this.projectCodename = (this.$parent.hash.items.length==0)?this.spot.vars('default_project_codename'):this.$parent.hash.items[0];
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
let bFirstLoad = (typeof this.project.codename == 'undefined');
|
||||
this.initProject();
|
||||
if(bFirstLoad) this.initLightbox();
|
||||
this.initFeed();
|
||||
this.initMap();
|
||||
},
|
||||
initProject() {
|
||||
this.project = this.projects[this.projectCodename];
|
||||
this.modeHisto = (this.project.mode == this.spot.consts.modes.histo);
|
||||
this.feed = {loading:false, updatable:true, outOfData:false, refIdFirst:0, refIdLast:0, firstChunk:true};
|
||||
this.posts = [];
|
||||
//this.baseMap = null;
|
||||
this.baseMaps = {};
|
||||
},
|
||||
initLightbox() {
|
||||
lightbox.option({
|
||||
alwaysShowNavOnTouchDevices: true,
|
||||
albumLabel: '<i class="fa fa-fw fa-lg fa-media push"></i> %1 / %2',
|
||||
fadeDuration: 300,
|
||||
imageFadeDuration: 400,
|
||||
positionFromTop: 0,
|
||||
resizeDuration: 400,
|
||||
hasVideo: true,
|
||||
onMediaChange: (oMedia) => {
|
||||
this.spot.updateHash('media', oMedia.id);
|
||||
if(oMedia.set == 'post-medias') this.goToPost({type: 'media', id: oMedia.id});
|
||||
},
|
||||
onClosing: () => {this.spot.flushHash();}
|
||||
});
|
||||
},
|
||||
async initFeed() {
|
||||
//Simplebar event
|
||||
this.$refs.feedSimpleBar.scrollElement.addEventListener('scroll', (oEvent) => {this.onFeedScroll(oEvent);});
|
||||
|
||||
//Mobile Touchscreen Events
|
||||
//TODO
|
||||
|
||||
//Add post Event handling
|
||||
//TODO
|
||||
|
||||
await this.getNextFeed();
|
||||
|
||||
//Scroll to post
|
||||
if(this.$parent.hash.items.length == 3) this.findPost({type: this.$parent.hash.items[1], id: this.$parent.hash.items[2]});
|
||||
},
|
||||
async initMap() {
|
||||
//Get Map Info
|
||||
const aoMarkers = await this.spot.get2('markers', {id_project: this.project.id});
|
||||
this.baseMap = null;
|
||||
this.baseMaps = aoMarkers.maps;
|
||||
this.messages = aoMarkers.messages;
|
||||
|
||||
//Base maps (raster tiles)
|
||||
let asSources = {};
|
||||
let asLayers = [];
|
||||
for(const asBaseMap of this.baseMaps) {
|
||||
asSources[asBaseMap.codename] = {
|
||||
type: 'raster',
|
||||
tiles: [asBaseMap.pattern],
|
||||
tileSize: asBaseMap.tile_size
|
||||
};
|
||||
asLayers.push({
|
||||
id: asBaseMap.codename,
|
||||
type: 'raster',
|
||||
source: asBaseMap.codename,
|
||||
'layout': {'visibility': 'none'},
|
||||
minZoom: asBaseMap.min_zoom,
|
||||
maxZoom: asBaseMap.max_zoom
|
||||
});
|
||||
}
|
||||
|
||||
//Map
|
||||
if(this.map) this.map.remove();
|
||||
this.map = new Map({
|
||||
container: 'map',
|
||||
style: {
|
||||
version: 8,
|
||||
sources: asSources,
|
||||
layers: asLayers
|
||||
},
|
||||
attributionControl: false
|
||||
});
|
||||
|
||||
this.map.once('load', async () => {
|
||||
//Default Basemap
|
||||
this.baseMap = this.baseMaps.filter((asBM) => asBM.default_map)[0].codename;
|
||||
|
||||
//Get track
|
||||
const oTrack = await this.spot.get2('geojson', {id_project: this.project.id});
|
||||
this.map.addSource('track', {
|
||||
'type': 'geojson',
|
||||
'data': oTrack
|
||||
});
|
||||
|
||||
//Color mapping
|
||||
let asColorMapping = ['match', ['get', 'type']];
|
||||
for(const sHikeType in this.hikes.colors) {
|
||||
asColorMapping.push(sHikeType);
|
||||
asColorMapping.push(this.hikes.colors[sHikeType]);
|
||||
}
|
||||
asColorMapping.push('black'); //fallback value
|
||||
|
||||
//Track layer
|
||||
this.map.addLayer({
|
||||
'id': 'track',
|
||||
'type': 'line',
|
||||
'source': 'track',
|
||||
'layout': {
|
||||
'line-join': 'round',
|
||||
'line-cap': 'round'
|
||||
},
|
||||
'paint': {
|
||||
'line-color': asColorMapping,
|
||||
'line-width': this.hikes.width
|
||||
}
|
||||
});
|
||||
|
||||
//Markers
|
||||
let aoMarkerSource = {type:'geojson', data:{type: 'FeatureCollection', features: []}};
|
||||
for(const oMsg of this.messages) {
|
||||
aoMarkerSource.data.features.push({
|
||||
'type': 'Feature',
|
||||
'properties': {
|
||||
...oMsg,
|
||||
...{'description': ''}
|
||||
},
|
||||
'geometry': {
|
||||
'type': 'Point',
|
||||
'coordinates': [oMsg.longitude, oMsg.latitude]
|
||||
}
|
||||
});
|
||||
//Tooltip
|
||||
/*
|
||||
let $Tooltip = $($('<div>', {'class':'info-window'})
|
||||
.append($('<h1>')
|
||||
.addIcon('fa-message fa-lg', true)
|
||||
.append($('<span>').text(this.spot.lang('post_message')+' '+this.spot.lang('counter', oMsg.displayed_id)))
|
||||
.append($('<span>', {'class':'message-type'}).text('('+oMsg.type+')'))
|
||||
)
|
||||
.append($('<div>', {'class':'separator'}))
|
||||
.append($('<p>', {'class':'coordinates'})
|
||||
.addIcon('fa-coords fa-fw fa-lg', true)
|
||||
.append(this.getGoogleMapsLink(oMsg))
|
||||
)
|
||||
.append($('<p>', {'class':'time'})
|
||||
.addIcon('fa-time fa-fw fa-lg', true)
|
||||
.append(oMsg.formatted_time+(this.project.mode==this.spot.consts.modes.blog?' ('+oMsg.relative_time+')':''))))[0];
|
||||
|
||||
const vTooltip = h(SpotIcon, {icon:'project', 'classes':'fa-fw', text:'hikes'});
|
||||
|
||||
//let vTooltip = h(SpotIcon, {icon:'project', 'classes':'fa-fw', text:'hikes'});
|
||||
|
||||
oPopup.setDOMContent(vTooltip);
|
||||
|
||||
new Marker({
|
||||
element: $('<div style="width:'+this.markerSize.width+'px;height:'+this.markerSize.height+'px;"><span class="fa-stack"><i class="fa fa-message fa-stack-2x"></i><i class="fa fa-message-in fa-rotate-270 fa-stack-1x"></i></span></div>')[0],
|
||||
anchor: 'bottom'
|
||||
})
|
||||
.setLngLat(new LngLat(oMsg.longitude, oMsg.latitude))
|
||||
.setPopup(oPopup)
|
||||
.addTo(this.map)
|
||||
;
|
||||
*/
|
||||
}
|
||||
this.map.addSource('markers', aoMarkerSource);
|
||||
const image = await this.map.loadImage('https://maplibre.org/maplibre-gl-js/docs/assets/custom_marker.png');
|
||||
this.map.addImage('markerIcon', image.data);
|
||||
this.map.addLayer({
|
||||
'id': 'markers',
|
||||
'type': 'symbol',
|
||||
'source': 'markers',
|
||||
'layout': {
|
||||
//'icon-anchor': 'bottom',
|
||||
'icon-image': 'markerIcon'
|
||||
//'icon-overlap': 'always'
|
||||
}
|
||||
});
|
||||
this.map.on("click", "markers", (e) => {
|
||||
var oPopup = new Popup({
|
||||
anchor: 'bottom',
|
||||
offset: [0, this.markerSize.height * -1],
|
||||
closeButton: false
|
||||
})
|
||||
.setHTML('<div id="popup"></div>')
|
||||
.setLngLat(e.lngLat)
|
||||
.addTo(this.map);
|
||||
|
||||
let rProp = ref(e.features[0].properties);
|
||||
const vPopup = defineComponent({
|
||||
extends: ProjectPopup,
|
||||
setup: () => {
|
||||
console.log(rProp.value);
|
||||
provide('options', rProp.value);
|
||||
provide('spot', this.spot);
|
||||
provide('project', this.project);
|
||||
return {'options': rProp.value, 'spot':this.spot, 'project':this.project};
|
||||
}
|
||||
});
|
||||
nextTick(() => {
|
||||
createApp(vPopup).mount("#popup");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
//Centering map
|
||||
let bOpenFeedPanel = !this.mobile;
|
||||
let oBounds = new LngLatBounds();
|
||||
if(
|
||||
this.project.mode == this.spot.consts.modes.blog &&
|
||||
this.messages.length > 0 &&
|
||||
this.$parent.hash.items[2] != 'message'
|
||||
) {
|
||||
//Fit to last message
|
||||
let oLastMsg = this.messages[this.messages.length - 1];
|
||||
oBounds.extend(new LngLat(oLastMsg.longitude, oLastMsg.latitude));
|
||||
}
|
||||
else {
|
||||
//Fit to track
|
||||
for(const iFeatureId in oTrack.features) {
|
||||
oBounds = oTrack.features[iFeatureId].geometry.coordinates.reduce(
|
||||
(bounds, coord) => {
|
||||
return bounds.extend(coord);
|
||||
},
|
||||
oBounds
|
||||
);
|
||||
}
|
||||
}
|
||||
const iFeedPanelPadding = bOpenFeedPanel?(getOuterWidth(this.$refs.feed)/2):0;
|
||||
await this.map.fitBounds(
|
||||
oBounds,
|
||||
{
|
||||
padding: {
|
||||
top: 20,
|
||||
bottom: 20,
|
||||
left: (20 + iFeedPanelPadding),
|
||||
right: (20 + iFeedPanelPadding)
|
||||
},
|
||||
animate: false,
|
||||
maxZoom: 15
|
||||
}
|
||||
);
|
||||
|
||||
//Toggle only when map is ready, for the tilt effet
|
||||
this.toggleFeedPanel(bOpenFeedPanel);
|
||||
});
|
||||
|
||||
this.map.on('idle', () => {
|
||||
|
||||
});
|
||||
|
||||
//Legend
|
||||
|
||||
|
||||
|
||||
},
|
||||
getGoogleMapsLink(asInfo) {
|
||||
return $('<a>', {
|
||||
href:'https://www.google.com/maps/place/'+asInfo.lat_dms+'+'+asInfo.lon_dms+'/@'+asInfo.latitude+','+asInfo.longitude+',10z',
|
||||
title: this.spot.lang('see_on_google'),
|
||||
target: '_blank',
|
||||
rel: 'noreferrer noopener'
|
||||
}).text(asInfo.lat_dms+' '+asInfo.lon_dms);
|
||||
},
|
||||
async getNextFeed() {
|
||||
if(!this.feed.outOfData && !this.feed.loading) {
|
||||
//Get next chunk
|
||||
this.feed.loading = true;
|
||||
let aoData = await this.spot.get2('next_feed', {id_project: this.project.id, id: this.feed.refIdLast});
|
||||
let iPostCount = Object.keys(aoData.feed).length;
|
||||
this.feed.loading = false;
|
||||
this.feed.firstChunk = false;
|
||||
|
||||
//Update pointers
|
||||
this.feed.outOfData = (iPostCount < this.spot.consts.chunk_size);
|
||||
if(iPostCount > 0) {
|
||||
this.feed.refIdLast = aoData.ref_id_last;
|
||||
if(this.feed.firstChunk) this.feed.refIdFirst = aoData.ref_id_first;
|
||||
}
|
||||
|
||||
//Add posts
|
||||
this.posts.push(...aoData.feed);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
onFeedScroll(oEvent) {
|
||||
//FIXME remvove jquery dependency
|
||||
var $Box = $(oEvent.currentTarget);
|
||||
var $BoxContent = $Box.find('.simplebar-content');
|
||||
if(($Box.scrollTop() + $(window).height()) / $BoxContent.height() >= 0.8) this.getNextFeed();
|
||||
},
|
||||
async manageSubs() {
|
||||
var regexEmail = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
if(!regexEmail.test(this.user.email)) this.nlFeedbacks.push({type:'error', 'msg':this.spot.lang('nl_invalid_email')});
|
||||
else {
|
||||
this.spot.get2(this.nlAction, {'email': this.user.email, 'name': this.user.name}, this.nlLoading)
|
||||
.then((asUser, sDesc) => {
|
||||
this.nlFeedbacks.push('success', sDesc);
|
||||
this.user = asUser;
|
||||
})
|
||||
.catch((sDesc) => {this.nlFeedbacks.push('error', sDesc);});
|
||||
}
|
||||
},
|
||||
toggleFeedPanel(bShow, sMapAction) {
|
||||
let bOldValue = this.feedPanelOpen;
|
||||
this.feedPanelOpen = (typeof bShow === 'object' || typeof bShow === 'undefined')?(!this.feedPanelOpen):bShow;
|
||||
|
||||
if(bOldValue != this.feedPanelOpen && !this.mobile) {
|
||||
this.spot.onResize();
|
||||
|
||||
sMapAction = sMapAction || 'panTo';
|
||||
switch(sMapAction) {
|
||||
case 'none':
|
||||
break;
|
||||
case 'panTo':
|
||||
this.map.panBy(
|
||||
[(this.feedPanelOpen?1:-1) * getOuterWidth(this.$refs.feed) / 2, 0],
|
||||
{duration: 500}
|
||||
);
|
||||
break;
|
||||
case 'panToInstant':
|
||||
this.map.panBy([(this.feedPanelOpen?1:-1) * getOuterWidth(this.$refs.feed) / 2, 0]);
|
||||
break;
|
||||
case 'fitBounds':
|
||||
/*
|
||||
this.map.fitBounds(
|
||||
this.spot.tmp('track').getBounds(),
|
||||
{
|
||||
paddingTopLeft: L.point(5, this.spot.tmp('marker_size').height + 5),
|
||||
paddingBottomRight: L.point(this.spot.tmp('$Feed').outerWidth(true) + 5, 5)
|
||||
}
|
||||
);
|
||||
break;
|
||||
*/
|
||||
}
|
||||
}
|
||||
},
|
||||
toggleSettingsPanel(bShow, sMapAction) {
|
||||
let bOldValue = this.settingsPanelOpen;
|
||||
this.settingsPanelOpen = (typeof bShow === 'object' || typeof bShow === 'undefined')?(!this.settingsPanelOpen):bShow;
|
||||
|
||||
if(bOldValue != this.settingsPanelOpen && !this.mobile) {
|
||||
this.spot.onResize();
|
||||
|
||||
sMapAction = sMapAction || 'panTo';
|
||||
switch(sMapAction) {
|
||||
case 'none':
|
||||
break;
|
||||
case 'panTo':
|
||||
this.map.panBy(
|
||||
[(this.settingsPanelOpen?-1:1) * getOuterWidth(this.$refs.settings) / 2, 0],
|
||||
{duration: 500}
|
||||
);
|
||||
break;
|
||||
case 'panToInstant':
|
||||
this.map.panBy([(this.settingsPanelOpen?-1:1) * getOuterWidth(this.$refs.settings) /2, 0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
async findPost(oPost) {
|
||||
if(this.goToPost(oPost)) {
|
||||
//if(oPost.type=='media' || oPost.type=='message') $Post.find('a.drill').click();
|
||||
}
|
||||
else if(!this.feed.outOfData) {
|
||||
await this.getNextFeed();
|
||||
this.findPost(oPost);
|
||||
}
|
||||
else console.log('Missing element ID "'+oPost.id+'" of type "'+oPost.type+'"');
|
||||
},
|
||||
goToPost(oPost) {
|
||||
//TODO remove jquery deps
|
||||
let bFound = false;
|
||||
let aoRefs = this.$refs.posts.filter((post)=>{return post.postId == oPost.type+'-'+oPost.id;});
|
||||
if(aoRefs.length == 1) {
|
||||
this.$refs.feedSimpleBar.scrollElement.scrollTop += Math.round(
|
||||
$(aoRefs[0].$el).offset().top
|
||||
- parseInt($(this.$refs.feedSimpleBar.$el).css('padding-top'))
|
||||
);
|
||||
bFound = true;
|
||||
this.spot.flushHash(['post', 'message']);
|
||||
}
|
||||
|
||||
return bFound;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="projects" :class="projectClasses">
|
||||
<div id="background"></div>
|
||||
<div id="submap">
|
||||
<div class="loader fa fa-fw fa-map flicker"></div>
|
||||
</div>
|
||||
<div id="map"></div>
|
||||
<div id="settings" class="map-container map-container-left" ref="settings">
|
||||
<div id="settings-panel" class="map-panel">
|
||||
<div class="settings-header">
|
||||
<div class="logo"><img width="289" height="72" src="images/logo_black.png" alt="Spotty" /></div>
|
||||
<div id="last_update"><p><span><img src="images/spot-logo-only.svg" alt="" /></span><abbr></abbr></p></div>
|
||||
</div>
|
||||
<div class="settings-sections">
|
||||
<simplebar id="settings-sections-scrollbox">
|
||||
<div class="settings-section">
|
||||
<h1><SpotIcon :icon="'project'" :classes="'fa-fw'" :text="spot.lang('hikes')" /></h1>
|
||||
<div class="settings-section-body">
|
||||
<div class="radio" v-for="project in projects">
|
||||
<input type="radio" :id="project.id" :value="project.codename" v-model="projectCodename" />
|
||||
<label :for="project.id">
|
||||
<span>{{ project.name }}</span>
|
||||
<a class="download" :href="project.gpxfilepath" :title="spot.lang('track_download')" @click.stop="()=>{}">
|
||||
<SpotIcon :icon="'download'" :classes="'push-left'" />
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<h1><SpotIcon :icon="'map'" :classes="'fa-fw'" :text="spot.lang('maps')" /></h1>
|
||||
<div class="settings-section-body">
|
||||
<div class="radio" v-for="bm in baseMaps">
|
||||
<input type="radio" :id="bm.id_map" :value="bm.codename" v-model="baseMap" />
|
||||
<label :for="bm.id_map">{{ this.spot.lang('map_'+bm.codename) }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-section newsletter">
|
||||
<h1><SpotIcon :icon="'newsletter'" :classes="'fa-fw'" :text="spot.lang('newsletter')" /></h1>
|
||||
<input type="email" name="email" id="email" :placeholder="spot.lang('nl_email_placeholder')" v-model="user.email" :disabled="nlLoading || subscribed" />
|
||||
<SpotButton id="nl_btn" :classes="nlClasses" :title="spot.lang('nl_'+nlAction)" @click="manageSubs" />
|
||||
<div id="settings-feedback" class="feedback">
|
||||
<p v-for="feedback in nlFeedbacks" :class="feedback.type">
|
||||
<SpotIcon :icon="feedback.type" :text="feedback.msg" />
|
||||
</p>
|
||||
</div>
|
||||
{{ spot.lang(subscribed?'nl_subscribed_desc':'nl_unsubscribed_desc') }}
|
||||
</div>
|
||||
<div class="settings-section admin" v-if="spot.checkClearance(spot.consts.clearances.admin)">
|
||||
<h1><SpotIcon :icon="'admin fa-fw'" :text="spot.lang('admin')" /></h1>
|
||||
<a class="button" href="#admin"><SpotIcon :icon="'config'" :text="spot.lang('admin_config')" /></a>
|
||||
<a class="button" href="#upload"><SpotIcon :icon="'upload'" :text="spot.lang('admin_upload')" /></a>
|
||||
</div>
|
||||
</simplebar>
|
||||
</div>
|
||||
<div class="settings-footer">
|
||||
<a href="https://git.lutran.fr/franzz/spot" :title="spot.lang('credits_git')" target="_blank" rel="noopener">
|
||||
<SpotIcon :icon="'credits'" :text="spot.lang('credits_project')" />
|
||||
</a> {{ spot.lang('credits_license') }}</div>
|
||||
</div>
|
||||
<div :class="'map-control map-control-icon settings-control map-control-'+(mobile?'bottom':'top')" @click="toggleSettingsPanel">
|
||||
<SpotIcon :icon="settingsPanelOpen?'prev':'menu'" />
|
||||
</div>
|
||||
<div v-if="!mobile" id="legend" class="map-control settings-control map-control-bottom">
|
||||
<div v-for="(color, hikeType) in hikes.colors" class="track">
|
||||
<span class="line" :style="'background-color:'+color+'; height:'+hikes.width+'px;'"></span>
|
||||
<span class="desc">{{ spot.lang('track_'+hikeType) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="title" :class="'map-control settings-control map-control-'+(mobile?'bottom':'top')">
|
||||
<span>{{ project.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="feed" class="map-container map-container-right" ref="feed">
|
||||
<simplebar id="feed-panel" class="map-panel" ref="feedSimpleBar">
|
||||
<div id="feed-header">
|
||||
<ProjectPost v-if="modeHisto" :options="{type: 'archived', headerless: true}" />
|
||||
<ProjectPost v-else :options="{type: 'poster', relative_time: spot.lang('post_new_message')}" />
|
||||
</div>
|
||||
<div id="feed-posts">
|
||||
<ProjectPost v-for="post in posts" :options="post" ref="posts" />
|
||||
</div>
|
||||
<div id="feed-footer" v-if="feed.loading">
|
||||
<ProjectPost :options="{type: 'loading', headerless: true}" />
|
||||
</div>
|
||||
</simplebar>
|
||||
<div :class="'map-control map-control-icon feed-control map-control-'+(mobile?'bottom':'top')" @click="toggleFeedPanel">
|
||||
<SpotIcon :icon="feedPanelOpen?'next':'post'" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
17
src/components/projectMapLink.vue
Normal file
17
src/components/projectMapLink.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
options: Object
|
||||
},
|
||||
inject: ['spot']
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a
|
||||
:href="'https://www.google.com/maps/place/'+options.lat_dms+'+'+options.lon_dms+'/@'+options.latitude+','+options.longitude+',10z'"
|
||||
:title="spot.lang('see_on_google')"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>{{ options.lat_dms+' '+options.lon_dms }}</a>
|
||||
</template>
|
||||
60
src/components/projectMediaLink.vue
Normal file
60
src/components/projectMediaLink.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<script>
|
||||
import spotIcon from './spotIcon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
spotIcon
|
||||
},
|
||||
props: {
|
||||
options: Object,
|
||||
type: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title:''
|
||||
}
|
||||
},
|
||||
inject: ['spot'],
|
||||
mounted() {
|
||||
this.title =
|
||||
(this.$refs.comment?this.$refs.comment.outerHTML:'') +
|
||||
this.$refs[this.type=='marker'?'takenon':'postedon'].outerHTML +
|
||||
this.$refs[this.type=='marker'?'postedon':'takenon'].outerHTML
|
||||
;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a
|
||||
class="media-link drill"
|
||||
:href="options.media_path"
|
||||
:data-lightbox="type+'-medias'"
|
||||
:data-type="options.subtype"
|
||||
:data-id="options.id_media"
|
||||
:data-title="title"
|
||||
:data-orientation="options.rotate"
|
||||
>
|
||||
<img
|
||||
:src="options.thumb_path"
|
||||
:width="options.width"
|
||||
:height="options.height"
|
||||
:title="spot.lang((options.subtype == 'video')?'click_watch':'click_zoom')"
|
||||
class="clickable"
|
||||
/>
|
||||
<span class="drill-icon"><spotIcon :icon="'drill-'+options.subtype" /></span>
|
||||
<span v-if="options.comment" class="comment">{{ options.comment }}</span>
|
||||
</a>
|
||||
<div style="display:none">
|
||||
<span ref="comment" class="lb-caption-line comment desktop" :title="options.comment">
|
||||
<spotIcon :icon="'post'" :classes="'fa-lg fa-fw'" />
|
||||
<span class="comment-text">{{ options.comment }}</span>
|
||||
</span>
|
||||
<span ref="postedon" class="lb-caption-line" :title="$parent.timeDiff?spot.lang('local_time', options.posted_on_formatted_local):''">
|
||||
<spotIcon :icon="'upload'" :classes="'fa-lg fa-fw'" :text="options.posted_on_formatted" />
|
||||
</span>
|
||||
<span ref="takenon" class="lb-caption-line" :title="$parent.timeDiff?spot.lang('local_time', options.taken_on_formatted_local):''">
|
||||
<spotIcon :icon="options.subtype+'-shot'" :classes="'fa-lg fa-fw'" :text="options.taken_on_formatted" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
50
src/components/projectPopup.vue
Normal file
50
src/components/projectPopup.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script>
|
||||
import { options } from 'lightbox2';
|
||||
import projectMapLink from './projectMapLink.vue';
|
||||
import spotIcon from './spotIcon.vue';
|
||||
import projectRelTime from './projectRelTime.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
spotIcon,
|
||||
projectMapLink,
|
||||
projectRelTime
|
||||
},
|
||||
//props: {
|
||||
// options: Object,
|
||||
//},
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
//inject: ['options', 'spot', 'project'],
|
||||
mounted() {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="info-window">
|
||||
<h1>
|
||||
<spotIcon :icon="'message'" :classes="'fa-lg'" :text="spot.lang('post_message')+' '+spot.lang('counter', options.displayed_id)" />
|
||||
<span class="message-type">({{ options.type }})</span>
|
||||
</h1>
|
||||
<div class="separator"></div>
|
||||
<p class="coordinates">
|
||||
<spotIcon :icon="'coords'" :classes="'fa-fw fa-lg'" :margin="true" />
|
||||
<projectMapLink :options="options" />
|
||||
</p>
|
||||
<p class="time">
|
||||
<spotIcon :icon="'time'" :classes="'fa-fw fa-lg'" :text="options.formatted_time" />
|
||||
<span v-if="project.mode==spot.consts.modes.blog"> ({{ options.relative_time }})</span>
|
||||
</p>
|
||||
<p class="timezone" v-if="options.day_offset != '0'">
|
||||
<spotIcon :icon="'timezone'" :classes="'fa-fw fa-lg'" :margin="true" />
|
||||
<projectRelTime :localTime="options.formatted_time_local" :offset="options.day_offset" />
|
||||
</p>
|
||||
<p class="weather" v-if="options.weather_icon && options.weather_icon!='unknown'" :title="options.weather_cond==''?'':spot.lang(options.weather_cond)">
|
||||
<spotIcon :icon="options.weather_icon" :classes="'fa-fw fa-lg'" :text="options.weather_temp+'°C'" />
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
170
src/components/projectPost.vue
Normal file
170
src/components/projectPost.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<script>
|
||||
import spotIcon from './spotIcon.vue';
|
||||
import spotButton from './spotButton.vue';
|
||||
import projectMediaLink from './projectMediaLink.vue';
|
||||
import projectMapLink from './projectMapLink.vue';
|
||||
import projectRelTime from './projectRelTime.vue';
|
||||
|
||||
import autosize from 'autosize';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
spotIcon,
|
||||
spotButton,
|
||||
projectMediaLink,
|
||||
projectMapLink,
|
||||
projectRelTime
|
||||
},
|
||||
props: {
|
||||
options: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mouseOverHeader: false,
|
||||
absTime: this.options.formatted_time,
|
||||
absTimeLocal: this.options.formatted_time_local,
|
||||
timeDiff: (this.options.formatted_time && this.options.formatted_time_local != this.options.formatted_time),
|
||||
anchorVisible: ['message', 'media', 'post'].includes(this.options.type),
|
||||
anchorTitle: this.spot.lang('copy_to_clipboard'),
|
||||
anchorIcon: 'link'
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
postClass() {
|
||||
let sHeaderLess = this.options.headerless?' headerless':'';
|
||||
return 'post-item '+this.options.type+sHeaderLess;
|
||||
},
|
||||
postId() {
|
||||
return this.options.id?(this.options.type+'-'+this.options.id):'';
|
||||
},
|
||||
subType() {
|
||||
return this.options.subtype || this.options.type;
|
||||
},
|
||||
displayedId() {
|
||||
return this.options.displayed_id?(this.spot.lang('counter', this.options.displayed_id)):'';
|
||||
},
|
||||
hash() {
|
||||
let asHash = this.spot.getHash();
|
||||
return '#'+[asHash.page, asHash.items[0], this.options.type, this.options.id].join(this.spot.consts.hash_sep);
|
||||
},
|
||||
modeHisto() {
|
||||
return (this.project.mode==this.spot.consts.modes.histo);
|
||||
},
|
||||
relTime() {
|
||||
return this.modeHisto?(this.options.formatted_time || '').substr(0, 10):this.options.relative_time;
|
||||
},
|
||||
|
||||
},
|
||||
inject: ['spot', 'project', 'user'],
|
||||
methods: {
|
||||
copyAnchor() {
|
||||
copyTextToClipboard(this.spot.consts.server+this.spot.hash());
|
||||
this.anchorTitle = this.spot.lang('link_copied');
|
||||
this.anchorIcon = 'copied';
|
||||
setTimeout(()=>{ //TODO animation
|
||||
this.anchorTitle = this.spot.lang('copy_to_clipboard');
|
||||
this.anchorIcon = 'link';
|
||||
}, 5000);
|
||||
},
|
||||
panMapToMessage() {
|
||||
//TODO
|
||||
/*
|
||||
var $Parent = $(oEvent.currentTarget).parent();
|
||||
var oMarker = this.spot.tmp(['markers', $Parent.data('id')]);
|
||||
if(this.isMobile()) {
|
||||
this.toggleFeedPanel(false, 'panToInstant');
|
||||
this.spot.tmp('map').setView(oMarker.getLatLng(), 15);
|
||||
}
|
||||
else {
|
||||
var iOffset = (this.isFeedPanelOpen()?1:-1)*this.spot.tmp('$Feed').outerWidth(true)/2 - (this.isSettingsPanelOpen()?1:-1)*this.spot.tmp('$Settings').outerWidth(true)/2;
|
||||
var iRatio = -1 * iOffset / $('body').outerWidth(true);
|
||||
this.spot.tmp('map').setOffsetView(iRatio, oMarker.getLatLng(), 15);
|
||||
}
|
||||
|
||||
$Parent.data('clicked', true);
|
||||
if(!oMarker.isPopupOpen()) oMarker.openPopup();
|
||||
*/
|
||||
},
|
||||
openMarkerPopup() {
|
||||
//TODO
|
||||
/*
|
||||
let oMarker = this.spot.tmp(['markers', $(oEvent.currentTarget).data('id')]);
|
||||
if(this.spot.tmp('map') && this.spot.tmp('map').getBounds().contains(oMarker.getLatLng()) && !oMarker.isPopupOpen()) oMarker.openPopup();
|
||||
*/
|
||||
},
|
||||
closeMarkerPopup() {
|
||||
//TODO
|
||||
/*
|
||||
let $This = $(oEvent.currentTarget);
|
||||
let oMarker = this.spot.tmp(['markers', $This.data('id')]);
|
||||
if(oMarker && oMarker.isPopupOpen() && !$This.data('clicked')) oMarker.closePopup();
|
||||
$This.data('clicked', false);
|
||||
*/
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
//Auto-adjust text area height
|
||||
if(this.options.type == 'poster') autosize(this.$refs.post);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="postClass" :id="postId">
|
||||
<div class="header">
|
||||
<div class="index">
|
||||
<spotIcon :icon="subType" :text="displayedId" />
|
||||
<a v-if="anchorVisible" class="link desktop" @click="copyAnchor" ref="anchor" :href="hash" :title="anchorTitle">
|
||||
<spotIcon :icon="anchorIcon" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="time" @mouseleave="mouseOverHeader = false" @mouseover="mouseOverHeader = true" :title="timeDiff?spot.lang('local_time', absTimeLocal):''">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<span v-if="mouseOverHeader">{{ timeDiff?spot.lang('your_time', absTime):absTime }}</span>
|
||||
<span v-else>{{ relTime }}</span>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div v-if="options.type == 'message'" class="body-box" @mouseenter="openMarkerPopup" @mouseleave="closeMarkerPopup">
|
||||
<p><spotIcon :icon="'coords'" :classes="'push'" /><projectMapLink :options="options" /></p>
|
||||
<p><spotIcon :icon="'time'" :text="absTime" /></p>
|
||||
<p v-if="timeDiff"><spotIcon :icon="'timezone'" :classes="'push'" /><projectRelTime :localTime="absTimeLocal" :offset="options.day_offset" /></p>
|
||||
<a class="drill" @click.prevent="panMapToMessage">
|
||||
<span v-if="options.weather_icon && options.weather_icon!='unknown'" class="weather clickable" :title="spot.lang(options.weather_cond)">
|
||||
<spotIcon :icon="options.weather_icon" />
|
||||
<span>{{ options.weather_temp+'°C' }}</span>
|
||||
</span>
|
||||
<img class="staticmap clickable" :title="spot.lang('click_zoom')" :src="options.static_img_url" />
|
||||
<span class="drill-icon fa-stack clickable">
|
||||
<spotIcon :icon="'message'" :classes="'fa-stack-2x clickable'" />
|
||||
<spotIcon :icon="'message-in'" :classes="'fa-stack-1x fa-rotate-270'" />
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div v-else-if="options.type == 'media'" class="body-box">
|
||||
<projectMediaLink :options="options" :type="'post'" />
|
||||
</div>
|
||||
<div v-else-if="options.type == 'post'">
|
||||
<p class="message">{{ options.content }}</p>
|
||||
<p class="signature">
|
||||
<img v-if="options.gravatar" :src="'data:image/png;base64, '+options.gravatar" width="24" height="24" alt="--" />
|
||||
<span v-else>-- </span>
|
||||
<span>{{ options.formatted_name }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<p v-else-if="options.type == 'poster'" class="message">
|
||||
<textarea ref="post" name="post" :placeholder="spot.lang('post_message')" class="autoExpand" rows="1" v-model="$parent.post"></textarea>
|
||||
<input type="text" name="name" :placeholder="spot.lang('post_name')" v-model="user.name" />
|
||||
<spotButton name="submit" :aria-label="spot.lang('send')" :title="spot.lang('send')" :icon="'send'" />
|
||||
</p>
|
||||
<div v-else-if="options.type == 'archived'">
|
||||
<p><spotIcon :icon="'success'" /></p>
|
||||
<p>{{ spot.lang('mode_histo') }}</p>
|
||||
</div>
|
||||
<div v-else-if="options.type == 'loading'">
|
||||
<p class="flicker"><spotIcon :icon="'post'" /></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
17
src/components/projectRelTime.vue
Normal file
17
src/components/projectRelTime.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
localTime: String,
|
||||
offset: String
|
||||
},
|
||||
inject: ['spot']
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span>
|
||||
<span>{{ localTime.substring(-5) }}</span>
|
||||
<sup v-if="offset != '0'" :title="offset+' '+spot.lang('unit_day')+' ('+localTime.substring(0, 5)+')'">{{ ' '+offset }}</sup>
|
||||
<span> {{ spot.lang('local_time', ' ').trim() }}</span>
|
||||
</span>
|
||||
</template>
|
||||
17
src/components/spotButton.vue
Normal file
17
src/components/spotButton.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
import SpotIcon from './spotIcon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SpotIcon
|
||||
},
|
||||
props: {
|
||||
classes: String,
|
||||
text: String,
|
||||
icon: String
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<button :class="classes"><SpotIcon :icon="icon" :text="text" /></button>
|
||||
</template>
|
||||
19
src/components/spotIcon.vue
Normal file
19
src/components/spotIcon.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
icon: String,
|
||||
text: String,
|
||||
margin: Boolean,
|
||||
classes: String
|
||||
},
|
||||
computed: {
|
||||
classNames() {
|
||||
return 'fa fa-'+this.icon+((this.margin || this.text && this.text!='')?' push':'')+(this.classes?' '+this.classes:'')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<i :class="classNames"></i>{{ text }}
|
||||
</template>
|
||||
100
src/components/upload.vue
Normal file
100
src/components/upload.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<script>
|
||||
import SpotIcon from './spotIcon.vue';
|
||||
import SpotButton from './spotButton.vue';
|
||||
import "blueimp-file-upload/js/vendor/jquery.ui.widget.js";
|
||||
import "blueimp-file-upload/js/jquery.iframe-transport.js";
|
||||
import "blueimp-file-upload/js/jquery.fileupload.js";
|
||||
|
||||
export default {
|
||||
name: 'upload',
|
||||
components: { SpotButton, SpotIcon },
|
||||
inject: ['spot', 'projects', 'consts', 'user'],
|
||||
data() {
|
||||
return {
|
||||
project: this.projects[this.spot.vars('default_project_codename')],
|
||||
files: [],
|
||||
logs: [],
|
||||
progress: 0
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.spot.addPage('upload', {});
|
||||
|
||||
if(this.project.editable) {
|
||||
$('#fileupload')
|
||||
.fileupload({
|
||||
dataType: 'json',
|
||||
formData: {t: this.user.timezone},
|
||||
acceptFileTypes: /(\.|\/)(gif|jpe?g|png|mov)$/i,
|
||||
done: (e, asData) => {
|
||||
$.each(asData.result.files, (iKey, oFile) => {
|
||||
let bError = ('error' in oFile);
|
||||
|
||||
//Feedback
|
||||
this.logs.push(bError?oFile.error:(this.spot.lang('upload_success', [oFile.name])));
|
||||
|
||||
//Comments
|
||||
oFile.content = '';
|
||||
if(!bError) this.files.push(oFile);
|
||||
});
|
||||
},
|
||||
progressall: (e, data) => {
|
||||
this.progress = parseInt(data.loaded / data.total * 100, 10);
|
||||
}
|
||||
});
|
||||
}
|
||||
else this.logs = [this.spot.lang('upload_mode_archived', [this.project.name])];
|
||||
},
|
||||
methods: {
|
||||
addComment(oFile) {
|
||||
this.spot.get2('add_comment', {id: oFile.id, content: oFile.content})
|
||||
.then((asData) => {this.logs.push(this.spot.lang('media_comment_update', asData.filename));})
|
||||
.catch((sMsgId) => {this.logs.push(this.spot.lang(sMsgId));});
|
||||
},
|
||||
addPosition() {
|
||||
if(navigator.geolocation) {
|
||||
this.logs.push('Determining position...');
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
this.logs.push('Sending position...');
|
||||
this.spot.get2('add_position', {'latitude':position.coords.latitude, 'longitude':position.coords.longitude, 'timestamp':Math.round(position.timestamp / 1000)})
|
||||
.then((asData) => {this.logs.push('Position sent');})
|
||||
.catch((sMsgId) => {this.logs.push(self.lang(sMsgId));});
|
||||
},
|
||||
(error) => {
|
||||
this.logs.push(error.message);
|
||||
}
|
||||
);
|
||||
}
|
||||
else this.logs.push('This browser does not support geolocation');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div id="upload">
|
||||
<a name="back" class="button" href="#project"><SpotIcon :icon="'back'" :text="spot.lang('nav_back')" /></a>
|
||||
<h1>{{ spot.lang('upload_title') }}</h1>
|
||||
<h2>{{ this.project.name }}</h2>
|
||||
<div class="section" v-if="project.editable">
|
||||
<input id="fileupload" type="file" name="files[]" :data-url="this.spot.getActionLink('upload')" multiple />
|
||||
</div>
|
||||
<div class="section progress" v-if="progress > 0">
|
||||
<div class="bar" :style="{width:progress+'%'}"></div>
|
||||
</div>
|
||||
<div class="section comment" v-for="file in files">
|
||||
<img class="thumb" :src="file.thumbnail" />
|
||||
<div class="form">
|
||||
<input class="content" name="content" type="text" v-model="file.content" />
|
||||
<input class="id" name="id" type="hidden" :value="file.id" />
|
||||
<SpotButton :classes="'save'" :icon="'save'" :text="spot.lang('save')" @click="addComment(file)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="section location">
|
||||
<SpotButton :icon="'message'" :text="spot.lang('new_position')" @click="addPosition()" />
|
||||
</div>
|
||||
<div class="section logs" v-if="logs.length > 0">
|
||||
<p class="log" v-for="log in logs">{{ log }}.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,71 +0,0 @@
|
||||
<div id="admin">
|
||||
<a name="back" class="button" href="[#]server[#]"><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>
|
||||
@@ -20,12 +20,10 @@
|
||||
<meta name="msapplication-config" content="images/icons/browserconfig.xml?v=GvmqYyKwbb">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<script type="text/javascript">window.params = [#]GLOBAL_VARS[#];</script>
|
||||
<script type="text/javascript" src="[#]filepath_js[#]"></script>
|
||||
<title>Spotty</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<div id="main"></div>
|
||||
</div>
|
||||
<div id="container"></div>
|
||||
<script type="module" src="[#]filepath_js[#]"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,35 +2,40 @@
|
||||
import './jquery.helpers.js';
|
||||
|
||||
//Common
|
||||
import { copyArray, getElem, setElem, getDragPosition, copyTextToClipboard } from './common.js';
|
||||
window.copyArray = copyArray;
|
||||
window.getElem = getElem;
|
||||
window.setElem = setElem;
|
||||
window.getDragPosition = getDragPosition;
|
||||
window.copyTextToClipboard = copyTextToClipboard;
|
||||
import * as common from './common.js';
|
||||
window.copyArray = common.copyArray;
|
||||
window.getElem = common.getElem;
|
||||
window.setElem = common.setElem;
|
||||
window.getDragPosition = common.getDragPosition;
|
||||
window.copyTextToClipboard = common.copyTextToClipboard;
|
||||
window.getOuterWidth = common.getOuterWidth;
|
||||
|
||||
import Css from './../styles/spot.scss';
|
||||
import LogoText from '../images/logo_black.png';
|
||||
import Logo from '../images/spot-logo-only.svg';
|
||||
console.log(Logo);
|
||||
|
||||
//Masks
|
||||
import Spot from './spot.js';
|
||||
import Project from './page.project.js';
|
||||
import Upload from './page.upload.js';
|
||||
import Admin from './page.admin.js';
|
||||
//import Project from './page.project.js';
|
||||
//import Upload from './page.upload.js';
|
||||
//import Admin from './page.admin.js';
|
||||
|
||||
//const Upload = () => import('@scripts/page.upload.js');
|
||||
window.oSpot = new Spot(params);
|
||||
|
||||
let oSpot = new Spot(params);
|
||||
//let oProject = new Project(oSpot);
|
||||
//oSpot.addPage('project', oProject);
|
||||
|
||||
let oProject = new Project(oSpot);
|
||||
oSpot.addPage('project', oProject);
|
||||
//let oUpload = new Upload(oSpot);
|
||||
//oSpot.addPage('upload', oUpload);
|
||||
|
||||
let oUpload = new Upload(oSpot);
|
||||
oSpot.addPage('upload', oUpload);
|
||||
//let oAdmin = new Admin(oSpot);
|
||||
//oSpot.addPage('admin', oAdmin);
|
||||
|
||||
let oAdmin = new Admin(oSpot);
|
||||
oSpot.addPage('admin', oAdmin);
|
||||
//$(() => {oSpot.init();});
|
||||
|
||||
$(() => {oSpot.init();});
|
||||
import { createApp } from 'vue';
|
||||
import SpotVue from '../Spot.vue';
|
||||
|
||||
const oSpotVue = createApp(SpotVue);
|
||||
oSpotVue.provide('spot', window.oSpot);
|
||||
oSpotVue.mount('#container');
|
||||
@@ -66,3 +66,17 @@ export function copyTextToClipboard(text) {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function getOuterWidth(element) {
|
||||
var style = getComputedStyle(element);
|
||||
var width = element.offsetWidth; // Width without padding and border
|
||||
width += parseInt(style.marginLeft) + parseInt(style.marginRight); // Add margins
|
||||
|
||||
// Check if the box-sizing is border-box (includes padding and border in the width)
|
||||
if (style.boxSizing === 'border-box') {
|
||||
width += parseInt(style.paddingLeft) + parseInt(style.paddingRight); // Add padding
|
||||
width += parseInt(style.borderLeftWidth) + parseInt(style.borderRightWidth); // Add border
|
||||
}
|
||||
|
||||
return width;
|
||||
}
|
||||
@@ -1101,7 +1101,11 @@ export default class Project {
|
||||
.addIcon('fa-'+asData.subtype+'-shot fa-lg fa-fw', true)
|
||||
.append(asData.taken_on_formatted);
|
||||
|
||||
var $Title = $('<div>').append($Comment).append(sType=='marker'?$TakenOn:$PostedOn).append(sType=='marker'?$PostedOn:$TakenOn);
|
||||
var $Title = $('<div>')
|
||||
.append($Comment)
|
||||
.append(sType=='marker'?$TakenOn:$PostedOn)
|
||||
.append(sType=='marker'?$PostedOn:$TakenOn);
|
||||
|
||||
var $Link =
|
||||
$('<a>', {
|
||||
'class': 'media-link drill',
|
||||
|
||||
@@ -75,7 +75,7 @@ export default class Spot {
|
||||
if(oData.desc.substr(0, this.consts.lang_prefix.length)==this.consts.lang_prefix) oData.desc = this.lang(oData.desc.substr(5));
|
||||
|
||||
if(oData.result==this.consts.error) fOnError(oData.desc);
|
||||
else fOnSuccess(oData.data, oData.desc);
|
||||
else if(fOnSuccess) fOnSuccess(oData.data, oData.desc);
|
||||
})
|
||||
.fail((jqXHR, textStatus, errorThrown) => {
|
||||
fonProgress('fail');
|
||||
@@ -83,14 +83,48 @@ export default class Spot {
|
||||
});
|
||||
}
|
||||
|
||||
async get2(sAction, oVars, bLoading) {
|
||||
oVars = oVars || {};
|
||||
oVars['a'] = sAction;
|
||||
oVars['t'] = this.consts.timezone;
|
||||
bLoading = true;
|
||||
|
||||
let oUrl = new URL(this.consts.server+this.consts.process_page);
|
||||
oUrl.search = new URLSearchParams(oVars).toString();
|
||||
|
||||
try {
|
||||
let oUrl = new URL(this.consts.server+this.consts.process_page);
|
||||
oUrl.search = new URLSearchParams(oVars).toString();
|
||||
const oRequest = await fetch(oUrl, {method: 'GET', /*body: JSON.stringify(oVars),*/ headers: {"Content-Type": "application/json"}});
|
||||
if(!oRequest.ok) {
|
||||
bLoading = false;
|
||||
throw new Error('Error HTTP '+oRequest.status+': '+oRequest.statusText);
|
||||
}
|
||||
else {
|
||||
let oResponse = await oRequest.json();
|
||||
bLoading = false;
|
||||
if(oResponse.desc.substr(0, this.consts.lang_prefix.length)==this.consts.lang_prefix) oResponse.desc = this.lang(oData.desc.substr(this.consts.lang_prefix.length));
|
||||
|
||||
if(oResponse.result == this.consts.error) return Promise.reject(oResponse.desc);
|
||||
else return Promise.resolve(oResponse.data, oResponse.desc);
|
||||
}
|
||||
}
|
||||
catch(oError) {
|
||||
bLoading = false;
|
||||
throw oError;
|
||||
}
|
||||
}
|
||||
|
||||
lang(sKey, asParams) {
|
||||
asParams = asParams || [];
|
||||
if(typeof asParams == 'string') asParams = [asParams];
|
||||
|
||||
if(typeof asParams != 'object') asParams = [asParams];
|
||||
|
||||
var sLang = '';
|
||||
if(sKey in this.consts.lang) {
|
||||
sLang = this.consts.lang[sKey];
|
||||
for(let i in asParams) sLang = sLang.replace('$'+i, asParams[i]);
|
||||
for(let i in asParams) {
|
||||
sLang = sLang.replace('$'+i, asParams[i]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log('missing translation: '+sKey);
|
||||
@@ -100,6 +134,10 @@ export default class Spot {
|
||||
return sLang;
|
||||
}
|
||||
|
||||
isMobile() {
|
||||
return $('#mobile').is(':visible');
|
||||
}
|
||||
|
||||
/* Page Switch - Trigger & Event catching */
|
||||
|
||||
onHashChange() {
|
||||
@@ -176,6 +214,7 @@ export default class Spot {
|
||||
asContext = asContext || {};
|
||||
let sPage = this.vars('page');
|
||||
if(this.pages[sPage].onFeedback) this.pages[sPage].onFeedback(sType, sMsg, asContext);
|
||||
else console.log({type:sType, msg:sMsg, context:asContext});
|
||||
}
|
||||
|
||||
onKeydown(oEvent) {
|
||||
|
||||
@@ -139,3 +139,9 @@ h1 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
|
||||
#mobile {
|
||||
display: none;
|
||||
}
|
||||
@@ -79,6 +79,9 @@ $fa-css-prefix: fa;
|
||||
.#{$fa-css-prefix}-config:before { content: fa-content($fa-var-cogs); }
|
||||
.#{$fa-css-prefix}-upload:before { content: fa-content($fa-var-cloud-upload); }
|
||||
|
||||
/* Upload */
|
||||
.#{$fa-css-prefix}-save:before { content: fa-content($fa-var-floppy-disk); }
|
||||
|
||||
/* Feed */
|
||||
.#{$fa-css-prefix}-post:before { content: fa-content($fa-var-comment); }
|
||||
.#{$fa-css-prefix}-media:before { content: fa-content($fa-var-photo-video); }
|
||||
@@ -95,7 +98,7 @@ $fa-css-prefix: fa;
|
||||
.#{$fa-css-prefix}-video-shot:before { content: fa-content($fa-var-camcorder); }
|
||||
.#{$fa-css-prefix}-image-shot:before { content: fa-content($fa-var-camera-alt); }
|
||||
.#{$fa-css-prefix}-link:before { content: fa-content($fa-var-link); }
|
||||
.#{$fa-css-prefix}-link.copied:before { content: fa-content($fa-var-check); }
|
||||
.#{$fa-css-prefix}-copied:before { content: fa-content($fa-var-check); }
|
||||
|
||||
/* Feed - Poster */
|
||||
.#{$fa-css-prefix}-poster:before { content: fa-content($fa-var-comment-edit); }
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
$theme : "spot-theme";
|
||||
$base-color : #CCC;
|
||||
$highlight-color : #FFF;
|
||||
$background : rgba($base-color, 0.2);
|
||||
$drag-color : rgba($highlight-color, 0.2);
|
||||
$axis-color : darken($base-color,20%);
|
||||
$stroke-color : darken($base-color,40%);
|
||||
$stroke-width-mouse-focus : 1;
|
||||
$stroke-width-height-focus: 2;
|
||||
$stroke-width-axis : 2;
|
||||
|
||||
@import '../../node_modules/leaflet/dist/leaflet.css';
|
||||
@import '../../node_modules/leaflet.heightgraph/src/L.Control.Heightgraph.css';
|
||||
|
||||
/* Leaflet fixes */
|
||||
.leaflet-container {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.leaflet-popup {
|
||||
.leaflet-popup-content-wrapper {
|
||||
border-radius: 5px;
|
||||
padding: 0;
|
||||
|
||||
.leaflet-popup-content {
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.leaflet-control.spot-control, .leaflet-control.heightgraph .heightgraph-toggle {
|
||||
@extend .clickable;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
text-align: center;
|
||||
box-shadow: none;
|
||||
|
||||
.fa {
|
||||
@extend .control-icon;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Leaflet Heightgraph fixes */
|
||||
|
||||
.legend-text, .tick, .tick text, .focusbox, .height-focus.circle, .height-focus.label, .lineSelection, .horizontalLineText {
|
||||
fill: #333 !important;
|
||||
}
|
||||
|
||||
.axis path, .focusbox rect, .focusLine line, .height-focus.label rect, .height-focus.line, .horizontalLine {
|
||||
stroke: #333 !important;
|
||||
}
|
||||
|
||||
.focusbox rect, .height-focus.label rect {
|
||||
stroke-width: 0;
|
||||
}
|
||||
|
||||
.focusLine line, .focusbox rect, .height-focus.label rect {
|
||||
-webkit-filter: drop-shadow(1px 0px 2px rgba(0, 0, 0, 0.6));
|
||||
filter: drop-shadow(1px 0px 2px rgba(0, 0, 0, 0.6));
|
||||
}
|
||||
|
||||
.height-focus.label rect, .focusbox rect {
|
||||
fill: rgba(255,255,255,.6);
|
||||
}
|
||||
|
||||
.heightgraph.leaflet-control {
|
||||
svg.heightgraph-container {
|
||||
background: none;
|
||||
border-radius: 0;
|
||||
|
||||
.area {
|
||||
@include drop-shadow(0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.horizontalLine {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.heightgraph-toggle {
|
||||
background: none;
|
||||
|
||||
.heightgraph-toggle-icon {
|
||||
@extend .control-icon;
|
||||
@extend .fa-elev-chart;
|
||||
height: 44px;
|
||||
position: static;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.heightgraph-close-icon {
|
||||
@extend .control-icon;
|
||||
@extend .fa-unsubscribe;
|
||||
background: none;
|
||||
font-size: 20px;
|
||||
line-height: 26px;
|
||||
width: 26px;
|
||||
text-align: center;
|
||||
display: none;
|
||||
|
||||
&:before {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.leaflet-default-icon-path {
|
||||
background-image: none;
|
||||
}
|
||||
@@ -1,70 +1,51 @@
|
||||
@media only screen and (max-width: 800px) {
|
||||
|
||||
$panel-width: "100vw - #{$button-width} - 2 * #{$block-spacing}";
|
||||
$panel-width-max: $panel-width;
|
||||
$panel-actual-width: $panel-width;
|
||||
|
||||
.desktop {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#projects {
|
||||
#feed, #settings {
|
||||
.map-container {
|
||||
width: calc(#{$panel-width});
|
||||
max-width: calc(#{$panel-width});
|
||||
}
|
||||
|
||||
#feed {
|
||||
right: calc((#{$panel-width}) * -1);
|
||||
}
|
||||
|
||||
#settings {
|
||||
left: calc((#{$panel-width}) * -1);
|
||||
}
|
||||
|
||||
#title {
|
||||
width: calc(#{$panel-width} - #{$button-width} - 4 * #{$block-spacing});
|
||||
max-width: calc(#{$panel-width} - #{$button-width} - 4 * #{$block-spacing});
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.leaflet-right, .leaflet-left {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.with-feed, &.with-settings {
|
||||
#submap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.leaflet-control-container .leaflet-top.leaflet-right {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#title {
|
||||
width: calc(#{$panel-width} - #{$button-width} - 4 * #{$block-spacing});
|
||||
max-width: calc(#{$panel-width} - #{$button-width} - 4 * #{$block-spacing});
|
||||
max-width: calc(100vw - #{$block-spacing} - #{$panel-actual-width} - (#{$button-width} + #{$block-spacing} * 2) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
&.with-feed {
|
||||
.leaflet-right {
|
||||
right: calc(#{$panel-width});
|
||||
}
|
||||
.leaflet-left {
|
||||
left: calc((#{$panel-width}) * -1);
|
||||
}
|
||||
}
|
||||
|
||||
&.with-settings {
|
||||
.leaflet-right {
|
||||
right: calc((#{$panel-width}) * -1);
|
||||
}
|
||||
.leaflet-left {
|
||||
left: calc(#{$panel-width});
|
||||
#submap {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.leaflet-control-container .leaflet-top.leaflet-left {
|
||||
display: none;
|
||||
&.with-feed {
|
||||
.map-container-left {
|
||||
transform: translateX(-200vw);
|
||||
}
|
||||
|
||||
.map-container-right {
|
||||
transform: translateX(calc(#{$button-width} + #{$block-spacing} * 2));
|
||||
}
|
||||
}
|
||||
|
||||
&.with-settings {
|
||||
.map-container-left {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.map-container-right {
|
||||
transform: translateX(200vw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,11 +62,9 @@
|
||||
a.lb-next::before {
|
||||
right: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 801px) {
|
||||
.mobile {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
#mobile {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
256
src/styles/_page.project.feed.scss
Normal file
256
src/styles/_page.project.feed.scss
Normal file
@@ -0,0 +1,256 @@
|
||||
#feed {
|
||||
#feed-panel {
|
||||
#feed-header {
|
||||
.poster {
|
||||
textarea[name=post] {
|
||||
margin-bottom: 1em;
|
||||
width: calc(100% - 2em);
|
||||
}
|
||||
|
||||
input[name=name] {
|
||||
width: calc(100% - 6em);
|
||||
}
|
||||
|
||||
button[name=submit] {
|
||||
margin-left: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.archived {
|
||||
background: #EEE;
|
||||
}
|
||||
}
|
||||
|
||||
#feed-posts {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.body-box {
|
||||
position:relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.post-item {
|
||||
margin-bottom: $block-spacing;
|
||||
background: $post-bg;
|
||||
color: $post-color;
|
||||
border-radius: $block-radius;
|
||||
width: calc(100% - #{$block-spacing});
|
||||
box-shadow: 2px 2px 3px 0px rgba(0, 0, 0, 0.5);
|
||||
|
||||
a {
|
||||
color: $post-color;
|
||||
&:hover {
|
||||
color: $post-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
margin: 0;
|
||||
}
|
||||
.signature {
|
||||
margin: $elem-spacing 0 0 0;
|
||||
text-align: right;
|
||||
font-style: italic;
|
||||
|
||||
img {
|
||||
vertical-align: baseline;
|
||||
margin: 0 0.2em calc((1em - 24px)/2) 0;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
.header {
|
||||
padding: 0 $block-spacing;
|
||||
position: relative;
|
||||
|
||||
div {
|
||||
display: inline-block;
|
||||
font-size: 0.8em;
|
||||
padding: $elem-spacing 0px;
|
||||
|
||||
&.index {
|
||||
width: 25%;
|
||||
|
||||
.link {
|
||||
margin-left: $elem-spacing;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.time {
|
||||
width: 75%;
|
||||
text-align: right;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
.body {
|
||||
clear: both;
|
||||
padding: 0em $block-spacing $block-spacing;
|
||||
}
|
||||
|
||||
&.headerless {
|
||||
.header {
|
||||
display: none;
|
||||
}
|
||||
.body {
|
||||
padding-top: $block-spacing;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
|
||||
.fa {
|
||||
display: inline-block;
|
||||
font-size: 2em;
|
||||
margin: $elem-spacing 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.message {
|
||||
background: $message-bg;
|
||||
color: $message-color;
|
||||
|
||||
p {
|
||||
font-size: 0.9em;
|
||||
height: 1em;
|
||||
margin: 0 0 $elem-spacing 0;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $message-color;
|
||||
&:hover {
|
||||
color: $message-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
a.drill {
|
||||
line-height: 0;
|
||||
|
||||
.drill-icon {
|
||||
transform: translate(-16px, -32px);
|
||||
|
||||
.fa-message-in {
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.fa-message {
|
||||
@extend .#{$fa-css-prefix}-drill-message;
|
||||
top: 13px;
|
||||
left: 3px;
|
||||
}
|
||||
.fa-message-in {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.weather {
|
||||
position: absolute;
|
||||
top: $block-spacing;
|
||||
right: $block-spacing;
|
||||
|
||||
.fa {
|
||||
font-size: 1.3em;
|
||||
vertical-align: middle;
|
||||
line-height: 1rem;
|
||||
background: $message-color;
|
||||
color: $message-bg;
|
||||
border-radius: $block-radius 0 0 $block-radius;
|
||||
padding: $elem-spacing;
|
||||
}
|
||||
|
||||
span {
|
||||
vertical-align: middle;
|
||||
padding: $elem-spacing;
|
||||
background: $message-bg;
|
||||
color: $message-color;
|
||||
border-radius: 0 $block-radius $block-radius 0;
|
||||
}
|
||||
}
|
||||
|
||||
.staticmap {
|
||||
width: 100%;
|
||||
border-radius: $block-radius;
|
||||
}
|
||||
}
|
||||
|
||||
&.post {
|
||||
.body {
|
||||
padding: 0em 1em 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
&.media {
|
||||
background: $media-bg;
|
||||
color: $media-color;
|
||||
|
||||
.body {
|
||||
a {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
color: $media-color;
|
||||
position: relative;
|
||||
line-height: 0;
|
||||
|
||||
&.drill {
|
||||
&:hover {
|
||||
.drill-icon .fa-drill-image, .drill-icon .fa-drill-video {
|
||||
color: rgba($media-bg, 0.75);
|
||||
}
|
||||
.comment {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.drill-icon {
|
||||
font-size: 3em;
|
||||
|
||||
.fa-drill-image {
|
||||
color: transparent;
|
||||
}
|
||||
.fa-drill-video {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
image-orientation: from-image;
|
||||
outline: none;
|
||||
border-radius: $block-radius;
|
||||
}
|
||||
|
||||
.comment {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
line-height: normal;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0.5em;
|
||||
text-align: justify;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border-radius: 0 0 $block-radius $block-radius;
|
||||
transition: opacity 0.3s;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
110
src/styles/_page.project.map.scss
Normal file
110
src/styles/_page.project.map.scss
Normal file
@@ -0,0 +1,110 @@
|
||||
#map {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
|
||||
/* Leaflet Popup */
|
||||
.maplibregl-popup-content {
|
||||
|
||||
h1 {
|
||||
font-size: 1.4em;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.separator {
|
||||
border-top: 1px solid #CCC;
|
||||
margin: $elem-spacing 0 $block-spacing 0;
|
||||
}
|
||||
|
||||
/* Marker Popup */
|
||||
.info-window {
|
||||
h1 .message-type {
|
||||
color: #CCC;
|
||||
font-weight: normal;
|
||||
font-size: calc(1em / 1.4);
|
||||
margin-left: 0.5em;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.0em;
|
||||
margin: $elem-spacing 0 0 0;
|
||||
|
||||
a {
|
||||
color: $post-color;
|
||||
}
|
||||
}
|
||||
|
||||
.medias {
|
||||
line-height: 0;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
|
||||
margin: $block-spacing $block-spacing 0 0;
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&.drill {
|
||||
font-size: 2em;
|
||||
|
||||
.fa-drill-image {
|
||||
color: transparent;
|
||||
}
|
||||
.fa-drill-video {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.fa-drill-video, .fa-drill-image {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 200px;
|
||||
max-height: 100px;
|
||||
border-radius: $block-radius;
|
||||
image-orientation: from-image;
|
||||
transition: All 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Track Popup */
|
||||
.track_tooltip {
|
||||
p {
|
||||
margin: 0;
|
||||
|
||||
&.description {
|
||||
font-size: 1.15em;
|
||||
}
|
||||
}
|
||||
|
||||
h1, .description {
|
||||
@include no-text-overflow();
|
||||
}
|
||||
.body {
|
||||
padding-left: calc(1.25em*1.4 + #{$elem-spacing} );
|
||||
|
||||
.details {
|
||||
margin-top: -$block-spacing;
|
||||
|
||||
p.detail {
|
||||
margin-top: $block-spacing;
|
||||
width: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
213
src/styles/_page.project.panel.scss
Normal file
213
src/styles/_page.project.panel.scss
Normal file
@@ -0,0 +1,213 @@
|
||||
$panel-width: 30vw;
|
||||
$panel-width-max: "400px + 3 * #{$block-spacing}";
|
||||
$panel-actual-width: min(#{$panel-width}, #{$panel-width-max});
|
||||
|
||||
#projects {
|
||||
&.with-feed, &.with-settings {
|
||||
#title {
|
||||
max-width: calc(100vw - #{$block-spacing} - #{$panel-actual-width} - (#{$button-width} + #{$block-spacing} * 2) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
&.with-feed {
|
||||
#submap {
|
||||
transform: translateX(calc(#{$panel-actual-width} / -2));
|
||||
}
|
||||
|
||||
.map-container-right {
|
||||
transform: translateX(calc(100vw - #{$panel-actual-width}));
|
||||
}
|
||||
}
|
||||
|
||||
&.with-settings {
|
||||
#submap {
|
||||
transform: translateX(calc(#{$panel-actual-width} / 2));
|
||||
}
|
||||
|
||||
.map-container-left {
|
||||
transform: translateX(0);
|
||||
|
||||
.map-panel {
|
||||
box-shadow: 2px 2px $block-shadow 0px rgba(0, 0, 0, .5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.with-feed.with-settings {
|
||||
#submap {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
#title {
|
||||
max-width: calc(100vw - #{$block-spacing} - #{$panel-actual-width} * 2 - (#{$button-width} + #{$block-spacing} * 2) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
.map-container { //#feed, #settings
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
user-select: none;
|
||||
width: #{$panel-width};
|
||||
max-width: calc(#{$panel-width-max});
|
||||
transition: transform 0.5s;
|
||||
|
||||
&.moving {
|
||||
cursor: grabbing;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.map-panel { //#feed-panel, #settings-panel
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
background-color: $post-input-bg;
|
||||
color: $post-color;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button, a.button {
|
||||
background-color: $post-color;
|
||||
color: $post-bg;
|
||||
|
||||
&:hover, &:hover a, &:hover a:visited {
|
||||
background-color: $post-input-bg;
|
||||
color: $post-color;
|
||||
}
|
||||
|
||||
a, a:visited {
|
||||
background-color: $post-color;
|
||||
color: $post-bg;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&+ button, &+ a.button {
|
||||
margin-left: $elem-spacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.map-container-left { //#settings
|
||||
transform: translateX(-100%);
|
||||
|
||||
.map-panel { //#settings-panel
|
||||
width: calc(100% - #{$block-spacing});
|
||||
margin: $block-spacing;
|
||||
border-radius: $block-radius;
|
||||
color: $post-color;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.map-container-right { //#feed
|
||||
transform: translateX(100vw);
|
||||
|
||||
.map-panel { //#feed-panel
|
||||
width: 100%;
|
||||
padding-top: $block-spacing;
|
||||
}
|
||||
}
|
||||
|
||||
.map-control {
|
||||
position: absolute;
|
||||
background-color: $post-bg;
|
||||
padding: $elem-spacing;
|
||||
border-radius: 3px;
|
||||
box-shadow: 2px 2px 3px 0px rgba(0, 0, 0, 0.5);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
|
||||
&.map-control-top {
|
||||
top: $block-spacing;
|
||||
}
|
||||
|
||||
&.map-control-bottom {
|
||||
bottom: $block-spacing;
|
||||
}
|
||||
|
||||
&.map-control-icon {
|
||||
cursor: pointer;
|
||||
|
||||
.fa {
|
||||
@extend .fa-fw;
|
||||
color: $post-color;
|
||||
}
|
||||
|
||||
&:hover .fa {
|
||||
color: #000000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.feed-control {
|
||||
right: calc(100% + $block-spacing);
|
||||
}
|
||||
|
||||
.settings-control {
|
||||
left: calc(100% + $block-spacing);
|
||||
}
|
||||
|
||||
#legend {
|
||||
.track {
|
||||
white-space: nowrap;
|
||||
.line {
|
||||
width: 2em;
|
||||
display: inline-block;
|
||||
border-radius: 2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 1em;
|
||||
margin-left: 0.5em;
|
||||
color: $legend-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#title {
|
||||
left: calc(100% + #{$button-width} + 2 * #{$block-spacing});
|
||||
max-width: calc(100vw - #{$block-spacing} - (#{$button-width} + 2 * #{$block-spacing}) * 2);
|
||||
transition: max-width 0.5s;
|
||||
@include no-text-overflow();
|
||||
|
||||
span {
|
||||
font-size: 1.3em;
|
||||
line-height: $block-spacing;
|
||||
}
|
||||
}
|
||||
|
||||
#background {
|
||||
background: #666;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#submap {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
transition: transform 0.5s;
|
||||
|
||||
.loader {
|
||||
position: absolute;
|
||||
font-size: 3em;
|
||||
top: calc(50% - 0.5em);
|
||||
left: calc(50% - 1.25em/2);
|
||||
color: #CCC;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,31 +3,31 @@ $elem-spacing: 0.5rem;
|
||||
$block-spacing: 1rem;
|
||||
$block-radius: 3px;
|
||||
$block-shadow: 3px;
|
||||
$panel-width: 30vw;
|
||||
$panel-width-max: "400px + 3 * #{$block-spacing}";
|
||||
$button-width: 44px;
|
||||
$button-width: 31px;
|
||||
|
||||
//Feed colors
|
||||
$post-input-bg: #ffffff; //#d9deff;
|
||||
$post-color: #333; //#323268;
|
||||
$post-input-bg: #ffffff;
|
||||
$post-color: #333;
|
||||
$post-color-hover: darken($post-color, 10%);
|
||||
$post-bg: rgba(255,255,255,.8); //#B4BDFF;
|
||||
$post-bg: rgba(255, 255, 255, .8);
|
||||
$message-color: #326526;
|
||||
$message-color-hover: darken($message-color, 10%);
|
||||
$message-bg: #6DFF58;
|
||||
$media-color: #333; //#635C28;
|
||||
$media-bg: rgba(255,255,255,.8); //#F3EC9F;
|
||||
$media-color: #333;
|
||||
$media-bg: rgba(255, 255, 255, .8);
|
||||
|
||||
//Settings colors
|
||||
$title-color: $post-color;
|
||||
$subtitle-color: #999;
|
||||
|
||||
//Legend colors
|
||||
$track-main-color: #00ff78;
|
||||
$track-off-track-color: #0000ff;
|
||||
$track-hitchhiking-color: #FF7814;
|
||||
$legend-color: $post-color;
|
||||
|
||||
@import 'page.project.map';
|
||||
@import 'page.project.panel';
|
||||
@import 'page.project.feed';
|
||||
@import 'page.project.settings';
|
||||
|
||||
#projects {
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
@@ -36,291 +36,6 @@ $legend-color: $post-color;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
/* Panels movements */
|
||||
&.with-feed {
|
||||
#submap {
|
||||
width: calc(100% - min(#{$panel-width}, #{$panel-width-max}));
|
||||
}
|
||||
|
||||
#feed {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.leaflet-right {
|
||||
right: min(#{$panel-width}, #{$panel-width-max});
|
||||
}
|
||||
|
||||
#feed-button {
|
||||
.fa {
|
||||
@extend .fa-next;
|
||||
}
|
||||
}
|
||||
|
||||
#title {
|
||||
max-width: calc(100vw - max(#{$panel-width}, #{$panel-width-max}) - (#{$button-width} + #{$block-spacing} * 2) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
&.with-settings {
|
||||
#submap {
|
||||
width: calc(100% - min(#{$panel-width}, #{$panel-width-max}));
|
||||
left: min(#{$panel-width}, #{$panel-width-max});
|
||||
}
|
||||
|
||||
#settings {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.leaflet-left {
|
||||
left: min(#{$panel-width}, #{$panel-width-max});
|
||||
}
|
||||
|
||||
#settings-button {
|
||||
.fa {
|
||||
@extend .fa-prev;
|
||||
}
|
||||
}
|
||||
|
||||
#title {
|
||||
max-width: calc(100vw - #{$block-spacing} * 2 - min(#{$panel-width}, #{$panel-width-max}) - (#{$button-width} + #{$block-spacing} * 2) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
&.with-feed.with-settings {
|
||||
#submap {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#title {
|
||||
max-width: calc(100vw - #{$block-spacing} * 2 - min(#{$panel-width}, #{$panel-width-max}) * 2 - (#{$button-width} + #{$block-spacing} * 2) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
#background {
|
||||
background: #666;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#submap {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
transition: width 0.5s, left 0.5s;
|
||||
|
||||
.loader {
|
||||
position: absolute;
|
||||
font-size: 3em;
|
||||
top: calc(50% - 0.5em);
|
||||
left: calc(50% - 1.25em/2);
|
||||
color: #CCC;
|
||||
}
|
||||
}
|
||||
|
||||
#map {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
|
||||
/* Leaflet Popup */
|
||||
.leaflet-popup-content {
|
||||
|
||||
h1 {
|
||||
font-size: 1.4em;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.separator {
|
||||
border-top: 1px solid #CCC;
|
||||
margin: $elem-spacing 0 $block-spacing 0;
|
||||
}
|
||||
|
||||
/* Marker Popup */
|
||||
.info-window {
|
||||
h1 .message-type {
|
||||
color: #CCC;
|
||||
font-weight: normal;
|
||||
font-size: calc(1em / 1.4);
|
||||
margin-left: 0.5em;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.0em;
|
||||
margin: $elem-spacing 0 0 0;
|
||||
|
||||
a {
|
||||
color: $post-color;
|
||||
}
|
||||
}
|
||||
|
||||
.medias {
|
||||
line-height: 0;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
|
||||
margin: $block-spacing $block-spacing 0 0;
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&.drill {
|
||||
font-size: 2em;
|
||||
|
||||
.fa-drill-image {
|
||||
color: transparent;
|
||||
}
|
||||
.fa-drill-video {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.fa-drill-video, .fa-drill-image {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 200px;
|
||||
max-height: 100px;
|
||||
border-radius: $block-radius;
|
||||
image-orientation: from-image;
|
||||
transition: All 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Track Popup */
|
||||
.track_tooltip {
|
||||
p {
|
||||
margin: 0;
|
||||
|
||||
&.description {
|
||||
font-size: 1.15em;
|
||||
}
|
||||
}
|
||||
|
||||
h1, .description {
|
||||
@include no-text-overflow();
|
||||
}
|
||||
.body {
|
||||
padding-left: calc(1.25em*1.4 + #{$elem-spacing} );
|
||||
|
||||
.details {
|
||||
margin-top: -$block-spacing;
|
||||
|
||||
p.detail {
|
||||
margin-top: $block-spacing;
|
||||
width: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Leaflet patches */
|
||||
.leaflet-control {
|
||||
background-color: rgba(255, 255, 255, 0.6);
|
||||
font-family: Roboto, Arial, sans-serif;
|
||||
border-radius: $block-radius;
|
||||
border: none;
|
||||
margin: $block-spacing;
|
||||
box-shadow: 0 1px 7px rgba(0, 0, 0, .4);
|
||||
|
||||
&+ .leaflet-control:not(.leaflet-control-inline) {
|
||||
margin-top: 0;
|
||||
}
|
||||
&+ .leaflet-control.leaflet-control-inline {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&.leaflet-control-scale {
|
||||
padding: 0.5em;
|
||||
|
||||
.leaflet-control-scale-line {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.leaflet-control-inline {
|
||||
clear: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Pull right/left controls by $panel-width */
|
||||
.leaflet-right, .leaflet-left {
|
||||
transition: left 0.5s, right 0.5s;
|
||||
}
|
||||
|
||||
/* Hide default layer control */
|
||||
.leaflet-top.leaflet-left .leaflet-control-layers .leaflet-control-layers-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#legend {
|
||||
.track {
|
||||
white-space: nowrap;
|
||||
.line {
|
||||
width: 2em;
|
||||
height: 4px;
|
||||
display: inline-block;
|
||||
border-radius: 2px;
|
||||
vertical-align: middle;
|
||||
|
||||
&.main {
|
||||
background-color: $track-main-color;
|
||||
}
|
||||
&.off-track {
|
||||
background-color: $track-off-track-color;
|
||||
}
|
||||
&.hitchhiking {
|
||||
background-color: $track-hitchhiking-color;
|
||||
}
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 1em;
|
||||
margin-left: 0.5em;
|
||||
color: $legend-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#title {
|
||||
@include no-text-overflow();
|
||||
line-height: $button-width;
|
||||
height: $button-width;
|
||||
padding: 0 $block-spacing;
|
||||
margin-bottom: 0;
|
||||
|
||||
span#project_name {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
}
|
||||
|
||||
#feed-button .fa {
|
||||
@extend .fa-post;
|
||||
}
|
||||
#settings-button .fa {
|
||||
@extend .fa-menu;
|
||||
}
|
||||
|
||||
/* Drill & Map icons */
|
||||
|
||||
a.drill {
|
||||
@@ -359,484 +74,7 @@ $legend-color: $post-color;
|
||||
top: 1px;
|
||||
}
|
||||
.fa-track-end {
|
||||
color: $track-hitchhiking-color;
|
||||
color: #FF7814;
|
||||
}
|
||||
}
|
||||
|
||||
/* Feed/Settings Panel */
|
||||
|
||||
#feed, #settings {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
z-index: 999;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
|
||||
&.moving {
|
||||
cursor: grabbing;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
background-color: $post-input-bg;
|
||||
color: $post-color;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button, a.button {
|
||||
background-color: $post-color;
|
||||
color: $post-bg;
|
||||
|
||||
&:hover, &:hover a, &:hover a:visited {
|
||||
background-color: $post-input-bg;
|
||||
color: $post-color;
|
||||
}
|
||||
|
||||
a, a:visited {
|
||||
background-color: $post-color;
|
||||
color: $post-bg;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&+ button, &+ a.button {
|
||||
margin-left: $elem-spacing;
|
||||
}
|
||||
}
|
||||
|
||||
#feed-panel, #settings-panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
#feed {
|
||||
right: calc(min(#{$panel-width}, #{$panel-width-max}) * -1);
|
||||
transition: right 0.5s;
|
||||
width: #{$panel-width};
|
||||
max-width: calc(#{$panel-width-max});
|
||||
|
||||
#feed-panel {
|
||||
width: 100%;
|
||||
padding-top: $block-spacing;
|
||||
|
||||
#posts_list {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#poster {
|
||||
&.histo-mode .poster, &:not(.histo-mode) .archived {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.poster {
|
||||
textarea#post {
|
||||
margin-bottom: 1em;
|
||||
width: calc(100% - 2em);
|
||||
}
|
||||
|
||||
input#name {
|
||||
width: calc(100% - 6em);
|
||||
}
|
||||
|
||||
button#submit {
|
||||
margin-left: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.archived {
|
||||
background: #EEE;
|
||||
}
|
||||
}
|
||||
|
||||
.body-box {
|
||||
position:relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.post-item {
|
||||
margin-bottom: $block-spacing;
|
||||
background: $post-bg;
|
||||
color: $post-color;
|
||||
border-radius: $block-radius;
|
||||
width: calc(100% - #{$block-spacing});
|
||||
box-shadow: 2px 2px 3px 0px rgba(0, 0, 0, 0.5);
|
||||
|
||||
a {
|
||||
color: $post-color;
|
||||
&:hover {
|
||||
color: $post-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
margin: 0;
|
||||
}
|
||||
.signature {
|
||||
margin: $elem-spacing 0 0 0;
|
||||
text-align: right;
|
||||
font-style: italic;
|
||||
|
||||
img {
|
||||
vertical-align: baseline;
|
||||
margin: 0 0.2em calc((1em - 24px)/2) 0;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
.header {
|
||||
padding: 0 $block-spacing;
|
||||
position: relative;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
font-size: 0.8em;
|
||||
padding: $elem-spacing 0px;
|
||||
|
||||
&.index {
|
||||
width: 25%;
|
||||
|
||||
.link, .link:visited, .link_copied {
|
||||
margin-left: $elem-spacing;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.time {
|
||||
width: 75%;
|
||||
text-align: right;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
.body {
|
||||
clear: both;
|
||||
padding: 0em $block-spacing $block-spacing;
|
||||
}
|
||||
|
||||
&.headerless {
|
||||
.header {
|
||||
display: none;
|
||||
}
|
||||
.body {
|
||||
padding-top: $block-spacing;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
|
||||
.fa {
|
||||
display: inline-block;
|
||||
font-size: 2em;
|
||||
margin: $elem-spacing 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.message {
|
||||
background: $message-bg;
|
||||
color: $message-color;
|
||||
|
||||
p {
|
||||
font-size: 0.9em;
|
||||
margin: 0 0 $elem-spacing 0;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $message-color;
|
||||
&:hover {
|
||||
color: $message-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
a.drill {
|
||||
line-height: 0;
|
||||
|
||||
.drill-icon {
|
||||
transform: translate(-16px, -32px);
|
||||
|
||||
.fa-message-in {
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.fa-message {
|
||||
@extend .#{$fa-css-prefix}-drill-message;
|
||||
top: 13px;
|
||||
left: 3px;
|
||||
}
|
||||
.fa-message-in {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.weather {
|
||||
position: absolute;
|
||||
top: $block-spacing;
|
||||
right: $block-spacing;
|
||||
|
||||
.fa {
|
||||
font-size: 1.3em;
|
||||
vertical-align: middle;
|
||||
line-height: 1rem;
|
||||
background: $message-color;
|
||||
color: $message-bg;
|
||||
border-radius: $block-radius 0 0 $block-radius;
|
||||
padding: $elem-spacing;
|
||||
}
|
||||
|
||||
span {
|
||||
vertical-align: middle;
|
||||
padding: $elem-spacing;
|
||||
background: $message-bg;
|
||||
color: $message-color;
|
||||
border-radius: 0 $block-radius $block-radius 0;
|
||||
}
|
||||
}
|
||||
|
||||
.staticmap {
|
||||
width: 100%;
|
||||
border-radius: $block-radius;
|
||||
}
|
||||
}
|
||||
|
||||
&.post {
|
||||
.body {
|
||||
padding: 0em 1em 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
&.media {
|
||||
background: $media-bg;
|
||||
color: $media-color;
|
||||
|
||||
.body {
|
||||
a {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
color: $media-color;
|
||||
position: relative;
|
||||
line-height: 0;
|
||||
|
||||
&.drill {
|
||||
&:hover {
|
||||
.drill-icon .fa-drill-image, .drill-icon .fa-drill-video {
|
||||
color: rgba($media-bg, 0.75);
|
||||
}
|
||||
.comment {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.drill-icon {
|
||||
font-size: 3em;
|
||||
|
||||
.fa-drill-image {
|
||||
color: transparent;
|
||||
}
|
||||
.fa-drill-video {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
image-orientation: from-image;
|
||||
outline: none;
|
||||
border-radius: $block-radius;
|
||||
}
|
||||
|
||||
.comment {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
line-height: normal;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0.5em;
|
||||
text-align: justify;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border-radius: 0 0 $block-radius $block-radius;
|
||||
transition: opacity 0.3s;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#settings {
|
||||
left: calc(min(#{$panel-width} + #{$block-shadow}, #{$panel-width-max} + #{$block-shadow}) * -1);
|
||||
transition: left 0.5s;
|
||||
width: calc(#{$panel-width} + #{$block-shadow}); //Add box-shadow
|
||||
max-width: calc(#{$panel-width-max} + #{$block-shadow}); //Add box-shadow
|
||||
|
||||
#settings-panel {
|
||||
width: calc(100% - #{$block-spacing} - #{$block-shadow}); //Remove box-shadow
|
||||
margin: $block-spacing;
|
||||
border-radius: $block-radius;
|
||||
box-shadow: 2px 2px $block-shadow 0px rgba(0, 0, 0, .5);
|
||||
color: $post-color;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
.settings-header {
|
||||
text-align: center;
|
||||
flex: 0 1 auto;
|
||||
|
||||
.logo {
|
||||
background: rgba(255, 255, 255, .4);
|
||||
padding: 2rem 1rem;
|
||||
border-radius: $block-radius $block-radius 0 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 180px;
|
||||
transform: translateX(-10%); //Center Text, not logo. logo width (40px) / image width (200px) = 20%. And centering: 20% / 2 = 10%
|
||||
}
|
||||
}
|
||||
|
||||
#last_update {
|
||||
position: absolute;
|
||||
margin-top: -2em;
|
||||
padding: 0 1rem;
|
||||
width: calc(100% - 2rem);
|
||||
|
||||
p {
|
||||
text-align: center;
|
||||
font-size: 0.8em;
|
||||
margin: 0;
|
||||
color: $subtitle-color;
|
||||
transform: translateX(calc(-0.5 * (12px + 0.5em))); //icon width + margin right
|
||||
|
||||
span {
|
||||
margin-right: 0.5em;
|
||||
img {
|
||||
width: 12px;
|
||||
vertical-align: middle;
|
||||
animation: spotlogo 20s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
abbr {
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-footer {
|
||||
flex: 0 1 auto;
|
||||
background: rgba(255, 255, 255, .4);
|
||||
border-radius: 0 0 3px 3px;
|
||||
font-size: 0.7em;
|
||||
padding: 0.3rem;
|
||||
text-align: center;
|
||||
color: #888;
|
||||
|
||||
a {
|
||||
color: #777;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-sections {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
|
||||
#settings-sections-scrollbox {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
display: inline-block;
|
||||
margin: 1.5rem 1rem 0 1rem;
|
||||
width: calc(100% - 2 * #{$block-spacing});
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 $block-spacing;
|
||||
color: $title-color;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: .3em;
|
||||
display: block;
|
||||
@extend .clickable;
|
||||
|
||||
& > div {
|
||||
@include no-text-overflow();
|
||||
}
|
||||
}
|
||||
|
||||
&.newsletter {
|
||||
input#email {
|
||||
width: calc(100% - 6em);
|
||||
|
||||
&:disabled {
|
||||
color: #999;
|
||||
background: rgba(255,255,255,0.2);
|
||||
}
|
||||
}
|
||||
button#nl_btn {
|
||||
margin-left: 1em;
|
||||
margin-bottom: 1em;
|
||||
|
||||
&.subscribe .fa {
|
||||
@extend .fa-send;
|
||||
}
|
||||
&.unsubscribe .fa {
|
||||
@extend .fa-unsubscribe;
|
||||
}
|
||||
&.loading {
|
||||
background-color: $message-color;
|
||||
color: white;
|
||||
span {
|
||||
@extend .flicker;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#settings-projects {
|
||||
a.fa-download {
|
||||
color: $legend-color;
|
||||
|
||||
&:hover {
|
||||
color: #0078A8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#elems {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
142
src/styles/_page.project.settings.scss
Normal file
142
src/styles/_page.project.settings.scss
Normal file
@@ -0,0 +1,142 @@
|
||||
#settings {
|
||||
#settings-panel {
|
||||
.settings-header {
|
||||
text-align: center;
|
||||
flex: 0 1 auto;
|
||||
|
||||
.logo {
|
||||
background: rgba(255, 255, 255, .4);
|
||||
padding: 2rem 1rem;
|
||||
border-radius: $block-radius $block-radius 0 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 180px;
|
||||
transform: translateX(-10%); //Center Text, not logo. logo width (40px) / image width (200px) = 20%. And centering: 20% / 2 = 10%
|
||||
}
|
||||
}
|
||||
|
||||
#last_update {
|
||||
position: absolute;
|
||||
margin-top: -2em;
|
||||
padding: 0 1rem;
|
||||
width: calc(100% - 2rem);
|
||||
|
||||
p {
|
||||
text-align: center;
|
||||
font-size: 0.8em;
|
||||
margin: 0;
|
||||
color: $subtitle-color;
|
||||
transform: translateX(calc(-0.5 * (12px + 0.5em))); //icon width + margin right
|
||||
|
||||
span {
|
||||
margin-right: 0.5em;
|
||||
img {
|
||||
width: 12px;
|
||||
vertical-align: middle;
|
||||
animation: spotlogo 20s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
abbr {
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-footer {
|
||||
flex: 0 1 auto;
|
||||
background: rgba(255, 255, 255, .4);
|
||||
border-radius: 0 0 3px 3px;
|
||||
font-size: 0.7em;
|
||||
padding: 0.3rem;
|
||||
text-align: center;
|
||||
color: #888;
|
||||
|
||||
a {
|
||||
color: #777;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-sections {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
|
||||
#settings-sections-scrollbox {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
display: inline-block;
|
||||
margin: 1.5rem 1rem 0 1rem;
|
||||
width: calc(100% - 2 * #{$block-spacing});
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 $block-spacing;
|
||||
color: $title-color;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.settings-section-body {
|
||||
.radio {
|
||||
&:not(:first-child) {
|
||||
margin-top: $elem-spacing;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-left: .3rem;
|
||||
@extend .clickable;
|
||||
@include no-text-overflow();
|
||||
}
|
||||
|
||||
.download {
|
||||
color: $legend-color;
|
||||
|
||||
&:hover {
|
||||
color: #0078A8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.newsletter {
|
||||
input#email {
|
||||
width: calc(100% - 6em);
|
||||
|
||||
&:disabled {
|
||||
color: #999;
|
||||
background: rgba(255,255,255,0.2);
|
||||
}
|
||||
}
|
||||
button#nl_btn {
|
||||
margin-left: 1em;
|
||||
margin-bottom: 1em;
|
||||
|
||||
&.subscribe .fa {
|
||||
@extend .fa-send;
|
||||
}
|
||||
&.unsubscribe .fa {
|
||||
@extend .fa-unsubscribe;
|
||||
}
|
||||
&.loading {
|
||||
background-color: $message-color;
|
||||
color: white;
|
||||
span {
|
||||
@extend .flicker;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,53 @@
|
||||
#upload {
|
||||
padding: 1em;
|
||||
|
||||
.bar {
|
||||
height: 18px;
|
||||
background: green;
|
||||
|
||||
.section {
|
||||
border-radius: 3px;
|
||||
margin-top: 1rem;
|
||||
padding: 0 1rem 1rem 1rem;
|
||||
border-bottom: 1px solid #EEE;
|
||||
}
|
||||
|
||||
.progress {
|
||||
.bar {
|
||||
height: 18px;
|
||||
background: green;
|
||||
}
|
||||
}
|
||||
|
||||
.comment {
|
||||
margin-top: 1em;
|
||||
|
||||
.thumb {
|
||||
width: 30%;
|
||||
max-width: 100px;
|
||||
}
|
||||
form {
|
||||
|
||||
.form {
|
||||
display: inline-block;
|
||||
width: calc(70% - 1em);
|
||||
min-width: calc(100% - 100px - 1em);
|
||||
margin-left: 1em;
|
||||
width: calc(70% - 2rem);
|
||||
min-width: calc(100% - 100px - 2rem);
|
||||
padding: 1rem;
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
width: calc(100% - 2rem);
|
||||
box-sizing: border-box;
|
||||
padding: 0.5em;
|
||||
border: 1px solid #333;
|
||||
background: #EEE;
|
||||
}
|
||||
|
||||
.save {
|
||||
margin-top: 1em;
|
||||
margin-top: 1rem;
|
||||
padding: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logs {
|
||||
padding: 1rem;
|
||||
|
||||
p.log {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/styles/_vue.scss
Normal file
9
src/styles/_vue.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -13,10 +13,13 @@
|
||||
.#{$fa-css-prefix}-solid,
|
||||
.far,
|
||||
.#{$fa-css-prefix}-regular,
|
||||
.fasr,
|
||||
.fal,
|
||||
.#{$fa-css-prefix}-light,
|
||||
.fasl,
|
||||
.fat,
|
||||
.#{$fa-css-prefix}-thin,
|
||||
.fast,
|
||||
.fad,
|
||||
.#{$fa-css-prefix}-duotone,
|
||||
.fass,
|
||||
@@ -56,8 +59,14 @@
|
||||
}
|
||||
|
||||
.fass,
|
||||
.fasr,
|
||||
.fasl,
|
||||
.fast,
|
||||
.#{$fa-css-prefix}-sharp {
|
||||
font-family: 'Font Awesome 6 Sharp';
|
||||
}
|
||||
.fass,
|
||||
.#{$fa-css-prefix}-sharp {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
|
||||
/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen readers do not read off random characters that represent icons */
|
||||
|
||||
|
||||
@each $name, $icon in $fa-icons {
|
||||
.fad.#{$fa-css-prefix}-#{$name}::after, .#{$fa-css-prefix}-duotone.#{$fa-css-prefix}-#{$name}::after {
|
||||
content: unquote("\"#{ $icon }#{ $icon }\"");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,3 +7,4 @@ readers do not read off random characters that represent icons */
|
||||
@each $name, $icon in $fa-icons {
|
||||
.#{$fa-css-prefix}-#{$name}::before { content: unquote("\"#{ $icon }\""); }
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +110,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
@mixin fa-icon-sharp-regular($fa-var) {
|
||||
@extend %fa-icon;
|
||||
@extend .fa-sharp-regular;
|
||||
|
||||
&::before {
|
||||
content: unquote("\"#{ $fa-var }\"");
|
||||
}
|
||||
}
|
||||
|
||||
@mixin fa-icon-sharp-light($fa-var) {
|
||||
@extend %fa-icon;
|
||||
@extend .fa-sharp-light;
|
||||
|
||||
&::before {
|
||||
content: unquote("\"#{ $fa-var }\"");
|
||||
}
|
||||
}
|
||||
|
||||
@mixin fa-icon-sharp-thin($fa-var) {
|
||||
@extend %fa-icon;
|
||||
@extend .fa-sharp-thin;
|
||||
|
||||
&::before {
|
||||
content: unquote("\"#{ $fa-var }\"");
|
||||
}
|
||||
}
|
||||
|
||||
@mixin fa-icon-brands($fa-var) {
|
||||
@extend %fa-icon;
|
||||
@extend .fa-brands;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
/*!
|
||||
* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com
|
||||
* Font Awesome Pro 6.5.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license (Commercial License)
|
||||
* Copyright 2022 Fonticons, Inc.
|
||||
* Copyright 2023 Fonticons, Inc.
|
||||
*/
|
||||
@import 'functions';
|
||||
@import 'variables';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*!
|
||||
* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com
|
||||
* Font Awesome Pro 6.5.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license (Commercial License)
|
||||
* Copyright 2022 Fonticons, Inc.
|
||||
* Copyright 2023 Fonticons, Inc.
|
||||
*/
|
||||
@import 'functions';
|
||||
@import 'variables';
|
||||
|
||||
4
src/styles/fa/fontawesome.scss
vendored
4
src/styles/fa/fontawesome.scss
vendored
@@ -1,7 +1,7 @@
|
||||
/*!
|
||||
* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com
|
||||
* Font Awesome Pro 6.5.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license (Commercial License)
|
||||
* Copyright 2022 Fonticons, Inc.
|
||||
* Copyright 2023 Fonticons, Inc.
|
||||
*/
|
||||
// Font Awesome core compile (Web Fonts-based)
|
||||
// -------------------------
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
src/styles/fa/fonts/fa-sharp-light-300.ttf
Normal file
BIN
src/styles/fa/fonts/fa-sharp-light-300.ttf
Normal file
Binary file not shown.
BIN
src/styles/fa/fonts/fa-sharp-light-300.woff2
Normal file
BIN
src/styles/fa/fonts/fa-sharp-light-300.woff2
Normal file
Binary file not shown.
BIN
src/styles/fa/fonts/fa-sharp-regular-400.ttf
Normal file
BIN
src/styles/fa/fonts/fa-sharp-regular-400.ttf
Normal file
Binary file not shown.
BIN
src/styles/fa/fonts/fa-sharp-regular-400.woff2
Normal file
BIN
src/styles/fa/fonts/fa-sharp-regular-400.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
src/styles/fa/fonts/fa-sharp-thin-100.ttf
Normal file
BIN
src/styles/fa/fonts/fa-sharp-thin-100.ttf
Normal file
Binary file not shown.
BIN
src/styles/fa/fonts/fa-sharp-thin-100.woff2
Normal file
BIN
src/styles/fa/fonts/fa-sharp-thin-100.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
/*!
|
||||
* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com
|
||||
* Font Awesome Pro 6.5.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license (Commercial License)
|
||||
* Copyright 2022 Fonticons, Inc.
|
||||
* Copyright 2023 Fonticons, Inc.
|
||||
*/
|
||||
@import 'functions';
|
||||
@import 'variables';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*!
|
||||
* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com
|
||||
* Font Awesome Pro 6.5.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license (Commercial License)
|
||||
* Copyright 2022 Fonticons, Inc.
|
||||
* Copyright 2023 Fonticons, Inc.
|
||||
*/
|
||||
@import 'functions';
|
||||
@import 'variables';
|
||||
|
||||
26
src/styles/fa/sharp-light.scss
Normal file
26
src/styles/fa/sharp-light.scss
Normal file
@@ -0,0 +1,26 @@
|
||||
/*!
|
||||
* Font Awesome Pro 6.5.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license (Commercial License)
|
||||
* Copyright 2023 Fonticons, Inc.
|
||||
*/
|
||||
@import 'functions';
|
||||
@import 'variables';
|
||||
|
||||
:root, :host {
|
||||
--#{$fa-css-prefix}-style-family-sharp: 'Font Awesome 6 Sharp';
|
||||
--#{$fa-css-prefix}-font-sharp-light: normal 300 1em/1 'Font Awesome 6 Sharp';
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 6 Sharp';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: $fa-font-display;
|
||||
src: url('#{$fa-font-path}/fa-sharp-light-300.woff2') format('woff2'),
|
||||
url('#{$fa-font-path}/fa-sharp-light-300.ttf') format('truetype');
|
||||
}
|
||||
|
||||
.fasl,
|
||||
.#{$fa-css-prefix}-light {
|
||||
font-weight: 300;
|
||||
}
|
||||
26
src/styles/fa/sharp-regular.scss
Normal file
26
src/styles/fa/sharp-regular.scss
Normal file
@@ -0,0 +1,26 @@
|
||||
/*!
|
||||
* Font Awesome Pro 6.5.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license (Commercial License)
|
||||
* Copyright 2023 Fonticons, Inc.
|
||||
*/
|
||||
@import 'functions';
|
||||
@import 'variables';
|
||||
|
||||
:root, :host {
|
||||
--#{$fa-css-prefix}-style-family-sharp: 'Font Awesome 6 Sharp';
|
||||
--#{$fa-css-prefix}-font-sharp-regular: normal 400 1em/1 'Font Awesome 6 Sharp';
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 6 Sharp';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: $fa-font-display;
|
||||
src: url('#{$fa-font-path}/fa-sharp-regular-400.woff2') format('woff2'),
|
||||
url('#{$fa-font-path}/fa-sharp-regular-400.ttf') format('truetype');
|
||||
}
|
||||
|
||||
.fasr,
|
||||
.#{$fa-css-prefix}-regular {
|
||||
font-weight: 400;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/*!
|
||||
* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com
|
||||
* Font Awesome Pro 6.5.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license (Commercial License)
|
||||
* Copyright 2022 Fonticons, Inc.
|
||||
* Copyright 2023 Fonticons, Inc.
|
||||
*/
|
||||
@import 'functions';
|
||||
@import 'variables';
|
||||
@@ -21,6 +21,6 @@
|
||||
}
|
||||
|
||||
.fass,
|
||||
.#{$fa-css-prefix}-sharp-solid {
|
||||
.#{$fa-css-prefix}-solid {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
26
src/styles/fa/sharp-thin.scss
Normal file
26
src/styles/fa/sharp-thin.scss
Normal file
@@ -0,0 +1,26 @@
|
||||
/*!
|
||||
* Font Awesome Pro 6.5.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license (Commercial License)
|
||||
* Copyright 2023 Fonticons, Inc.
|
||||
*/
|
||||
@import 'functions';
|
||||
@import 'variables';
|
||||
|
||||
:root, :host {
|
||||
--#{$fa-css-prefix}-style-family-sharp: 'Font Awesome 6 Sharp';
|
||||
--#{$fa-css-prefix}-font-sharp-thin: normal 100 1em/1 'Font Awesome 6 Sharp';
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 6 Sharp';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: $fa-font-display;
|
||||
src: url('#{$fa-font-path}/fa-sharp-thin-100.woff2') format('woff2'),
|
||||
url('#{$fa-font-path}/fa-sharp-thin-100.ttf') format('truetype');
|
||||
}
|
||||
|
||||
.fast,
|
||||
.#{$fa-css-prefix}-thin {
|
||||
font-weight: 100;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/*!
|
||||
* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com
|
||||
* Font Awesome Pro 6.5.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license (Commercial License)
|
||||
* Copyright 2022 Fonticons, Inc.
|
||||
* Copyright 2023 Fonticons, Inc.
|
||||
*/
|
||||
@import 'functions';
|
||||
@import 'variables';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*!
|
||||
* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com
|
||||
* Font Awesome Pro 6.5.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license (Commercial License)
|
||||
* Copyright 2022 Fonticons, Inc.
|
||||
* Copyright 2023 Fonticons, Inc.
|
||||
*/
|
||||
@import 'functions';
|
||||
@import 'variables';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*!
|
||||
* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com
|
||||
* Font Awesome Pro 6.5.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license (Commercial License)
|
||||
* Copyright 2022 Fonticons, Inc.
|
||||
* Copyright 2023 Fonticons, Inc.
|
||||
*/
|
||||
// V4 shims compile (Web Fonts-based)
|
||||
// -------------------------
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
/* Modules */
|
||||
@import 'fa';
|
||||
@import 'lightbox';
|
||||
@import 'leaflet';
|
||||
@import '../../node_modules/simplebar/dist/simplebar.css';
|
||||
@import '../../node_modules/simplebar-vue/dist/simplebar.min.css';
|
||||
@import 'vue';
|
||||
|
||||
/* Pages Specific CSS */
|
||||
@import 'page.project';
|
||||
|
||||
Reference in New Issue
Block a user