mirror of
https://github.com/owntracks/frontend.git
synced 2026-02-13 20:59:50 +00:00
Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8d1d0dea3 | ||
|
|
22a605e9a9 | ||
|
|
773c217919 | ||
|
|
7877aaa9f8 | ||
|
|
341ce4c353 | ||
|
|
5ecad2bf40 | ||
|
|
3365959ed3 | ||
|
|
15a40f9c6c | ||
|
|
9839b5acdd | ||
|
|
0300e2fb4f | ||
|
|
4c680590a4 | ||
|
|
551b226fd0 | ||
|
|
132f15c52b | ||
|
|
c60bfb5368 | ||
|
|
31bf39795c | ||
|
|
d5c87a8727 | ||
|
|
0492b355bf | ||
|
|
998a97131b | ||
|
|
a44965226c | ||
|
|
554ce3f585 | ||
|
|
05ae629217 | ||
|
|
b141444b56 | ||
|
|
723ce684ae | ||
|
|
ed3e6125e9 | ||
|
|
8df1f86ab9 | ||
|
|
b5442363d6 | ||
|
|
35d55b57b1 | ||
|
|
976bb403d1 | ||
|
|
cecf7e797d | ||
|
|
91d99cd8da | ||
|
|
06faa73b70 | ||
|
|
7398da74c5 | ||
|
|
7b954dfbe3 | ||
|
|
c569aced1e | ||
|
|
3fad44509e | ||
|
|
aa13ddd832 | ||
|
|
6a2b113fb2 | ||
|
|
6f047ffa77 | ||
|
|
d5d6c1c268 | ||
|
|
4e86d8fac3 | ||
|
|
1cb6e3519e | ||
|
|
f5389b84ab | ||
|
|
9bb2edb78d | ||
|
|
6361d8f415 | ||
|
|
791b756d80 | ||
|
|
f1ef82d7bb | ||
|
|
aaef181141 | ||
|
|
b2273c071b | ||
|
|
865c89b43c | ||
|
|
32c64d18f5 | ||
|
|
5a64c06af0 | ||
|
|
8c3681b6ad | ||
|
|
89899de565 | ||
|
|
a386c15de1 | ||
|
|
8ac24c99aa | ||
|
|
f3cbf877f9 | ||
|
|
f5c1c82010 | ||
|
|
3ea1d02c65 | ||
|
|
f91341b205 | ||
|
|
0c983d6206 | ||
|
|
9e36d31997 | ||
|
|
5e37c7f4b8 | ||
|
|
7dda60d457 | ||
|
|
228900ff9f | ||
|
|
129446de1a | ||
|
|
af6c308bd6 | ||
|
|
223e19a118 | ||
|
|
1260814309 | ||
|
|
cfffbe9472 | ||
|
|
4031bda2f0 | ||
|
|
69094e240e | ||
|
|
dfa7a423fa | ||
|
|
411bc10b0b | ||
|
|
a994051940 | ||
|
|
d325543bc6 | ||
|
|
80d3060fa8 | ||
|
|
e6c79ac606 | ||
|
|
0b1271502f | ||
|
|
fdddd8e035 | ||
|
|
245c1295e5 | ||
|
|
9786487646 | ||
|
|
b3529c211c | ||
|
|
55178c7cc8 | ||
|
|
2fcf2151fa |
36
.eslintrc.js
36
.eslintrc.js
@@ -1,36 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
extends: ["plugin:vue/essential", "@vue/prettier"],
|
||||
rules: {
|
||||
"no-console": process.env.NODE_ENV === "production" ? "error" : "warn",
|
||||
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn",
|
||||
"max-len": [
|
||||
"error",
|
||||
{
|
||||
ignoreUrls: true,
|
||||
},
|
||||
],
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
trailingComma: "es5",
|
||||
printWidth: 80,
|
||||
htmlWhitespaceSensitivity: "ignore",
|
||||
},
|
||||
],
|
||||
},
|
||||
parserOptions: {
|
||||
parser: "babel-eslint",
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ["**/__tests__/*.{j,t}s?(x)"],
|
||||
env: {
|
||||
jest: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
13
.github/workflows/build.yml
vendored
13
.github/workflows/build.yml
vendored
@@ -6,12 +6,9 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install dependencies
|
||||
uses: borales/actions-yarn@v2.0.0
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cmd: install
|
||||
- name: Run production build
|
||||
uses: borales/actions-yarn@v2.0.0
|
||||
with:
|
||||
cmd: build
|
||||
node-version: '20'
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
|
||||
8
.github/workflows/docker.yml
vendored
8
.github/workflows/docker.yml
vendored
@@ -4,14 +4,14 @@ on:
|
||||
schedule:
|
||||
- cron: '0 3 * * *' # everyday at 3am
|
||||
pull_request:
|
||||
branches: master
|
||||
branches: main
|
||||
push:
|
||||
branches: master
|
||||
branches: main
|
||||
tags:
|
||||
- v*
|
||||
release:
|
||||
types: [published]
|
||||
branches: master
|
||||
branches: main
|
||||
tags:
|
||||
- v*
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
run: |
|
||||
DOCKER_IMAGE=owntracks/frontend
|
||||
DOCKER_PLATFORMS=linux/amd64,linux/arm/v7,linux/arm64
|
||||
VERSION=master
|
||||
VERSION=main
|
||||
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
VERSION=${GITHUB_REF#refs/tags/v}
|
||||
|
||||
19
.github/workflows/lint.yml
vendored
19
.github/workflows/lint.yml
vendored
@@ -6,16 +6,11 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install dependencies
|
||||
uses: borales/actions-yarn@v2.0.0
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cmd: install
|
||||
- name: Lint JavaScript/Vue files
|
||||
uses: borales/actions-yarn@v2.0.0
|
||||
with:
|
||||
cmd: lint:js
|
||||
- name: Lint Markdown files
|
||||
uses: borales/actions-yarn@v2.0.0
|
||||
with:
|
||||
cmd: lint:md
|
||||
node-version: '20'
|
||||
- run: npm install
|
||||
- run: npm run lint:js
|
||||
- run: npm run lint:md
|
||||
- run: npm run lint:scss
|
||||
|
||||
13
.github/workflows/test.yml
vendored
13
.github/workflows/test.yml
vendored
@@ -6,12 +6,9 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install dependencies
|
||||
uses: borales/actions-yarn@v2.0.0
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cmd: install
|
||||
- name: Run unit tests
|
||||
uses: borales/actions-yarn@v2.0.0
|
||||
with:
|
||||
cmd: test
|
||||
node-version: '20'
|
||||
- run: npm install
|
||||
- run: npm test
|
||||
|
||||
20
.github/workflows/upload-dist.yml
vendored
Normal file
20
.github/workflows/upload-dist.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: Upload dist/
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
upload-dist:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- run: zip -r $GITHUB_REF_NAME-dist.zip dist/
|
||||
- run: gh release upload $GITHUB_REF_NAME $GITHUB_REF_NAME-dist.zip
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
74
CHANGELOG.md
74
CHANGELOG.md
@@ -2,6 +2,80 @@
|
||||
|
||||
Dates are in UTC.
|
||||
|
||||
## 2.15.3 (2024-06-15)
|
||||
|
||||
- Force relative path for `config/config.js` even if it doesn't exist at build time
|
||||
|
||||
## 2.15.2 (2024-06-14)
|
||||
|
||||
- Fix npm lockfile
|
||||
|
||||
## 2.15.1 (2024-06-14)
|
||||
|
||||
- Update `index.html` to emit relative paths again, allowing deployment under a subpath
|
||||
- Update Docker image to use nginx 1.27
|
||||
- Upgrade dependencies
|
||||
|
||||
## 2.15.0 (2024-06-10)
|
||||
|
||||
- Implement POI map layer (see [Booklet](https://owntracks.org/booklet/features/poi/))
|
||||
|
||||
- Use the `map.poiMarker` config option to tweak the appearance, defaults to a red circle slightly larger than the default location points
|
||||
- Use `map.layers.poi` to change the layer visibility, defaults to `true`
|
||||
|
||||
## 2.14.0 (2024-06-09)
|
||||
|
||||
- Implement new date/time range picker ([#116](https://github.com/owntracks/frontend/pull/116), [@jduar](https://github.com/jduar) / [@Tofee](https://github.com/Tofee))
|
||||
|
||||
## 2.13.1 (2024-06-09)
|
||||
|
||||
- Bump versions, just to make sure the frontend shows the right one
|
||||
|
||||
## 2.13.0 (2024-06-09)
|
||||
|
||||
- Enable use of the frontend as a progressive web app (PWA) ([#98](https://github.com/owntracks/frontend/pull/98), [@RobinMeis](https://github.com/RobinMeis))
|
||||
- Add Turkish translations ([#94](https://github.com/owntracks/frontend/pull/94), [@ramazansancar](https://github.com/ramazansancar))
|
||||
- Add Slovak translations ([#110](https://github.com/owntracks/frontend/pull/110), [@aasami](https://github.com/aasami))
|
||||
- Add Czech translations ([#115](https://github.com/owntracks/frontend/pull/115), [@jmencak](https://github.com/jmencak))
|
||||
- Add action for uploading dist/ on release ([#114](https://github.com/owntracks/frontend/pull/114), [@abaumg](https://github.com/abaumg))
|
||||
- Replace outdated Twitter link with Mastodon
|
||||
- Remove the download modal
|
||||
- Show isolocal and tzname properties on the popup
|
||||
- Various changes to the underlying frontend build system:
|
||||
- Bump node to version 20
|
||||
- Switch from yarn to npm
|
||||
- Migrate from vue-cli / webpack to vite
|
||||
- Upgrade dependencies
|
||||
|
||||
## 2.12.0 (2022-09-06)
|
||||
|
||||
- Add Danish translation ([#87](https://github.com/owntracks/frontend/pull/87), [@atjn](https://github.com/atjn))
|
||||
- Ensure correct display of larger (192x192) face images ([#83](https://github.com/owntracks/frontend/pull/83), [@atjn](https://github.com/atjn))
|
||||
- Add `map.tileSize` and `map.zoomOffset` options ([#75](https://github.com/owntracks/frontend/pull/75), [@saesh](https://github.com/saesh))
|
||||
- Upgrade dependencies
|
||||
|
||||
## 2.11.0 (2022-03-16)
|
||||
|
||||
- Show WiFi SSID and BSSID in location popup, if available
|
||||
- Show address in location popup, if available ([#73](https://github.com/owntracks/frontend/pull/73), [@saesh](https://github.com/saesh))
|
||||
- Upgrade dependencies
|
||||
|
||||
## 2.10.0 (2021-11-28)
|
||||
|
||||
- Ensure location history line segments are drawn in chronological order ([#67](https://github.com/owntracks/frontend/issues/67))
|
||||
- Add trailing slashes to paths used by Docker nginx config ([#63](https://github.com/owntracks/frontend/pull/63), [@growse](https://github.com/growse))
|
||||
- Upgrade dependencies
|
||||
|
||||
## 2.9.0 (2021-05-01)
|
||||
|
||||
- Add a cancel button to the loading data modal
|
||||
- Replace remaining uses of "OwnTracks UI" with "OwnTracks Frontend"
|
||||
- Upgrade dependencies
|
||||
|
||||
## 2.8.0 (2021-02-19)
|
||||
|
||||
- Add elevation gain / loss to "distance travelled" calculation ([#51](https://github.com/owntracks/frontend/issues/51))
|
||||
|
||||
## 2.7.0 (2021-02-14)
|
||||
|
||||
- Rename translation files from `xx` to `xx-XX` format to allow different language variants
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019-2020 Linus Groh
|
||||
Copyright (c) 2019-2024 Linus Groh
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
43
README.md
43
README.md
@@ -1,14 +1,14 @@
|
||||
# OwnTracks UI
|
||||
# OwnTracks Frontend
|
||||
|
||||

|
||||
[](https://hub.docker.com/r/owntracks/frontend)
|
||||
[](https://github.com/owntracks/frontend/actions?query=workflow%3ABuild+branch%3Amaster)
|
||||
[](https://github.com/owntracks/frontend/actions?query=workflow%3ATests+branch%3Amaster)
|
||||
[](https://github.com/owntracks/frontend/actions?query=workflow%3ALint+branch%3Amaster)
|
||||
[](https://github.com/owntracks/frontend/actions?query=workflow%3ABuild+branch%3Amain)
|
||||
[](https://github.com/owntracks/frontend/actions?query=workflow%3ATests+branch%3Amain)
|
||||
[](https://github.com/owntracks/frontend/actions?query=workflow%3ALint+branch%3Amain)
|
||||
[](https://github.com/prettier/prettier)
|
||||
[](https://github.com/owntracks/frontend/blob/master/LICENSE)
|
||||
[](https://github.com/owntracks/frontend/blob/main/LICENSE)
|
||||
|
||||

|
||||

|
||||
|
||||
## Introduction
|
||||
|
||||
@@ -16,7 +16,7 @@ This is a web interface for [OwnTracks](https://github.com/owntracks/recorder) b
|
||||
a Vue.js single page application. The recorder itself already ships with some basic web
|
||||
pages, this is a more advanced interface with more functionality, all in one place.
|
||||
|
||||

|
||||

|
||||
|
||||
## Features
|
||||
|
||||
@@ -31,7 +31,6 @@ pages, this is a more advanced interface with more functionality, all in one pla
|
||||
- Display data in a specific date and time range
|
||||
- Filter by user or specific device
|
||||
- Calculation of distance travelled
|
||||
- Download selected location data as JSON
|
||||
- Highly customisable
|
||||
|
||||
## Installation
|
||||
@@ -53,7 +52,7 @@ compose config, and the service is named `otrecorder`):
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
owntracks-ui:
|
||||
owntracks-frontend:
|
||||
image: owntracks/frontend
|
||||
ports:
|
||||
- 80:80
|
||||
@@ -81,8 +80,8 @@ directory as `docker-compose.yml`)
|
||||
|
||||
### Manually
|
||||
|
||||
- Run `yarn install` to install dependencies
|
||||
- Run `yarn build` to compile and minify for production
|
||||
- Run `npm install` to install dependencies
|
||||
- Run `npm run build` to compile and minify for production
|
||||
- Copy the content of the `dist/` directory to your webroot
|
||||
|
||||
## Configuration
|
||||
@@ -97,13 +96,15 @@ See [`docs/config.md`](docs/config.md) for all available options.
|
||||
|
||||
## Development
|
||||
|
||||
- Run `yarn install` to install dependencies
|
||||
- Run `yarn serve` to compile for development and start the hot-reload server
|
||||
- Run `yarn lint:js` to lint JavaScript/Vue files
|
||||
- Run `yarn lint:md` to lint Markdown files
|
||||
- Run `yarn format:js` to format JavaScript/Vue files
|
||||
- Run `yarn format:md` to format Markdown files
|
||||
- Run `yarn test` to run unit tests
|
||||
- Run `npm install` to install dependencies
|
||||
- Run `npm run dev` to compile for development and start the hot-reload server
|
||||
- Run `npm run lint:js` to lint JavaScript/Vue files
|
||||
- Run `npm run lint:md` to lint Markdown files
|
||||
- Run `npm run lint:scss` to lint SCSS files
|
||||
- Run `npm run format:js` to format JavaScript/Vue files
|
||||
- Run `npm run format:md` to format Markdown files
|
||||
- Run `npm run format:scss` to format SCSS files
|
||||
- Run `npm test` to run unit tests
|
||||
|
||||
### CORS-Proxy
|
||||
|
||||
@@ -111,7 +112,7 @@ You can use the [`corsProxy.js`](scripts/corsProxy.js) script to use your produc
|
||||
instance of OwnTracks for development without making changes to its CORS-Headers:
|
||||
|
||||
```console
|
||||
$ yarn cors-proxy
|
||||
$ npm run cors-proxy
|
||||
```
|
||||
|
||||
If you have [basic authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#Basic_authentication_scheme)
|
||||
@@ -125,7 +126,7 @@ OT_BASIC_AUTH_PASSWORD='P@$$w0rd'
|
||||
Then run:
|
||||
|
||||
```console
|
||||
$ env $(cat .env | xargs) yarn cors-proxy
|
||||
$ env $(cat .env | xargs) npm run cors-proxy
|
||||
```
|
||||
|
||||
The default host and port it binds to is `0.0.0.0:8888`. Change using the `OT_PROXY_HOST`
|
||||
@@ -139,7 +140,7 @@ This project uses [Vue I18n](https://kazupon.github.io/vue-i18n/). To see missin
|
||||
unused i18n entries, run:
|
||||
|
||||
```console
|
||||
$ yarn i18n:report
|
||||
$ npm run i18n:report
|
||||
```
|
||||
|
||||
To add a new locale, copy `en-US.json` to `<locale>.json` in [`src/locales`](src/locales)
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
presets: ["@vue/cli-plugin-babel/preset"],
|
||||
};
|
||||
@@ -1,13 +1,13 @@
|
||||
FROM node:14 as build
|
||||
FROM node:20 as build
|
||||
WORKDIR /usr/src/app
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn install
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm install
|
||||
COPY . ./
|
||||
RUN yarn build
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.18-alpine
|
||||
LABEL version="2.7.0"
|
||||
LABEL description="OwnTracks UI"
|
||||
FROM nginx:1.27-alpine
|
||||
LABEL version="2.15.3"
|
||||
LABEL description="OwnTracks Frontend"
|
||||
LABEL maintainer="Linus Groh <mail@linusgroh.de>"
|
||||
ENV LISTEN_PORT=80 \
|
||||
SERVER_HOST=otrecorder \
|
||||
|
||||
@@ -10,10 +10,10 @@ http {
|
||||
listen ${LISTEN_PORT};
|
||||
listen [::]:${LISTEN_PORT};
|
||||
root /usr/share/nginx/html;
|
||||
location /api {
|
||||
location /api/ {
|
||||
proxy_pass http://otrecorder/api/;
|
||||
}
|
||||
location /ws {
|
||||
location /ws/ {
|
||||
proxy_pass http://otrecorder/ws/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
@@ -31,6 +31,7 @@ http {
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_types text/plain text/css application/json application/javascript text/javascript;
|
||||
proxy_read_timeout 600;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,10 +53,12 @@ window.owntracks.config = {};
|
||||
- [`heatmap`](#maplayersheatmap)
|
||||
- [`last`](#maplayerslast)
|
||||
- [`line`](#maplayersline)
|
||||
- [`poi`](#maplayerspoi)
|
||||
- [`points`](#maplayerspoints)
|
||||
- [`maxNativeZoom`](#mapmaxnativezoom)
|
||||
- [`maxPointDistance`](#mapmaxpointdistance)
|
||||
- [`maxZoom`](#mapmaxzoom)
|
||||
- [`poiMarker`](#mappoimarker)
|
||||
- [`polyline`](#mappolyline)
|
||||
- [`url`](#mapurl)
|
||||
- `onLocationChange`
|
||||
@@ -171,11 +173,15 @@ formats.
|
||||
|
||||
Available languages:
|
||||
|
||||
- `cs-CZ` (Standard Czech)
|
||||
- `da-DK` (Standard Danish)
|
||||
- `de-DE` (Standard German)
|
||||
- `en-GB` (British English)
|
||||
- `en-US` (American English)
|
||||
- `es-ES` (Castilian Spanish)
|
||||
- `fr-FR` (Standard French)
|
||||
- `sk-SK` (Standard Slovak)
|
||||
- `tr-TR` (Standard Turkish)
|
||||
|
||||
Using a locale with non-existent translations is possible and will affect date/time formats, but
|
||||
use `en-US` for translations.
|
||||
@@ -202,7 +208,7 @@ Attribution for map tiles.
|
||||
### `map.circle`
|
||||
|
||||
Location accuracy indicator configuation. `color` and `fillColor` default to
|
||||
`primaryColor` if `null`. See [Vue2Leaflet `l-circle` documentation](https://korigan.github.io/Vue2Leaflet/#/components/l-circle/)
|
||||
`primaryColor` if `null`. See [Vue2Leaflet `l-circle` documentation](https://vue2-leaflet.netlify.app/components/LCircle.html)
|
||||
for all possible values.
|
||||
|
||||
- Type: [`Object`]
|
||||
@@ -218,7 +224,7 @@ for all possible values.
|
||||
### `map.circleMarker`
|
||||
|
||||
Location point marker configuation. `color` defaults to `primaryColor` if `null`. See
|
||||
[Vue2Leaflet `l-circle-marker` documentation](https://korigan.github.io/Vue2Leaflet/#/components/l-circle-marker/)
|
||||
[Vue2Leaflet `l-circle-marker` documentation](https://vue2-leaflet.netlify.app/components/LCircleMarker.html)
|
||||
for all possible values.
|
||||
|
||||
- Type: [`Object`]
|
||||
@@ -262,7 +268,7 @@ Whether to show an metric scale (m) or not.
|
||||
|
||||
### `map.controls.scale.position`
|
||||
|
||||
Scale control position on the map. See [Leaflet control position documentation](https://leafletjs.com/reference-1.5.0.html#control-position)
|
||||
Scale control position on the map. See [Leaflet control position documentation](https://leafletjs.com/reference.html#control)
|
||||
for all possible values.
|
||||
|
||||
- Type: [`String`]
|
||||
@@ -277,7 +283,7 @@ Whether to show zoom control or not.
|
||||
|
||||
### `map.controls.zoom.position`
|
||||
|
||||
Zoom control position on the map. See [Leaflet control position documentation](https://leafletjs.com/reference-1.5.0.html#control-position)
|
||||
Zoom control position on the map. See [Leaflet control position documentation](https://leafletjs.com/reference.html#control)
|
||||
for all possible values.
|
||||
|
||||
- Type: [`String`]
|
||||
@@ -333,9 +339,16 @@ Initial visibility of the line layer.
|
||||
- Type: [`Boolean`]
|
||||
- Default: `true`
|
||||
|
||||
### `map.layers.poi`
|
||||
|
||||
Initial visibility of the POI layer.
|
||||
|
||||
- Type: [`Boolean`]
|
||||
- Default: `true`
|
||||
|
||||
### `map.layers.points`
|
||||
|
||||
Initial visibility of the points layer.
|
||||
Initial visibility of the location points layer.
|
||||
|
||||
- Type: [`Boolean`]
|
||||
- Default: `false`
|
||||
@@ -376,10 +389,26 @@ to disable.
|
||||
- Type: [`Number`]
|
||||
- Default: `21`
|
||||
|
||||
### `map.poiMarker`
|
||||
|
||||
POI marker configuration. See [Vue2Leaflet `l-circle-marker` documentation](https://vue2-leaflet.netlify.app/components/LCircleMarker.html)
|
||||
for all possible values.
|
||||
|
||||
- Type: [`Object`]
|
||||
- Default:
|
||||
```js
|
||||
{
|
||||
color: "red",
|
||||
fillColor: "red",
|
||||
fillOpacity: 0.2,
|
||||
radius: 12
|
||||
}
|
||||
```
|
||||
|
||||
### `map.polyline`
|
||||
|
||||
Location point marker configuation. `color` defaults to `primaryColor` if `null`. See
|
||||
[Vue2Leaflet `l-polyline` documentation](https://korigan.github.io/Vue2Leaflet/#/components/l-polyline/)
|
||||
Location point marker configuration. `color` defaults to `primaryColor` if `null`. See
|
||||
[Vue2Leaflet `l-polyline` documentation](https://vue2-leaflet.netlify.app/components/LPolyline.html)
|
||||
for all possible values.
|
||||
|
||||
- Type: [`Object`]
|
||||
@@ -391,9 +420,17 @@ for all possible values.
|
||||
}
|
||||
```
|
||||
|
||||
### `map.tileSize`
|
||||
|
||||
Size of the tiles in pixels returned by the tile server. Can be used together with
|
||||
[`map.zoomOffset`](#map.zoomOffset) to configure bigger tile sizes.
|
||||
|
||||
- Type: [`Number`]
|
||||
- Default: `256`
|
||||
|
||||
### `map.url`
|
||||
|
||||
Tile server URL. For more information see [Leaflet tile layer documentation](https://leafletjs.com/reference-1.5.0.html#tilelayer-url-template)
|
||||
Tile server URL. For more information see [Leaflet tile layer documentation](https://leafletjs.com/reference.html#tilelayer)
|
||||
and [this Wikipedia article](https://en.wikipedia.org/wiki/Tiled_web_map).
|
||||
|
||||
- Type: [`String`]
|
||||
@@ -403,12 +440,19 @@ and [this Wikipedia article](https://en.wikipedia.org/wiki/Tiled_web_map).
|
||||
// Use dark HDPI tiles from Mapbox
|
||||
window.owntracks.config = {
|
||||
map: {
|
||||
url:
|
||||
"https://api.mapbox.com/v4/mapbox.dark/{z}/{x}/{y}@2x.png?access_token=xxxxxxxxxxxxxxxx",
|
||||
url: "https://api.mapbox.com/styles/v1/mapbox/dark-v10/tiles/{z}/{x}/{y}@2x?access_token=xxxxxxxxxxxxxxxx",
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### `map.zoomOffset`
|
||||
|
||||
Offset the zoom level to account for different tile sizes. For example tiles with a
|
||||
size of 512x512 need an offset of -1 and for 1024x1024 an offset of -2.
|
||||
|
||||
- Type: [`Number`]
|
||||
- Default: `0`
|
||||
|
||||
### `onLocationChange.fitView`
|
||||
|
||||
Whether to re-fit the map's content into view or not when a location update is received.
|
||||
@@ -499,6 +543,8 @@ header bar. `maxPointDistance` is being takein into account, if a distance betwe
|
||||
subsequent points is greater than `maxPointDistance`, it will not contibute to the
|
||||
calculated travelled distance.
|
||||
|
||||
This also includes a calculation of elevation gain / loss.
|
||||
|
||||
- Type: [`Boolean`]
|
||||
- Default: `true`
|
||||
|
||||
@@ -532,4 +578,4 @@ Whether to enable verbose mode or not.
|
||||
[`object`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object
|
||||
[`string`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String
|
||||
[css `<color>`]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
|
||||
[`fetch()` docs on mdn]: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters
|
||||
[`fetch()` docs on mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch#parameters
|
||||
|
||||
|
Before Width: | Height: | Size: 653 KiB After Width: | Height: | Size: 653 KiB |
47
eslint.config.js
Normal file
47
eslint.config.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import eslintPluginVue from "eslint-plugin-vue";
|
||||
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
|
||||
import vueParser from "vue-eslint-parser";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const eslintrc = new FlatCompat({
|
||||
baseDirectory: dirname(fileURLToPath(import.meta.url)),
|
||||
});
|
||||
|
||||
export default [
|
||||
...eslintrc.extends("plugin:vue/essential"),
|
||||
eslintPluginPrettierRecommended,
|
||||
{
|
||||
languageOptions: {
|
||||
parser: vueParser,
|
||||
},
|
||||
plugins: {
|
||||
vue: eslintPluginVue,
|
||||
},
|
||||
rules: {
|
||||
"no-console": process.env.NODE_ENV === "production" ? "error" : "warn",
|
||||
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn",
|
||||
"max-len": [
|
||||
"error",
|
||||
{
|
||||
ignoreUrls: true,
|
||||
},
|
||||
],
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
trailingComma: "es5",
|
||||
printWidth: 80,
|
||||
htmlWhitespaceSensitivity: "ignore",
|
||||
},
|
||||
],
|
||||
"vue/multi-word-component-names": [
|
||||
"error",
|
||||
{
|
||||
ignores: ["Map"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,18 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title>OwnTracks UI</title>
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="manifest" crossorigin="use-credentials" href="/manifest.json">
|
||||
<title>OwnTracks Frontend</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but OwnTracks doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<script src="<%= BASE_URL %>config/config.js"></script>
|
||||
<!-- built files will be auto injected -->
|
||||
<script src="./config/config.js"></script>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,23 +0,0 @@
|
||||
module.exports = {
|
||||
moduleFileExtensions: ["js", "jsx", "json", "vue"],
|
||||
transform: {
|
||||
"^.+\\.vue$": "vue-jest",
|
||||
".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$":
|
||||
"jest-transform-stub",
|
||||
"^.+\\.jsx?$": "babel-jest",
|
||||
},
|
||||
transformIgnorePatterns: ["/node_modules/"],
|
||||
moduleNameMapper: {
|
||||
"^@/(.*)$": "<rootDir>/src/$1",
|
||||
},
|
||||
snapshotSerializers: ["jest-serializer-vue"],
|
||||
testMatch: [
|
||||
"**/tests/**/*.test.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)",
|
||||
],
|
||||
testURL: "http://localhost/",
|
||||
watchPlugins: [
|
||||
"jest-watch-typeahead/filename",
|
||||
"jest-watch-typeahead/testname",
|
||||
],
|
||||
setupFiles: ["<rootDir>/tests/setup.js"],
|
||||
};
|
||||
4911
package-lock.json
generated
Normal file
4911
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
107
package.json
107
package.json
@@ -1,65 +1,62 @@
|
||||
{
|
||||
"name": "owntracks-ui",
|
||||
"version": "2.7.0",
|
||||
"name": "owntracks-frontend",
|
||||
"version": "2.15.3",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Linus Groh",
|
||||
"email": "mail@linusgroh.de"
|
||||
},
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"cors-proxy": "node scripts/corsProxy.js",
|
||||
"format:js": "vue-cli-service lint",
|
||||
"format:md": "prettier --write '{*.md,docs/**/*.md,src/**/*.md}'",
|
||||
"i18n:report": "vue-cli-service i18n:report --src './src/**/*.?(js|vue)' --locales './src/locales/**/*.json'",
|
||||
"lint:js": "vue-cli-service lint --no-fix",
|
||||
"lint:md": "prettier --check '{*.md,docs/**/*.md,src/**/*.md}'",
|
||||
"test": "vue-cli-service test:unit"
|
||||
},
|
||||
"dependencies": {
|
||||
"clipboard-copy": "^4.0.1",
|
||||
"core-js": "^3.8.3",
|
||||
"deepmerge": "^4.2.2",
|
||||
"leaflet": "^1.7.1",
|
||||
"leaflet.heat": "^0.2.0",
|
||||
"moment": "^2.29.1",
|
||||
"vue": "^2.6.12",
|
||||
"vue-ctk-date-time-picker": "^2.5.0",
|
||||
"vue-feather-icons": "^5.1.0",
|
||||
"vue-i18n": "^8.22.4",
|
||||
"vue-js-modal": "^1.3.33",
|
||||
"vue-mq": "^1.0.1",
|
||||
"vue-outside-events": "^1.1.3",
|
||||
"vue-router": "^3.5.1",
|
||||
"vue2-leaflet": "^2.6.0",
|
||||
"vuex": "^3.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.5.11",
|
||||
"@vue/cli-plugin-eslint": "~4.5.11",
|
||||
"@vue/cli-plugin-unit-jest": "~4.5.11",
|
||||
"@vue/cli-service": "~4.5.11",
|
||||
"@vue/eslint-config-prettier": "^6.0.0",
|
||||
"@vue/test-utils": "1.1.3",
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^26.6.3",
|
||||
"cors-anywhere": "^0.4.3",
|
||||
"eslint": "^7.20.0",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"eslint-plugin-vue": "^7.5.0",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"lint-staged": "^10.5.4",
|
||||
"moment-locales-webpack-plugin": "^1.2.0",
|
||||
"prettier": "^2.2.1",
|
||||
"sass": "^1.32.7",
|
||||
"sass-loader": "^10.1.1",
|
||||
"vue-cli-plugin-i18n": "~1.0.1",
|
||||
"vue-template-compiler": "^2.6.12"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/owntracks/frontend.git"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"cors-proxy": "node scripts/corsProxy.js",
|
||||
"format:js": "eslint --fix 'src/**/*.{js,vue}'",
|
||||
"format:md": "prettier --write '{*.md,docs/**/*.md,src/**/*.md}'",
|
||||
"format:scss": "prettier --write 'src/**/*.scss'",
|
||||
"lint:js": "eslint 'src/**/*.{js,vue}'",
|
||||
"lint:md": "prettier --check '{*.md,docs/**/*.md,src/**/*.md}'",
|
||||
"lint:scss": "prettier --check 'src/**/*.scss'",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"deepmerge": "^4.3.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet.heat": "^0.2.0",
|
||||
"moment": "^2.30.1",
|
||||
"vue": "^2.7.16",
|
||||
"vue-feather-icons": "^5.1.0",
|
||||
"vue-i18n": "^8.28.2",
|
||||
"vue-js-modal": "^2.0.1",
|
||||
"vue-mq": "^1.0.1",
|
||||
"vue-outside-events": "^1.1.3",
|
||||
"vue-router": "^3.6.5",
|
||||
"vue2-datepicker": "^3.11.1",
|
||||
"vue2-leaflet": "^2.7.1",
|
||||
"vuex": "^3.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@vitejs/plugin-vue2": "^2.3.1",
|
||||
"cors-anywhere": "^0.4.4",
|
||||
"eslint": "^9.6.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-vue": "^9.27.0",
|
||||
"jsdom": "^24.1.0",
|
||||
"lint-staged": "^15.2.7",
|
||||
"moment-locales-webpack-plugin": "^1.2.0",
|
||||
"prettier": "^3.3.2",
|
||||
"sass": "^1.77.6",
|
||||
"vite": "^5.3.3",
|
||||
"vite-plugin-package-version": "^1.1.0",
|
||||
"vitest": "^1.6.0",
|
||||
"vitest-fetch-mock": "^0.2.2",
|
||||
"vue-eslint-parser": "^9.4.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
189
public/OwnTracks.svg
Normal file
189
public/OwnTracks.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 35 KiB |
@@ -1,3 +1,3 @@
|
||||
// Here you can overwite the default configuration values
|
||||
// Here you can overwrite the default configuration values
|
||||
window.owntracks = window.owntracks || {};
|
||||
window.owntracks.config = {};
|
||||
|
||||
BIN
public/icon-180x180.png
Normal file
BIN
public/icon-180x180.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
20
public/manifest.json
Normal file
20
public/manifest.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "OwnTracks Frontend",
|
||||
"description": "OwnTracks Frontend",
|
||||
"icons": [
|
||||
{
|
||||
"src": "icon-180x180.png",
|
||||
"type": "image/png",
|
||||
"sizes": "180x180"
|
||||
},
|
||||
{
|
||||
"src": "OwnTracks.svg",
|
||||
"sizes": "any"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"background_color": "#3f51b5",
|
||||
"display": "standalone",
|
||||
"scope": ".",
|
||||
"theme_color": "#3f51b5"
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
const corsProxy = require("cors-anywhere");
|
||||
import { createServer } from "cors-anywhere";
|
||||
|
||||
const host = process.env.OT_PROXY_HOST || "0.0.0.0";
|
||||
const port = process.env.OT_PROXY_PORT || 8888;
|
||||
@@ -20,6 +20,6 @@ if (username !== null && password !== null) {
|
||||
};
|
||||
}
|
||||
|
||||
corsProxy.createServer(options).listen(port, host, () => {
|
||||
createServer(options).listen(port, host, () => {
|
||||
console.log(`Running CORS Anywhere on http://${host}:${port}`);
|
||||
});
|
||||
|
||||
18
src/App.vue
18
src/App.vue
@@ -4,28 +4,22 @@
|
||||
<main>
|
||||
<router-view />
|
||||
</main>
|
||||
<DownloadModal />
|
||||
<InformationModal />
|
||||
<LoadingModal />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import "styles/main";
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapActions } from "vuex";
|
||||
|
||||
import * as types from "@/store/mutation-types";
|
||||
import { log } from "@/logging";
|
||||
import AppHeader from "@/components/AppHeader";
|
||||
import DownloadModal from "@/components/modals/Download";
|
||||
import InformationModal from "@/components/modals/Information";
|
||||
import LoadingModal from "@/components/modals/Loading";
|
||||
import AppHeader from "@/components/AppHeader.vue";
|
||||
import InformationModal from "@/components/modals/InformationModal.vue";
|
||||
import LoadingModal from "@/components/modals/LoadingModal.vue";
|
||||
|
||||
export default {
|
||||
components: { AppHeader, DownloadModal, InformationModal, LoadingModal },
|
||||
components: { AppHeader, InformationModal, LoadingModal },
|
||||
created() {
|
||||
document.documentElement.style.setProperty(
|
||||
"--color-primary",
|
||||
@@ -95,3 +89,7 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "styles/main";
|
||||
</style>
|
||||
|
||||
69
src/api.js
69
src/api.js
@@ -7,23 +7,30 @@ import { getApiUrl, getLocationHistoryCount } from "@/util";
|
||||
*
|
||||
* @param {String} path API resource path
|
||||
* @param {Object} [params] Query parameters
|
||||
* @returns {Promise} Promise returned by the fetch function
|
||||
* @param {Object} [fetchOptions]
|
||||
* fetch() options (merged with config.api.fetchOptions)
|
||||
* @returns {Promise<Response>} Response returned by the fetch call
|
||||
*/
|
||||
const fetchApi = (path, params = {}) => {
|
||||
const fetchApi = (path, params = {}, fetchOptions = {}) => {
|
||||
const url = getApiUrl(path);
|
||||
Object.keys(params).forEach((key) =>
|
||||
url.searchParams.append(key, params[key])
|
||||
);
|
||||
Object.keys(params).forEach((key) => url.searchParams.set(key, params[key]));
|
||||
log("HTTP", `GET ${url.href}`);
|
||||
return fetch(url.href, config.api.fetchOptions).catch((error) =>
|
||||
log("HTTP", error, logLevels.ERROR)
|
||||
);
|
||||
return fetch(url.href, {
|
||||
...fetchOptions,
|
||||
...config.api.fetchOptions,
|
||||
}).catch((error) => {
|
||||
if (error.name === "AbortError") {
|
||||
log("HTTP", `GET ${url.href} - Request was aborted`, logLevels.WARNING);
|
||||
} else {
|
||||
log("HTTP", error, logLevels.ERROR);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the recorder's version.
|
||||
*
|
||||
* @returns {String} Version
|
||||
* @returns {Promise<String>} Version
|
||||
*/
|
||||
export const getVersion = async () => {
|
||||
const response = await fetchApi("/api/0/version");
|
||||
@@ -36,7 +43,7 @@ export const getVersion = async () => {
|
||||
/**
|
||||
* Get all users.
|
||||
*
|
||||
* @returns {User[]} Array of usernames
|
||||
* @returns {Promise<User[]>} Array of usernames
|
||||
*/
|
||||
export const getUsers = async () => {
|
||||
const response = await fetchApi("/api/0/list");
|
||||
@@ -50,7 +57,7 @@ export const getUsers = async () => {
|
||||
* Get all devices for the provided users.
|
||||
*
|
||||
* @param {User[]} users Array of usernames
|
||||
* @returns {{User: Device[]}}
|
||||
* @returns {Promise<{User: Device[]}>}
|
||||
* Object mapping each username to an array of device names
|
||||
*/
|
||||
export const getDevices = async (users) => {
|
||||
@@ -80,7 +87,7 @@ export const getDevices = async (users) => {
|
||||
*
|
||||
* @param {User} [user] Get last locations of all devices from this user
|
||||
* @param {Device} [device] Get last location of specific device
|
||||
* @returns {OTLocation[]} Array of last location objects
|
||||
* @returns {Promise<OTLocation[]>} Array of last location objects
|
||||
*/
|
||||
export const getLastLocations = async (user, device) => {
|
||||
const params = {};
|
||||
@@ -107,23 +114,33 @@ export const getLastLocations = async (user, device) => {
|
||||
* @param {Device} device Device name
|
||||
* @param {String} start Start date and time in UTC
|
||||
* @param {String} end End date and time in UTC
|
||||
* @returns {OTLocation[]} Array of location history objects
|
||||
* @param {Object} [fetchOptions] fetch() options
|
||||
* @returns {Promise<OTLocation[]>} Array of location history objects
|
||||
*/
|
||||
export const getUserDeviceLocationHistory = async (
|
||||
user,
|
||||
device,
|
||||
start,
|
||||
end
|
||||
end,
|
||||
fetchOptions
|
||||
) => {
|
||||
const response = await fetchApi("/api/0/locations", {
|
||||
from: start,
|
||||
to: end,
|
||||
user,
|
||||
device,
|
||||
format: "json",
|
||||
});
|
||||
const response = await fetchApi(
|
||||
"/api/0/locations",
|
||||
{
|
||||
from: start,
|
||||
to: end,
|
||||
user,
|
||||
device,
|
||||
format: "json",
|
||||
},
|
||||
fetchOptions
|
||||
);
|
||||
const json = await response.json();
|
||||
const userDeviceLocationHistory = json.data;
|
||||
// We need to manually sort by timestamp, otherwise the line segments may be
|
||||
// drawn in the wrong order. The recorder API simply returns entries in the
|
||||
// same order in which they are in each *.rec file.
|
||||
// See https://github.com/owntracks/frontend/issues/67.
|
||||
const userDeviceLocationHistory = json.data.sort((a, b) => a.tst - b.tst);
|
||||
log(
|
||||
"API",
|
||||
() =>
|
||||
@@ -141,9 +158,10 @@ export const getUserDeviceLocationHistory = async (
|
||||
* Devices of which the history should be fetched
|
||||
* @param {String} start Start date and time in UTC
|
||||
* @param {String} end End date and time in UTC
|
||||
* @returns {LocationHistory} Location history
|
||||
* @param {Object} [fetchOptions] fetch() options
|
||||
* @returns {Promise<LocationHistory>} Location history
|
||||
*/
|
||||
export const getLocationHistory = async (devices, start, end) => {
|
||||
export const getLocationHistory = async (devices, start, end, fetchOptions) => {
|
||||
const locationHistory = {};
|
||||
await Promise.all(
|
||||
Object.keys(devices).map(async (user) => {
|
||||
@@ -154,7 +172,8 @@ export const getLocationHistory = async (devices, start, end) => {
|
||||
user,
|
||||
device,
|
||||
start,
|
||||
end
|
||||
end,
|
||||
fetchOptions
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -32,7 +32,10 @@
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<LayersIcon size="1x" aria-hidden="true" role="img" />
|
||||
<Dropdown :label="$t('Layer settings')" :title="$t('Show/hide layers')">
|
||||
<DropdownButton
|
||||
:label="$t('Layer settings')"
|
||||
:title="$t('Show/hide layers')"
|
||||
>
|
||||
<label v-for="option in layerSettingsOptions" :key="option.layer">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -46,39 +49,35 @@
|
||||
/>
|
||||
{{ option.label }}
|
||||
</label>
|
||||
</Dropdown>
|
||||
</DropdownButton>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<CalendarIcon size="1x" aria-hidden="true" role="img" />
|
||||
<VueCtkDateTimePicker
|
||||
v-model="startDateTime"
|
||||
:format="DATE_TIME_FORMAT"
|
||||
:color="$config.primaryColor"
|
||||
:locale="$config.locale"
|
||||
:max-date="endDateTime"
|
||||
:button-now-translation="$t('Now')"
|
||||
<date-picker
|
||||
v-model="dateTimeRange"
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
:editable="false"
|
||||
:clearable="false"
|
||||
:confirm="true"
|
||||
:show-second="false"
|
||||
:range="true"
|
||||
range-separator=" – "
|
||||
:shortcuts="shortcuts"
|
||||
:show-time-panel="showTimeRangePanel"
|
||||
:disabled-date="(date, _) => date > new Date()"
|
||||
@change="handleDateTimeRangeChange"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-button button"
|
||||
:title="$t('Select start date')"
|
||||
/>
|
||||
</VueCtkDateTimePicker>
|
||||
<span>{{ $t("to") }}</span>
|
||||
<VueCtkDateTimePicker
|
||||
v-model="endDateTime"
|
||||
:format="DATE_TIME_FORMAT"
|
||||
:color="$config.primaryColor"
|
||||
:locale="$config.locale"
|
||||
:min-date="startDateTime"
|
||||
:button-now-translation="$t('Now')"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-button button"
|
||||
:title="$t('Select end date')"
|
||||
/>
|
||||
</VueCtkDateTimePicker>
|
||||
<template v-slot:footer>
|
||||
<button
|
||||
class="mx-btn toggle-date-btn"
|
||||
type="button"
|
||||
@click="toggleTimeRangePanel"
|
||||
>
|
||||
{{ showTimeRangePanel ? $t("Select date") : $t("Select time") }}
|
||||
</button>
|
||||
</template>
|
||||
</date-picker>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<UserIcon size="1x" aria-hidden="true" role="img" />
|
||||
@@ -90,7 +89,7 @@
|
||||
<option :value="null">
|
||||
{{ $t("Show all") }}
|
||||
</option>
|
||||
<option v-for="user in users" :value="user" :key="user">
|
||||
<option v-for="user in users" :key="user" :value="user">
|
||||
{{ user }}
|
||||
</option>
|
||||
</select>
|
||||
@@ -107,8 +106,8 @@
|
||||
</option>
|
||||
<option
|
||||
v-for="device in devices[selectedUser]"
|
||||
:value="device"
|
||||
:key="`${selectedUser}-${device}`"
|
||||
:value="device"
|
||||
>
|
||||
{{ device }}
|
||||
</option>
|
||||
@@ -118,23 +117,19 @@
|
||||
<nav class="header-item header-item-right">
|
||||
<div
|
||||
v-if="$config.showDistanceTravelled && distanceTravelled"
|
||||
class="nav-item"
|
||||
:title="$t('Distance travelled')"
|
||||
class="nav-item distance-travelled"
|
||||
>
|
||||
{{ humanReadableDistance(distanceTravelled) }}
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<button
|
||||
class="button button-flat button-icon"
|
||||
:title="$t('Download raw data')"
|
||||
@click="$modal.show('download')"
|
||||
>
|
||||
<DownloadIcon
|
||||
size="1x"
|
||||
:aria-label="$t('Download raw data')"
|
||||
role="img"
|
||||
/>
|
||||
</button>
|
||||
<span :title="$t('Distance travelled')">
|
||||
{{ humanReadableDistance(distanceTravelled) }}
|
||||
</span>
|
||||
<br />
|
||||
<span :title="$t('Elevation gain / loss')">
|
||||
<ArrowUpIcon size="0.8x" role="img" />
|
||||
{{ humanReadableDistance(elevationGain) }}
|
||||
/
|
||||
<ArrowDownIcon size="0.8x" role="img" />
|
||||
{{ humanReadableDistance(elevationLoss) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<button
|
||||
@@ -153,35 +148,38 @@
|
||||
import moment from "moment";
|
||||
import { mapActions, mapGetters, mapMutations, mapState } from "vuex";
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
ArrowUpIcon,
|
||||
CalendarIcon,
|
||||
CrosshairIcon,
|
||||
DownloadIcon,
|
||||
InfoIcon,
|
||||
LayersIcon,
|
||||
MenuIcon,
|
||||
SmartphoneIcon,
|
||||
UserIcon,
|
||||
} from "vue-feather-icons";
|
||||
import VueCtkDateTimePicker from "vue-ctk-date-time-picker";
|
||||
import "vue-ctk-date-time-picker/dist/vue-ctk-date-time-picker.css";
|
||||
|
||||
import Dropdown from "@/components/Dropdown";
|
||||
import DatePicker from "vue2-datepicker";
|
||||
import "vue2-datepicker/index.css";
|
||||
|
||||
import DropdownButton from "@/components/DropdownButton.vue";
|
||||
import { DATE_TIME_FORMAT } from "@/constants";
|
||||
import * as types from "@/store/mutation-types";
|
||||
import { humanReadableDistance } from "@/util";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ArrowDownIcon,
|
||||
ArrowUpIcon,
|
||||
CalendarIcon,
|
||||
CrosshairIcon,
|
||||
DownloadIcon,
|
||||
DatePicker,
|
||||
InfoIcon,
|
||||
LayersIcon,
|
||||
MenuIcon,
|
||||
SmartphoneIcon,
|
||||
UserIcon,
|
||||
VueCtkDateTimePicker,
|
||||
Dropdown,
|
||||
DropdownButton,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -191,12 +189,121 @@ export default {
|
||||
{ layer: "line", label: this.$t("Show location history (line)") },
|
||||
{ layer: "points", label: this.$t("Show location history (points)") },
|
||||
{ layer: "heatmap", label: this.$t("Show location heatmap") },
|
||||
{ layer: "poi", label: this.$t("Show points of interest") },
|
||||
],
|
||||
showMobileNav: false,
|
||||
shortcuts: [
|
||||
{
|
||||
text: this.$t("Today"),
|
||||
onClick() {
|
||||
const end = new Date();
|
||||
end.setHours(23, 59, 59, 0);
|
||||
const start = new Date();
|
||||
start.setHours(0, 0, 0, 0);
|
||||
return [start, end];
|
||||
},
|
||||
},
|
||||
{
|
||||
text: this.$t("Yesterday"),
|
||||
onClick() {
|
||||
const end = new Date();
|
||||
end.setDate(end.getDate() - 1);
|
||||
end.setHours(23, 59, 59, 0);
|
||||
const start = new Date(end);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
return [start, end];
|
||||
},
|
||||
},
|
||||
{
|
||||
text: this.$t("3 days"),
|
||||
onClick() {
|
||||
const end = new Date();
|
||||
end.setHours(23, 59, 59, 0);
|
||||
const start = new Date();
|
||||
start.setDate(end.getDate() - 3);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
return [start, end];
|
||||
},
|
||||
},
|
||||
{
|
||||
text: this.$t("7 days"),
|
||||
onClick() {
|
||||
const end = new Date();
|
||||
end.setHours(23, 59, 59, 0);
|
||||
const start = new Date();
|
||||
start.setDate(end.getDate() - 7);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
return [start, end];
|
||||
},
|
||||
},
|
||||
{
|
||||
text: this.$t("15 days"),
|
||||
onClick() {
|
||||
const end = new Date();
|
||||
end.setHours(23, 59, 59, 0);
|
||||
const start = new Date();
|
||||
start.setDate(end.getDate() - 15);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
return [start, end];
|
||||
},
|
||||
},
|
||||
{
|
||||
text: this.$t("30 days"),
|
||||
onClick() {
|
||||
const end = new Date();
|
||||
end.setHours(23, 59, 59, 0);
|
||||
const start = new Date();
|
||||
start.setDate(end.getDate() - 30);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
return [start, end];
|
||||
},
|
||||
},
|
||||
{
|
||||
text: this.$t("3 months"),
|
||||
onClick() {
|
||||
const end = new Date();
|
||||
end.setHours(23, 59, 59, 0);
|
||||
const start = new Date();
|
||||
start.setMonth(end.getMonth() - 3);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
return [start, end];
|
||||
},
|
||||
},
|
||||
{
|
||||
text: this.$t("6 months"),
|
||||
onClick() {
|
||||
const end = new Date();
|
||||
end.setHours(23, 59, 59, 0);
|
||||
const start = new Date();
|
||||
start.setMonth(end.getMonth() - 6);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
return [start, end];
|
||||
},
|
||||
},
|
||||
{
|
||||
text: this.$t("1 year"),
|
||||
onClick() {
|
||||
const end = new Date();
|
||||
end.setHours(23, 59, 59, 0);
|
||||
const start = new Date();
|
||||
start.setFullYear(end.getFullYear() - 1);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
return [start, end];
|
||||
},
|
||||
},
|
||||
],
|
||||
showTimeRangePanel: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(["users", "devices", "map", "distanceTravelled"]),
|
||||
...mapState([
|
||||
"users",
|
||||
"devices",
|
||||
"map",
|
||||
"distanceTravelled",
|
||||
"elevationGain",
|
||||
"elevationLoss",
|
||||
]),
|
||||
selectedUser: {
|
||||
get() {
|
||||
return this.$store.state.selectedUser;
|
||||
@@ -213,32 +320,25 @@ export default {
|
||||
this.setSelectedDevice(value);
|
||||
},
|
||||
},
|
||||
startDateTime: {
|
||||
dateTimeRange: {
|
||||
get() {
|
||||
return moment
|
||||
const startDateTime = moment
|
||||
.utc(this.$store.state.startDateTime, DATE_TIME_FORMAT)
|
||||
.local()
|
||||
.format(DATE_TIME_FORMAT);
|
||||
},
|
||||
set(value) {
|
||||
this.setStartDateTime(
|
||||
moment(value, DATE_TIME_FORMAT).utc().format(DATE_TIME_FORMAT)
|
||||
);
|
||||
},
|
||||
},
|
||||
endDateTime: {
|
||||
get() {
|
||||
return moment
|
||||
.toDate();
|
||||
const endDateTime = moment
|
||||
.utc(this.$store.state.endDateTime, DATE_TIME_FORMAT)
|
||||
.local()
|
||||
.format(DATE_TIME_FORMAT);
|
||||
.toDate();
|
||||
return [startDateTime, endDateTime];
|
||||
},
|
||||
set(value) {
|
||||
set([startDateTime, endDateTime]) {
|
||||
this.setStartDateTime(
|
||||
moment(startDateTime).utc().format(DATE_TIME_FORMAT)
|
||||
);
|
||||
|
||||
this.setEndDateTime(
|
||||
moment(value, DATE_TIME_FORMAT)
|
||||
.set("seconds", 59)
|
||||
.utc()
|
||||
.format(DATE_TIME_FORMAT)
|
||||
moment(endDateTime).set("seconds", 59).utc().format(DATE_TIME_FORMAT)
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -254,6 +354,25 @@ export default {
|
||||
"setEndDateTime",
|
||||
]),
|
||||
humanReadableDistance,
|
||||
toggleTimeRangePanel() {
|
||||
this.showTimeRangePanel = !this.showTimeRangePanel;
|
||||
},
|
||||
// Resetting to date choice after value change
|
||||
handleDateTimeRangeChange(value, type) {
|
||||
this.showTimeRangePanel = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.distance-travelled {
|
||||
text-align: right;
|
||||
line-height: 1.2;
|
||||
|
||||
.feather {
|
||||
margin-top: 3px;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="dropdown" v-focus-outside="hide" v-click-outside="hide">
|
||||
<div v-focus-outside="hide" v-click-outside="hide" class="dropdown">
|
||||
<button class="dropdown-button button" :title="title" @click="toggle">
|
||||
{{ label }}
|
||||
</button>
|
||||
@@ -7,11 +7,18 @@
|
||||
:src="faceImageDataURI"
|
||||
:alt="$t('Image of {deviceName}', { deviceName })"
|
||||
:title="$t('Image of {deviceName}', { deviceName })"
|
||||
class="face"
|
||||
/>
|
||||
<ul class="info-list">
|
||||
<li :title="$t('Timestamp')">
|
||||
<ClockIcon size="1x" aria-hidden="true" role="img" />
|
||||
{{ new Date(timestamp * 1000).toLocaleString($config.locale) }}
|
||||
<span v-if="isoLocal && timeZone">
|
||||
<br />
|
||||
<code style="font-size: 0.7rem">
|
||||
{{ isoLocal }}[{{ timeZone }}]
|
||||
</code>
|
||||
</span>
|
||||
</li>
|
||||
<li :title="$t('Location')">
|
||||
<MapPinIcon size="1x" aria-hidden="true" role="img" />
|
||||
@@ -33,6 +40,11 @@
|
||||
<ZapIcon size="1x" aria-hidden="true" role="img" />
|
||||
{{ speed }} km/h
|
||||
</li>
|
||||
<li v-if="wifi.ssid" :title="$t('WiFi')">
|
||||
<WifiIcon size="1x" aria-hidden="true" role="img" />
|
||||
{{ wifi.ssid }}
|
||||
<span v-if="wifi.bssid">({{ wifi.bssid }})</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="regions.length" class="regions">
|
||||
@@ -42,37 +54,13 @@
|
||||
</LPopup>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.device {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: -5px;
|
||||
color: var(--color-primary);
|
||||
font-weight: bold;
|
||||
}
|
||||
.wrapper {
|
||||
display: flex;
|
||||
margin-top: 10px;
|
||||
margin-right: 20px;
|
||||
|
||||
img {
|
||||
align-self: start;
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
.regions {
|
||||
border-top: 1px solid var(--color-separator);
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import {
|
||||
BatteryIcon,
|
||||
ClockIcon,
|
||||
HomeIcon,
|
||||
MapPinIcon,
|
||||
WifiIcon,
|
||||
ZapIcon,
|
||||
} from "vue-feather-icons";
|
||||
import { LPopup } from "vue2-leaflet";
|
||||
@@ -84,6 +72,7 @@ export default {
|
||||
ClockIcon,
|
||||
HomeIcon,
|
||||
MapPinIcon,
|
||||
WifiIcon,
|
||||
ZapIcon,
|
||||
LPopup,
|
||||
},
|
||||
@@ -108,6 +97,14 @@ export default {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
isoLocal: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
timeZone: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
lat: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
@@ -136,6 +133,10 @@ export default {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
wifi: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
@@ -163,3 +164,27 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.device {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: -5px;
|
||||
color: var(--color-primary);
|
||||
font-weight: bold;
|
||||
}
|
||||
.wrapper {
|
||||
display: flex;
|
||||
margin-top: 10px;
|
||||
|
||||
img {
|
||||
align-self: start;
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
.regions {
|
||||
border-top: 1px solid var(--color-separator);
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,12 +2,6 @@
|
||||
<div />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
div {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// See https://github.com/KoRiGaN/Vue2Leaflet/blob/e0cf0f29bc519f0a70f0f1eb6e579f947e7ea4ce/src/utils/utils.js
|
||||
// to understand the `custom` attribute of each prop, how the `set<Prop>`
|
||||
@@ -136,3 +130,9 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
div {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
<template>
|
||||
<modal name="download" adaptive>
|
||||
<pre class="data"><code>{{ data }}</code></pre>
|
||||
<div class="options">
|
||||
<input
|
||||
v-model="options.minifyJson"
|
||||
type="checkbox"
|
||||
id="option-minify-json"
|
||||
/>
|
||||
<label for="option-minify-json">
|
||||
{{ $t("Minify JSON") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button
|
||||
class="button button-outline button-primary"
|
||||
:title="$t('Copy to clipboard')"
|
||||
@click="copy"
|
||||
>
|
||||
{{ $t("Copy to clipboard") }}
|
||||
</button>
|
||||
<button
|
||||
class="button button-primary"
|
||||
:title="$t('Download')"
|
||||
@click="download"
|
||||
>
|
||||
{{ $t("Download") }}
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.data {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.options {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
margin-top: 30px;
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
|
||||
&:first-child {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from "vuex";
|
||||
import copy from "clipboard-copy";
|
||||
|
||||
import { download } from "@/util";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
options: {
|
||||
minifyJson: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
"startDateTime",
|
||||
"endDateTime",
|
||||
"selectedUser",
|
||||
"selectedDevice",
|
||||
"locationHistory",
|
||||
]),
|
||||
data() {
|
||||
return this.locationHistory;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
copy() {
|
||||
const data = JSON.stringify(
|
||||
this.data,
|
||||
null,
|
||||
this.options.minifyJson ? 0 : 2
|
||||
);
|
||||
copy(data);
|
||||
},
|
||||
download() {
|
||||
const data = JSON.stringify(
|
||||
this.data,
|
||||
null,
|
||||
this.options.minifyJson ? 0 : 2
|
||||
);
|
||||
const start = this.startDateTime;
|
||||
const end = this.endDateTime;
|
||||
const user = this.selectedUser ? `_${this.selectedUser}` : "";
|
||||
const device = this.selectedDevice ? `_${this.selectedDevice}` : "";
|
||||
const filename = `data_${start}_${end}${user}${device}.json`;
|
||||
download(data, filename, "application/json");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -24,9 +24,9 @@
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<TwitterIcon size="1x" aria-hidden="true" role="img" />
|
||||
<a href="https://twitter.com/OwnTracks">
|
||||
{{ $t("OwnTracks on Twitter") }}
|
||||
<AtSignIcon size="1x" aria-hidden="true" role="img" />
|
||||
<a href="https://fosstodon.org/@owntracks">
|
||||
{{ $t("OwnTracks on Mastodon") }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -35,15 +35,10 @@
|
||||
|
||||
<script>
|
||||
import { mapState } from "vuex";
|
||||
import {
|
||||
BookIcon,
|
||||
GithubIcon,
|
||||
GlobeIcon,
|
||||
TwitterIcon,
|
||||
} from "vue-feather-icons";
|
||||
import { AtSignIcon, BookIcon, GithubIcon, GlobeIcon } from "vue-feather-icons";
|
||||
|
||||
export default {
|
||||
components: { BookIcon, GithubIcon, GlobeIcon, TwitterIcon },
|
||||
components: { AtSignIcon, BookIcon, GithubIcon, GlobeIcon },
|
||||
computed: {
|
||||
...mapState(["frontendVersion", "recorderVersion"]),
|
||||
},
|
||||
@@ -1,16 +1,43 @@
|
||||
<template>
|
||||
<modal name="loading" :click-to-close="false" adaptive>
|
||||
<LoaderIcon class="loader" size="1.5x" />
|
||||
<LoaderIcon class="loader-icon" size="1.5x" />
|
||||
{{ $t("Loading data, please wait...") }}
|
||||
<br />
|
||||
<button
|
||||
type="button"
|
||||
class="button button-primary button-cancel"
|
||||
@click="requestAbortController.abort()"
|
||||
>
|
||||
{{ $t("Cancel") }}
|
||||
</button>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from "vuex";
|
||||
import { LoaderIcon } from "vue-feather-icons";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LoaderIcon,
|
||||
},
|
||||
computed: {
|
||||
...mapState(["requestAbortController"]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.loader {
|
||||
.loader-icon {
|
||||
animation: spinning 2s linear infinite;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.button-cancel {
|
||||
display: block;
|
||||
margin: 20px auto 0;
|
||||
}
|
||||
|
||||
@keyframes spinning {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
@@ -20,13 +47,3 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { LoaderIcon } from "vue-feather-icons";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LoaderIcon,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -55,16 +55,25 @@ const DEFAULT_CONFIG = {
|
||||
heatmap: false,
|
||||
last: true,
|
||||
line: true,
|
||||
poi: true,
|
||||
points: false,
|
||||
},
|
||||
maxNativeZoom: 19,
|
||||
maxPointDistance: null,
|
||||
maxZoom: 21,
|
||||
poiMarker: {
|
||||
color: "red",
|
||||
fillColor: "red",
|
||||
fillOpacity: 0.2,
|
||||
radius: 12,
|
||||
},
|
||||
polyline: {
|
||||
color: null,
|
||||
fillColor: "transparent",
|
||||
},
|
||||
tileSize: 256,
|
||||
url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
zoomOffset: 0,
|
||||
},
|
||||
onLocationChange: {
|
||||
fitView: false,
|
||||
|
||||
32
src/i18n.js
32
src/i18n.js
@@ -3,17 +3,31 @@ import VueI18n from "vue-i18n";
|
||||
|
||||
import config from "@/config";
|
||||
|
||||
// TODO: This should be possible to do with https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n,
|
||||
// but that breaks at runtime - may only work with vue-i18n@9?
|
||||
import cs_CZ from "@/locales/cs-CZ.json";
|
||||
import da_DK from "@/locales/da-DK.json";
|
||||
import de_DE from "@/locales/de-DE.json";
|
||||
import en_GB from "@/locales/en-GB.json";
|
||||
import en_US from "@/locales/en-US.json";
|
||||
import es_ES from "@/locales/es-ES.json";
|
||||
import fr_FR from "@/locales/fr-FR.json";
|
||||
import sk_SK from "@/locales/sk-SK.json";
|
||||
import tr_TR from "@/locales/tr-TR.json";
|
||||
|
||||
Vue.use(VueI18n);
|
||||
|
||||
const locales = require.context("./locales", true, /[A-Za-z0-9-_,\s]+\.json$/i);
|
||||
const messages = {};
|
||||
locales.keys().forEach((key) => {
|
||||
const matched = key.match(/([A-Za-z0-9-_]+)\./i);
|
||||
if (matched && matched.length > 1) {
|
||||
const locale = matched[1];
|
||||
messages[locale] = locales(key);
|
||||
}
|
||||
});
|
||||
const messages = {
|
||||
"cs-CZ": cs_CZ,
|
||||
"da-DK": da_DK,
|
||||
"de-DE": de_DE,
|
||||
"en-GB": en_GB,
|
||||
"en-US": en_US,
|
||||
"es-ES": es_ES,
|
||||
"fr-FR": fr_FR,
|
||||
"sk-SK": sk_SK,
|
||||
"tr-TR": tr_TR,
|
||||
};
|
||||
|
||||
export default new VueI18n({
|
||||
locale: config.locale,
|
||||
|
||||
26
src/index.d.ts
vendored
26
src/index.d.ts
vendored
@@ -46,16 +46,25 @@ interface Config {
|
||||
heatmap: boolean;
|
||||
last: boolean;
|
||||
line: boolean;
|
||||
poi: boolean;
|
||||
points: boolean;
|
||||
};
|
||||
maxNativeZoom: number;
|
||||
maxPointDistance: number | null;
|
||||
maxZoom: number;
|
||||
poiMarker: {
|
||||
color: OptionalColor;
|
||||
fillColor: OptionalColor;
|
||||
fillOpacity: number;
|
||||
radius: number;
|
||||
};
|
||||
polyline: {
|
||||
color: OptionalColor;
|
||||
fillColor: OptionalColor;
|
||||
};
|
||||
tileSize: number;
|
||||
url: string;
|
||||
zoomOffset: number;
|
||||
};
|
||||
onLocationChange: {
|
||||
fitView: boolean;
|
||||
@@ -94,6 +103,7 @@ interface State {
|
||||
heatmap: boolean;
|
||||
last: boolean;
|
||||
line: boolean;
|
||||
poi: boolean;
|
||||
points: boolean;
|
||||
};
|
||||
zoom: number;
|
||||
@@ -136,6 +146,8 @@ interface OTLocation {
|
||||
* - `"m"` = mobile data
|
||||
*/
|
||||
conn?: string;
|
||||
/** identifies the time at which the message is constructed (vs. `tst` which is the timestamp of the GPS fix) */
|
||||
created_at?: string;
|
||||
/** Device name */
|
||||
device?: Device;
|
||||
/** Timestamp in a readable format */
|
||||
@@ -147,8 +159,10 @@ interface OTLocation {
|
||||
* https://en.wikipedia.org/wiki/Geohash
|
||||
*/
|
||||
ghash?: string;
|
||||
/** Regions the device is currently in (e.g. `["Home", "Garage"]`). Might be empty. */
|
||||
/** contains a list of regions the device is currently in (e.g. ["Home","Garage"]). Might be empty. */
|
||||
inregions?: string[];
|
||||
/** contains a list of region IDs the device is currently in (e.g. ["6da9cf","3defa7"]). Might be empty. */
|
||||
inrids?: string[];
|
||||
/**
|
||||
* No idea; some kind of timestamp as well - figure it out yourself. :)
|
||||
* https://github.com/owntracks/recorder/blob/df009f791a845012e9cce24923e6203a079ca1ed/storage.c#L659
|
||||
@@ -157,12 +171,18 @@ interface OTLocation {
|
||||
isorcv?: string;
|
||||
/** ISO 8601 timestamp */
|
||||
isotst?: string;
|
||||
/** ISO 8601 timestamp in local time */
|
||||
isolocal?: string;
|
||||
/** tzdb time zone name */
|
||||
tzname?: string;
|
||||
/** Latitude in degrees */
|
||||
lat: number;
|
||||
/** Longitude in degrees */
|
||||
lon: number;
|
||||
/** Friendly device name */
|
||||
name?: string;
|
||||
/** Point of interest name */
|
||||
poi?: string;
|
||||
/**
|
||||
* Trigger for the location report
|
||||
*
|
||||
@@ -190,6 +210,10 @@ interface OTLocation {
|
||||
vac?: number;
|
||||
/** Velocity in km/h */
|
||||
vel?: number;
|
||||
/** SSID, if available, is the unique name of the WLAN. */
|
||||
SSID?: string;
|
||||
/** BSSID, if available, identifies the access point. */
|
||||
BSSID?: string;
|
||||
}
|
||||
|
||||
/** URL query parameters (prior to any parsing so it's all strings). */
|
||||
|
||||
45
src/locales/cs-CZ.json
Normal file
45
src/locales/cs-CZ.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"Automatically center the map view and zoom in to relevant data": "Automaticky vystředit pohled na mapu a přiblížit na příslušné údaje",
|
||||
"Fit view": "Napasovat pohled",
|
||||
"Layer settings": "Nastavení vrstvy",
|
||||
"Show/hide layers": "Zobrazit/skrýť vrstvy",
|
||||
"Now": "Teď",
|
||||
"Select start date": "Zvolit počáteční datum",
|
||||
"to": "do",
|
||||
"Select end date": "Zvolit konečný datum",
|
||||
"Select user": "Zvolit uživatele",
|
||||
"Show all": "Zobrazit všechno",
|
||||
"Select device": "Zvolit zařízení",
|
||||
"Distance travelled": "Procestovaná vzdálenost",
|
||||
"Elevation gain / loss": "Výškový výstup / pokles",
|
||||
"Information": "Informace",
|
||||
"Show last known locations": "Zobrazit naposledy známé polohy",
|
||||
"Show location history (line)": "Zobrazit historii poloh (čára)",
|
||||
"Show location history (points)": "Zobrazit historii poloh (body)",
|
||||
"Show location heatmap": "Zobrazit tepelnou mapu poloh",
|
||||
"Minify JSON": "Zminimalizovat JSON",
|
||||
"Copy to clipboard": "Zkopírovat do schránky",
|
||||
"Loading version...": "Nahrávám verzi...",
|
||||
"OwnTracks website": "Web Stránka OwnTracks",
|
||||
"OwnTracks documentation": "Dokumentace OwnTracks",
|
||||
"OwnTracks on Mastodon": "OwnTracks na Mastodon",
|
||||
"Loading data, please wait...": "Nahrávám údaje, prosím počkejte...",
|
||||
"Cancel": "Zrušit",
|
||||
"Image of {deviceName}": "Obrázek {deviceName}",
|
||||
"Timestamp": "Čas",
|
||||
"Location": "Poloha",
|
||||
"Address": "Adresa",
|
||||
"Battery": "Baterie",
|
||||
"Speed": "Rychlost",
|
||||
"Regions:": "Oblasti:",
|
||||
"WiFi": "WiFi",
|
||||
"Today": "Dnes",
|
||||
"Yesterday": "Včera",
|
||||
"3 days": "3 dny",
|
||||
"7 days": "7 dní",
|
||||
"15 days": "15 dní",
|
||||
"30 days": "30 dní",
|
||||
"3 months": "3 měsíce",
|
||||
"6 months": "6 měsíců",
|
||||
"1 year": "1 rok"
|
||||
}
|
||||
45
src/locales/da-DK.json
Normal file
45
src/locales/da-DK.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"Automatically center the map view and zoom in to relevant data": "Centrér automatisk kortvisningen og zoom ind på relevant data",
|
||||
"Fit view": "Tilpas visning",
|
||||
"Layer settings": "Lag-indstillinger",
|
||||
"Show/hide layers": "Vis/skjul lag",
|
||||
"Now": "Nu",
|
||||
"Select start date": "Vælg startdato",
|
||||
"to": "til",
|
||||
"Select end date": "Vælg slutdato",
|
||||
"Select user": "Vælg bruger",
|
||||
"Show all": "Vis alt",
|
||||
"Select device": "Vælg enhed",
|
||||
"Distance travelled": "Afstand rejst",
|
||||
"Elevation gain / loss": "Højde vundet / tabt",
|
||||
"Information": "Information",
|
||||
"Show last known locations": "Vis sidst kendte positioner",
|
||||
"Show location history (line)": "Vis positionshistorik (linje)",
|
||||
"Show location history (points)": "Vis positionshistorik (punkter)",
|
||||
"Show location heatmap": "Vis positions-heatmap",
|
||||
"Minify JSON": "Minificér JSON",
|
||||
"Copy to clipboard": "Kopiér til udklipsholder",
|
||||
"Loading version...": "Indlæser version...",
|
||||
"OwnTracks website": "OwnTracks hjemmeside",
|
||||
"OwnTracks documentation": "OwnTracks dokumentation",
|
||||
"OwnTracks on Mastodon": "OwnTracks på Mastodon",
|
||||
"Loading data, please wait...": "Indlæser data, vent venligst...",
|
||||
"Cancel": "Fortryd",
|
||||
"Image of {deviceName}": "Billede af {deviceName}",
|
||||
"Timestamp": "Tidspunkt",
|
||||
"Location": "Position",
|
||||
"Address": "Adresse",
|
||||
"Battery": "Batteri",
|
||||
"Speed": "Hastighed",
|
||||
"Regions:": "Regioner:",
|
||||
"WiFi": "WiFi",
|
||||
"Today": "I dag",
|
||||
"Yesterday": "I går",
|
||||
"3 days": "3 dage",
|
||||
"7 days": "7 dage",
|
||||
"15 days": "15 dage",
|
||||
"30 days": "30 dage",
|
||||
"3 months": "3 måneder",
|
||||
"6 months": "6 måneder",
|
||||
"1 year": "1 år"
|
||||
}
|
||||
@@ -11,25 +11,38 @@
|
||||
"Show all": "Alle anzeigen",
|
||||
"Select device": "Gerät auswählen",
|
||||
"Distance travelled": "Gereiste Entfernung",
|
||||
"Download raw data": "Rohdaten herunterladen",
|
||||
"Elevation gain / loss": "Höhengewinn / -verlust",
|
||||
"Information": "Information",
|
||||
"Show last known locations": "Zeige letzte bekannte Standorte",
|
||||
"Show location history (line)": "Zeige Standortverlauf (Linie)",
|
||||
"Show location history (points)": "Zeige Standortverlauf (Punkte)",
|
||||
"Show location heatmap": "Zeige Standort-Heatmap",
|
||||
"Show points of interest": "Zeige Sehenswürdigkeiten",
|
||||
"Minify JSON": "JSON minimieren",
|
||||
"Copy to clipboard": "In die Zwischenablage kopieren",
|
||||
"Download": "Herunterladen",
|
||||
"Loading version...": "Version wird abgerufen...",
|
||||
"OwnTracks website": "OwnTracks Webseite",
|
||||
"OwnTracks documentation": "OwnTracks Dokumentation",
|
||||
"OwnTracks on Twitter": "OwnTracks auf Twitter",
|
||||
"OwnTracks on Mastodon": "OwnTracks auf Mastodon",
|
||||
"Loading data, please wait...": "Daten werden geladen, bitte warten...",
|
||||
"Cancel": "Abbrechen",
|
||||
"Image of {deviceName}": "Bild von {deviceName}",
|
||||
"Timestamp": "Zeitstempel",
|
||||
"Location": "Standort",
|
||||
"Address": "Adresse",
|
||||
"Battery": "Akku",
|
||||
"Speed": "Geschwindigkeit",
|
||||
"Regions:": "Regionen:"
|
||||
"Regions:": "Regionen:",
|
||||
"Select date": "Datum auswählen",
|
||||
"Select time": "Uhrzeit auswählen",
|
||||
"WiFi": "WLAN",
|
||||
"Today": "Heute",
|
||||
"Yesterday": "Gestern",
|
||||
"3 days": "3 Tage",
|
||||
"7 days": "7 Tage",
|
||||
"15 days": "15 Tage",
|
||||
"30 days": "30 Tage",
|
||||
"3 months": "3 Monate",
|
||||
"6 months": "6 Monate",
|
||||
"1 year": "1 Jahr"
|
||||
}
|
||||
|
||||
@@ -11,25 +11,38 @@
|
||||
"Show all": "Show all",
|
||||
"Select device": "Select device",
|
||||
"Distance travelled": "Distance travelled",
|
||||
"Download raw data": "Download raw data",
|
||||
"Elevation gain / loss": "Elevation gain / loss",
|
||||
"Information": "Information",
|
||||
"Show last known locations": "Show last known locations",
|
||||
"Show location history (line)": "Show location history (line)",
|
||||
"Show location history (points)": "Show location history (points)",
|
||||
"Show location heatmap": "Show location heatmap",
|
||||
"Show points of interest": "Show points of interest",
|
||||
"Minify JSON": "Minify JSON",
|
||||
"Copy to clipboard": "Copy to clipboard",
|
||||
"Download": "Download",
|
||||
"Loading version...": "Loading version...",
|
||||
"OwnTracks website": "OwnTracks website",
|
||||
"OwnTracks documentation": "OwnTracks documentation",
|
||||
"OwnTracks on Twitter": "OwnTracks on Twitter",
|
||||
"OwnTracks on Mastodon": "OwnTracks on Mastodon",
|
||||
"Loading data, please wait...": "Loading data, please wait...",
|
||||
"Cancel": "Cancel",
|
||||
"Image of {deviceName}": "Image of {deviceName}",
|
||||
"Timestamp": "Timestamp",
|
||||
"Location": "Location",
|
||||
"Address": "Address",
|
||||
"Battery": "Battery",
|
||||
"Speed": "Speed",
|
||||
"Regions:": "Regions:"
|
||||
"Regions:": "Regions:",
|
||||
"WiFi": "WiFi",
|
||||
"Select date": "Select date",
|
||||
"Select time": "Select time",
|
||||
"Today": "Today",
|
||||
"Yesterday": "Yesterday",
|
||||
"3 days": "3 days",
|
||||
"7 days": "7 days",
|
||||
"15 days": "15 days",
|
||||
"30 days": "30 days",
|
||||
"3 months": "3 months",
|
||||
"6 months": "6 months",
|
||||
"1 year": "1 year"
|
||||
}
|
||||
|
||||
@@ -11,25 +11,38 @@
|
||||
"Show all": "Show all",
|
||||
"Select device": "Select device",
|
||||
"Distance travelled": "Distance traveled",
|
||||
"Download raw data": "Download raw data",
|
||||
"Elevation gain / loss": "Elevation gain / loss",
|
||||
"Information": "Information",
|
||||
"Show last known locations": "Show last known locations",
|
||||
"Show location history (line)": "Show location history (line)",
|
||||
"Show location history (points)": "Show location history (points)",
|
||||
"Show location heatmap": "Show location heatmap",
|
||||
"Show points of interest": "Show points of interest",
|
||||
"Minify JSON": "Minify JSON",
|
||||
"Copy to clipboard": "Copy to clipboard",
|
||||
"Download": "Download",
|
||||
"Loading version...": "Loading version...",
|
||||
"OwnTracks website": "OwnTracks website",
|
||||
"OwnTracks documentation": "OwnTracks documentation",
|
||||
"OwnTracks on Twitter": "OwnTracks on Twitter",
|
||||
"OwnTracks on Mastodon": "OwnTracks on Mastodon",
|
||||
"Loading data, please wait...": "Loading data, please wait...",
|
||||
"Cancel": "Cancel",
|
||||
"Image of {deviceName}": "Image of {deviceName}",
|
||||
"Timestamp": "Timestamp",
|
||||
"Location": "Location",
|
||||
"Address": "Address",
|
||||
"Battery": "Battery",
|
||||
"Speed": "Speed",
|
||||
"Regions:": "Regions:"
|
||||
"Regions:": "Regions:",
|
||||
"WiFi": "WiFi",
|
||||
"Select date": "Select date",
|
||||
"Select time": "Select time",
|
||||
"Today": "Today",
|
||||
"Yesterday": "Yesterday",
|
||||
"3 days": "3 days",
|
||||
"7 days": "7 days",
|
||||
"15 days": "15 days",
|
||||
"30 days": "30 days",
|
||||
"3 months": "3 months",
|
||||
"6 months": "6 months",
|
||||
"1 year": "1 year"
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"Show all": "Mostrar todos",
|
||||
"Select device": "Seleccionar dispositivo",
|
||||
"Distance travelled": "Distancia recorrida",
|
||||
"Download raw data": "Descargar datos en crudo",
|
||||
"Elevation gain / loss": "Aumento / disminución de la altura",
|
||||
"Information": "Información",
|
||||
"Show last known locations": "Mostrar última ubicación conocida",
|
||||
"Show location history (line)": "Mostrar historial (línea)",
|
||||
@@ -19,17 +19,27 @@
|
||||
"Show location heatmap": "Mostra mapa de calor",
|
||||
"Minify JSON": "Reducir JSON",
|
||||
"Copy to clipboard": "Copiar al portapapeles",
|
||||
"Download": "Descarga",
|
||||
"Loading version...": "Cargando versión...",
|
||||
"OwnTracks website": "OwnTracks - Sitio web",
|
||||
"OwnTracks documentation": "OwnTracks - documentación",
|
||||
"OwnTracks on Twitter": "OwnTracks en Twitter",
|
||||
"OwnTracks on Mastodon": "OwnTracks en Mastodon",
|
||||
"Loading data, please wait...": "Cargando datos, por favor, espera...",
|
||||
"Cancel": "Cancelar",
|
||||
"Image of {deviceName}": "Imágen de {deviceName}",
|
||||
"Timestamp": "Fecha / Hora",
|
||||
"Location": "Ubicación",
|
||||
"Address": "Dirección",
|
||||
"Battery": "Bateria",
|
||||
"Speed": "Velocidad",
|
||||
"Regions:": "Regiones:"
|
||||
"Regions:": "Regiones:",
|
||||
"WiFi": "WiFi",
|
||||
"Today": "Hoy",
|
||||
"Yesterday": "Ayer",
|
||||
"3 days": "3 días",
|
||||
"7 days": "7 días",
|
||||
"15 days": "15 días",
|
||||
"30 days": "30 días",
|
||||
"3 months": "3 meses",
|
||||
"6 months": "6 meses",
|
||||
"1 year": "1 año"
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"Show all": "Tout afficher",
|
||||
"Select device": "Sélectionner un appareil",
|
||||
"Distance travelled": "Distance parcourue",
|
||||
"Download raw data": "Télécharger les données brutes",
|
||||
"Elevation gain / loss": "Augmentation / diminution de l'altitude",
|
||||
"Information": "Informations",
|
||||
"Show last known locations": "Afficher les dernières localisations connues",
|
||||
"Show location history (line)": "Afficher l'historique de localisation (lignes)",
|
||||
@@ -19,17 +19,27 @@
|
||||
"Show location heatmap": "Afficher la carte de fréquentation",
|
||||
"Minify JSON": "Minifier JSON",
|
||||
"Copy to clipboard": "Copier dans le presse-papier",
|
||||
"Download": "Télécharger",
|
||||
"Loading version...": "Chargement de la version...",
|
||||
"OwnTracks website": "Site d'OwnTracks",
|
||||
"OwnTracks documentation": "Documentation d'OwnTracks",
|
||||
"OwnTracks on Twitter": "OwnTracks sur Twitter",
|
||||
"OwnTracks on Mastodon": "OwnTracks sur Mastodon",
|
||||
"Loading data, please wait...": "Chargement des données, merci de patienter ...",
|
||||
"Cancel": "Annuler",
|
||||
"Image of {deviceName}": "Image de {deviceName}",
|
||||
"Timestamp": "Horodatage",
|
||||
"Location": "Localisation",
|
||||
"Address": "Addresse",
|
||||
"Battery": "Batterie",
|
||||
"Speed": "Vitesse",
|
||||
"Regions:": "Régions:"
|
||||
"Regions:": "Régions:",
|
||||
"WiFi": "WiFi",
|
||||
"Today": "Aujourd'hui",
|
||||
"Yesterday": "Hier",
|
||||
"3 days": "3 jours",
|
||||
"7 days": "7 jours",
|
||||
"15 days": "15 jours",
|
||||
"30 days": "30 jours",
|
||||
"3 months": "3 mois",
|
||||
"6 months": "6 mois",
|
||||
"1 year": "1 an"
|
||||
}
|
||||
|
||||
45
src/locales/sk-SK.json
Normal file
45
src/locales/sk-SK.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"Automatically center the map view and zoom in to relevant data": "Automaticky vystrediť pohľad na mapu a priblížiť na príslušné údaje",
|
||||
"Fit view": "Napasovať pohľad",
|
||||
"Layer settings": "Nastavenia vrstvy",
|
||||
"Show/hide layers": "Zobraziť/skryť vrstvy",
|
||||
"Now": "Teraz",
|
||||
"Select start date": "Zvoliť začiatočný dátum",
|
||||
"to": "do",
|
||||
"Select end date": "Zvoliť konečný dátum",
|
||||
"Select user": "Zvoliť používateľa",
|
||||
"Show all": "Zobraziť všetko",
|
||||
"Select device": "Zvoliť zariadenie",
|
||||
"Distance travelled": "Prejdená vzdialenosť",
|
||||
"Elevation gain / loss": "Výškový výstup / pokles",
|
||||
"Information": "Informácie",
|
||||
"Show last known locations": "Zobraziť posledné známe polohy",
|
||||
"Show location history (line)": "Zobraziť históriu polôh (čiara)",
|
||||
"Show location history (points)": "Zobraziť históriu polôh (body)",
|
||||
"Show location heatmap": "Zobraziť tepelnú mapu polôh",
|
||||
"Minify JSON": "Zostručniť JSON",
|
||||
"Copy to clipboard": "Skopírovať do schránky",
|
||||
"Loading version...": "Nahrávam verziu...",
|
||||
"OwnTracks website": "Web Stránka OwnTracks",
|
||||
"OwnTracks documentation": "Dokumentácia OwnTracks",
|
||||
"OwnTracks on Mastodon": "OwnTracks na Mastodon",
|
||||
"Loading data, please wait...": "Nahrávam údaje, prosím počkajte...",
|
||||
"Cancel": "Zrušiť",
|
||||
"Image of {deviceName}": "Obrázok {deviceName}",
|
||||
"Timestamp": "Časová pečiatka",
|
||||
"Location": "Poloha",
|
||||
"Address": "Adresa",
|
||||
"Battery": "Batéria",
|
||||
"Speed": "Rýchlosť",
|
||||
"Regions:": "Oblasti:",
|
||||
"WiFi": "WiFi",
|
||||
"Today": "Dnes",
|
||||
"Yesterday": "Včera",
|
||||
"3 days": "3 dni",
|
||||
"7 days": "7 dní",
|
||||
"15 days": "15 dní",
|
||||
"30 days": "30 dní",
|
||||
"3 months": "3 mesiace",
|
||||
"6 months": "6 mesiacov",
|
||||
"1 year": "1 rok"
|
||||
}
|
||||
48
src/locales/tr-TR.json
Normal file
48
src/locales/tr-TR.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"Automatically center the map view and zoom in to relevant data": "Harita görünümünü otomatik olarak ortalayın ve ilgili verileri yakınlaştırın",
|
||||
"Fit view": "Görünümü sığdır",
|
||||
"Layer settings": "Katman ayarları",
|
||||
"Show/hide layers": "Katmanları göster/gizle",
|
||||
"Now": "Şimdi",
|
||||
"Select start date": "Başlangıç tarihini seçin",
|
||||
"to": "ile",
|
||||
"Select end date": "Bitiş tarihini seçin",
|
||||
"Select user": "Kullanıcı seç",
|
||||
"Show all": "Tümünü göster",
|
||||
"Select device": "Cihaz Seç",
|
||||
"Distance travelled": "Gidilen mesafe",
|
||||
"Elevation gain / loss": "Yükseklik kazancı / kaybı",
|
||||
"Information": "Bilgi",
|
||||
"Show last known locations": "Bilinen son yerleri göster",
|
||||
"Show location history (line)": "Konum geçmişini göster (çizgi)",
|
||||
"Show location history (points)": "Konum geçmişini göster (nokta)",
|
||||
"Show location heatmap": "Konum ısı haritasını göster",
|
||||
"Show points of interest": "İlgi çekici noktaları göster",
|
||||
"Minify JSON": "JSON'u Küçült",
|
||||
"Copy to clipboard": "Panoya kopyala",
|
||||
"Loading version...": "Versiyon yükleniyor...",
|
||||
"OwnTracks website": "OwnTracks internet sitesi",
|
||||
"OwnTracks documentation": "OwnTracks dokümanı",
|
||||
"OwnTracks on Mastodon": "Mastodon'da OwnTracks",
|
||||
"Loading data, please wait...": "Veriler yükleniyor, lüften bekleyin...",
|
||||
"Cancel": "İptal",
|
||||
"Image of {deviceName}": "{deviceName} resmi",
|
||||
"Timestamp": "Zaman Damgası",
|
||||
"Location": "Konum",
|
||||
"Address": "Adres",
|
||||
"Battery": "Batarya",
|
||||
"Speed": "Hız",
|
||||
"Regions:": "Bölgeler:",
|
||||
"WiFi": "WiFi",
|
||||
"Select date": "Tarih seç",
|
||||
"Select time": "Saat seç",
|
||||
"Today": "Bugün",
|
||||
"Yesterday": "Dün",
|
||||
"3 days": "3 gün",
|
||||
"7 days": "7 gün",
|
||||
"15 days": "15 gün",
|
||||
"30 days": "30 gün",
|
||||
"3 months": "3 ay",
|
||||
"6 months": "6 ay",
|
||||
"1 year": "1 yıl"
|
||||
}
|
||||
@@ -122,9 +122,11 @@ const getLastLocations = async ({ commit, state }) => {
|
||||
commit(types.SET_LAST_LOCATIONS, lastLocations);
|
||||
};
|
||||
|
||||
const _getDistanceTravelled = (locationHistory) => {
|
||||
const _getTravelStats = (locationHistory) => {
|
||||
const start = Date.now();
|
||||
let distanceTravelled = 0;
|
||||
let elevationGain = 0;
|
||||
let elevationLoss = 0;
|
||||
Object.keys(locationHistory).forEach((user) => {
|
||||
Object.keys(locationHistory[user]).forEach((device) => {
|
||||
let lastLatLng = null;
|
||||
@@ -134,20 +136,21 @@ const _getDistanceTravelled = (locationHistory) => {
|
||||
location.acc > config.filters.minAccuracy
|
||||
)
|
||||
return;
|
||||
const latLng = L.latLng(location.lat, location.lon);
|
||||
const latLng = L.latLng(location.lat, location.lon, location.alt ?? 0);
|
||||
if (lastLatLng !== null) {
|
||||
const distance = distanceBetweenCoordinates(lastLatLng, latLng);
|
||||
const elevationChange = latLng.alt - lastLatLng.alt;
|
||||
if (
|
||||
typeof config.map.maxPointDistance === "number" &&
|
||||
config.map.maxPointDistance > 0
|
||||
? // If part of the current group, add to total
|
||||
distance <= config.map.maxPointDistance
|
||||
: // If grouping is disabled, always add to total
|
||||
true
|
||||
) {
|
||||
if (distance <= config.map.maxPointDistance) {
|
||||
// Part of the current group, add calculated distance to total
|
||||
distanceTravelled += distance;
|
||||
}
|
||||
} else {
|
||||
// If grouping is disabled always add calculated distance to total
|
||||
distanceTravelled += distance;
|
||||
if (elevationChange >= 0) elevationGain += elevationChange;
|
||||
else elevationLoss += -elevationChange;
|
||||
}
|
||||
}
|
||||
lastLatLng = latLng;
|
||||
@@ -155,15 +158,15 @@ const _getDistanceTravelled = (locationHistory) => {
|
||||
});
|
||||
});
|
||||
const end = Date.now();
|
||||
log("DISTANCE", () => {
|
||||
log("PERFORMANCE", () => {
|
||||
const locationHistoryCount = getLocationHistoryCount(locationHistory);
|
||||
const duration = (end - start) / 1000;
|
||||
return (
|
||||
`[_getDistanceTravelled] Took ${duration} seconds to ` +
|
||||
`calculate distance of ${locationHistoryCount} locations`
|
||||
`[_getTravelStats] Took ${duration} seconds to calculate distance ` +
|
||||
`and elevation gain/loss of ${locationHistoryCount} locations`
|
||||
);
|
||||
});
|
||||
return distanceTravelled;
|
||||
return { distanceTravelled, elevationGain, elevationLoss };
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -181,18 +184,28 @@ const getLocationHistory = async ({ commit, state }) => {
|
||||
} else {
|
||||
devices = state.devices;
|
||||
}
|
||||
const locationHistory = await api.getLocationHistory(
|
||||
devices,
|
||||
state.startDateTime,
|
||||
state.endDateTime
|
||||
);
|
||||
commit(types.SET_IS_LOADING, false);
|
||||
commit(types.SET_REQUEST_ABORT_CONTROLLER, new AbortController());
|
||||
let locationHistory;
|
||||
try {
|
||||
locationHistory = await api.getLocationHistory(
|
||||
devices,
|
||||
state.startDateTime,
|
||||
state.endDateTime,
|
||||
{ signal: state.requestAbortController.signal }
|
||||
);
|
||||
} catch (error) {
|
||||
return;
|
||||
} finally {
|
||||
commit(types.SET_REQUEST_ABORT_CONTROLLER, null);
|
||||
commit(types.SET_IS_LOADING, false);
|
||||
}
|
||||
commit(types.SET_LOCATION_HISTORY, locationHistory);
|
||||
if (config.showDistanceTravelled) {
|
||||
commit(
|
||||
types.SET_DISTANCE_TRAVELLED,
|
||||
_getDistanceTravelled(locationHistory)
|
||||
);
|
||||
const { distanceTravelled, elevationGain, elevationLoss } =
|
||||
_getTravelStats(locationHistory);
|
||||
commit(types.SET_DISTANCE_TRAVELLED, distanceTravelled);
|
||||
commit(types.SET_ELEVATION_GAIN, elevationGain);
|
||||
commit(types.SET_ELEVATION_LOSS, elevationLoss);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ Vue.use(Vuex);
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
isLoading: false,
|
||||
frontendVersion: process.env.PACKAGE_VERSION,
|
||||
frontendVersion: import.meta.env.PACKAGE_VERSION,
|
||||
recorderVersion: "",
|
||||
users: [],
|
||||
devices: {},
|
||||
@@ -30,7 +30,10 @@ export default new Vuex.Store({
|
||||
zoom: 19,
|
||||
layers: config.map.layers,
|
||||
},
|
||||
distanceTravelled: null,
|
||||
distanceTravelled: 0,
|
||||
elevationGain: 0,
|
||||
elevationLoss: 0,
|
||||
requestAbortController: null,
|
||||
},
|
||||
getters,
|
||||
mutations,
|
||||
|
||||
@@ -12,3 +12,6 @@ export const SET_MAP_CENTER = "SET_MAP_CENTER";
|
||||
export const SET_MAP_ZOOM = "SET_MAP_ZOOM";
|
||||
export const SET_MAP_LAYER_VISIBILITY = "SET_MAP_LAYER_VISIBILITY";
|
||||
export const SET_DISTANCE_TRAVELLED = "SET_DISTANCE_TRAVELLED";
|
||||
export const SET_ELEVATION_GAIN = "SET_ELEVATION_GAIN";
|
||||
export const SET_ELEVATION_LOSS = "SET_ELEVATION_LOSS";
|
||||
export const SET_REQUEST_ABORT_CONTROLLER = "SET_REQUEST_ABORT_CONTROLLER";
|
||||
|
||||
@@ -43,4 +43,13 @@ export default {
|
||||
[types.SET_DISTANCE_TRAVELLED](state, distanceTravelled) {
|
||||
state.distanceTravelled = distanceTravelled;
|
||||
},
|
||||
[types.SET_ELEVATION_GAIN](state, elevationGain) {
|
||||
state.elevationGain = elevationGain;
|
||||
},
|
||||
[types.SET_ELEVATION_LOSS](state, elevationLoss) {
|
||||
state.elevationLoss = elevationLoss;
|
||||
},
|
||||
[types.SET_REQUEST_ABORT_CONTROLLER](state, requestAbortController) {
|
||||
state.requestAbortController = requestAbortController;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
--pin-width: 32px;
|
||||
}
|
||||
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -85,7 +86,8 @@ pre {
|
||||
display: block;
|
||||
font-family: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console",
|
||||
"Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono",
|
||||
"Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace;
|
||||
"Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier,
|
||||
monospace;
|
||||
overflow-x: auto;
|
||||
|
||||
code {
|
||||
@@ -99,8 +101,7 @@ pre {
|
||||
min-height: 100%;
|
||||
flex-direction: column;
|
||||
|
||||
// Only select immediate child as the datepicker contains a <header> as well
|
||||
> header {
|
||||
header {
|
||||
display: flex;
|
||||
padding: 20px;
|
||||
white-space: nowrap;
|
||||
@@ -180,19 +181,33 @@ pre {
|
||||
}
|
||||
|
||||
> .button,
|
||||
> .dropdown,
|
||||
> .date-time-picker {
|
||||
> .mx-datepicker,
|
||||
> .mx-input,
|
||||
> .dropdown {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
> .dropdown .dropdown-button,
|
||||
> .date-time-picker .dropdown-button {
|
||||
> .dropdown .dropdown-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> .date-time-picker {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
.mx-datepicker {
|
||||
display: flex;
|
||||
width: auto;
|
||||
|
||||
.mx-datepicker-range {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.mx-input-wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
.mx-input {
|
||||
font-size: 13px;
|
||||
letter-spacing: -0.6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> span {
|
||||
@@ -289,7 +304,9 @@ pre {
|
||||
|
||||
&.button-outline,
|
||||
&.button-flat {
|
||||
transition: background-color 0.2s, box-shadow 0.2s;
|
||||
transition:
|
||||
background-color 0.2s,
|
||||
box-shadow 0.2s;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
|
||||
@@ -1,10 +1,45 @@
|
||||
.date-time-picker {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
.mx-datepicker {
|
||||
width: 280px;
|
||||
|
||||
.datepicker {
|
||||
box-shadow: none !important;
|
||||
filter: var(--drop-shadow);
|
||||
margin-top: 5px;
|
||||
.mx-input {
|
||||
border: 0;
|
||||
border-radius: 18px;
|
||||
text-align: center;
|
||||
height: 33px;
|
||||
padding-right: 0px;
|
||||
padding-left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
.mx-datepicker-main {
|
||||
display: flex;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.mx-datepicker-sidebar {
|
||||
flex: 0.7;
|
||||
}
|
||||
|
||||
.mx-datepicker-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx-time {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.toggle-date-btn {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.mx-icon-calendar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -15,17 +15,24 @@
|
||||
|
||||
.leaflet-popup-content {
|
||||
margin: 30px;
|
||||
|
||||
.face {
|
||||
width: 40px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.leaflet-popup-close-button {
|
||||
color: inherit;
|
||||
width: auto;
|
||||
height: auto;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-top: 15px;
|
||||
margin-right: 15px;
|
||||
border-radius: 100px;
|
||||
border-radius: 100%;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover,
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
.v--modal-overlay {
|
||||
background: rgba(0, 0, 0, 0.5) !important;
|
||||
|
||||
.v--modal-background-click {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.v--modal-box.v--modal {
|
||||
top: initial !important;
|
||||
left: initial !important;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
max-width: 95vw;
|
||||
max-height: 95vh;
|
||||
overflow: auto;
|
||||
padding: 30px;
|
||||
border-radius: 3px;
|
||||
background: var(--color-background);
|
||||
}
|
||||
.vm--container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.vm--overlay {
|
||||
background: rgba(0, 0, 0, 0.5) !important;
|
||||
}
|
||||
.vm--modal {
|
||||
top: initial !important;
|
||||
left: initial !important;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
max-width: 95vw;
|
||||
max-height: 95vh;
|
||||
overflow: auto;
|
||||
padding: 30px;
|
||||
border-radius: 3px;
|
||||
background: var(--color-background);
|
||||
}
|
||||
}
|
||||
|
||||
18
src/util.js
18
src/util.js
@@ -64,24 +64,6 @@ export const distanceBetweenCoordinates = (c1, c2) => {
|
||||
return d;
|
||||
};
|
||||
|
||||
/**
|
||||
* Let the user download a string as file.
|
||||
*
|
||||
* @param {String} text Content of the file
|
||||
* @param {String} filename Suggested filename for the browser
|
||||
* @param {String} [mimeType] Content mime type
|
||||
*/
|
||||
export const download = (text, filename, mimeType = "text/plain") => {
|
||||
const dataUrl = `data:${mimeType},${encodeURIComponent(text)}`;
|
||||
const element = document.createElement("a");
|
||||
element.href = dataUrl;
|
||||
element.download = filename;
|
||||
element.style.display = "none";
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a distance in meters into a human-readable string with unit.
|
||||
*
|
||||
|
||||
@@ -14,16 +14,73 @@
|
||||
<LControlScale
|
||||
v-if="controls.scale.display"
|
||||
:position="controls.scale.position"
|
||||
:maxWidth="controls.scale.maxWidth"
|
||||
:max-width="controls.scale.maxWidth"
|
||||
:metric="controls.scale.metric"
|
||||
:imperial="controls.scale.imperial"
|
||||
/>
|
||||
<LTileLayer
|
||||
:url="url"
|
||||
:attribution="attribution"
|
||||
:options="{ maxNativeZoom, maxZoom }"
|
||||
:tile-size="tileSize"
|
||||
:options="{ maxNativeZoom, maxZoom, zoomOffset }"
|
||||
/>
|
||||
|
||||
<template v-if="map.layers.line">
|
||||
<LPolyline
|
||||
v-for="(group, i) in filteredLocationHistoryLatLngGroups"
|
||||
:key="i"
|
||||
:lat-lngs="group"
|
||||
v-bind="polyline"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-for="(userDevices, user) in filteredLocationHistory">
|
||||
<template v-for="(deviceLocations, device) in userDevices">
|
||||
<template
|
||||
v-for="(l, n) in deviceLocationsWithNameAndFace(
|
||||
user,
|
||||
device,
|
||||
deviceLocations
|
||||
)"
|
||||
>
|
||||
<LCircleMarker
|
||||
v-if="map.layers.poi && l.poi"
|
||||
:key="`${l.topic}-poi-${n}`"
|
||||
:lat-lng="[l.lat, l.lon]"
|
||||
v-bind="poiMarker"
|
||||
>
|
||||
<LTooltip :options="{ permanent: true }">
|
||||
{{ l.poi }}
|
||||
</LTooltip>
|
||||
</LCircleMarker>
|
||||
<LCircleMarker
|
||||
v-if="map.layers.points"
|
||||
:key="`${l.topic}-location-${n}`"
|
||||
:lat-lng="[l.lat, l.lon]"
|
||||
v-bind="circleMarker"
|
||||
>
|
||||
<LDeviceLocationPopup
|
||||
:user="user"
|
||||
:device="device"
|
||||
:name="l.name"
|
||||
:face="l.face"
|
||||
:timestamp="l.tst"
|
||||
:iso-local="l.isolocal"
|
||||
:time-zone="l.tzname"
|
||||
:lat="l.lat"
|
||||
:lon="l.lon"
|
||||
:alt="l.alt"
|
||||
:battery="l.batt"
|
||||
:speed="l.vel"
|
||||
:regions="l.inregions"
|
||||
:wifi="{ ssid: l.SSID, bssid: l.BSSID }"
|
||||
:address="l.addr"
|
||||
></LDeviceLocationPopup>
|
||||
</LCircleMarker>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-if="map.layers.last">
|
||||
<LCircle
|
||||
v-for="l in lastLocations"
|
||||
@@ -45,57 +102,21 @@
|
||||
:name="l.name"
|
||||
:face="l.face"
|
||||
:timestamp="l.tst"
|
||||
:iso-local="l.isolocal"
|
||||
:time-zone="l.tzname"
|
||||
:lat="l.lat"
|
||||
:lon="l.lon"
|
||||
:alt="l.alt"
|
||||
:battery="l.batt"
|
||||
:speed="l.vel"
|
||||
:regions="l.inregions"
|
||||
:options="{ className: 'leaflet-popup--for-pin' }"
|
||||
:wifi="{ ssid: l.SSID, bssid: l.BSSID }"
|
||||
:options="{ className: 'leaflet-popup--for-pin', maxWidth: 400 }"
|
||||
:address="l.addr"
|
||||
/>
|
||||
</LMarker>
|
||||
</template>
|
||||
|
||||
<template v-if="map.layers.line">
|
||||
<LPolyline
|
||||
v-for="(group, i) in filteredLocationHistoryLatLngGroups"
|
||||
:key="i"
|
||||
:lat-lngs="group"
|
||||
v-bind="polyline"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-if="map.layers.points">
|
||||
<template v-for="(userDevices, user) in filteredLocationHistory">
|
||||
<template v-for="(deviceLocations, device) in userDevices">
|
||||
<LCircleMarker
|
||||
v-for="(l, n) in deviceLocationsWithNameAndFace(
|
||||
user,
|
||||
device,
|
||||
deviceLocations
|
||||
)"
|
||||
:key="`${user}-${device}-${n}`"
|
||||
:lat-lng="[l.lat, l.lon]"
|
||||
v-bind="circleMarker"
|
||||
>
|
||||
<LDeviceLocationPopup
|
||||
:user="user"
|
||||
:device="device"
|
||||
:name="l.name"
|
||||
:face="l.face"
|
||||
:timestamp="l.tst"
|
||||
:lat="l.lat"
|
||||
:lon="l.lon"
|
||||
:alt="l.alt"
|
||||
:battery="l.batt"
|
||||
:speed="l.vel"
|
||||
:regions="l.inregions"
|
||||
></LDeviceLocationPopup>
|
||||
</LCircleMarker>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-if="map.layers.heatmap">
|
||||
<LHeatmap
|
||||
v-if="filteredLocationHistoryLatLngs.length"
|
||||
@@ -121,12 +142,13 @@ import {
|
||||
LCircleMarker,
|
||||
LCircle,
|
||||
LPolyline,
|
||||
LTooltip,
|
||||
} from "vue2-leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import * as types from "@/store/mutation-types";
|
||||
import LCustomMarker from "@/components/LCustomMarker";
|
||||
import LHeatmap from "@/components/LHeatmap";
|
||||
import LDeviceLocationPopup from "@/components/LDeviceLocationPopup";
|
||||
import LHeatmap from "@/components/LHeatmap.vue";
|
||||
import LDeviceLocationPopup from "@/components/LDeviceLocationPopup.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -140,6 +162,7 @@ export default {
|
||||
LPolyline,
|
||||
LDeviceLocationPopup,
|
||||
LHeatmap,
|
||||
LTooltip,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -150,8 +173,10 @@ export default {
|
||||
markerIcon: LCustomMarker,
|
||||
maxZoom: this.$config.map.maxZoom,
|
||||
maxNativeZoom: this.$config.map.maxNativeZoom,
|
||||
tileSize: this.$config.map.tileSize,
|
||||
url: this.$config.map.url,
|
||||
zoom: this.$store.state.map.zoom,
|
||||
zoomOffset: this.$config.map.zoomOffset,
|
||||
circle: {
|
||||
...this.$config.map.circle,
|
||||
color: this.$config.map.circle.color || this.$config.primaryColor,
|
||||
@@ -162,17 +187,13 @@ export default {
|
||||
...this.$config.map.circleMarker,
|
||||
color: this.$config.map.circleMarker.color || this.$config.primaryColor,
|
||||
},
|
||||
poiMarker: this.$config.map.poiMarker,
|
||||
polyline: {
|
||||
...this.$config.map.polyline,
|
||||
color: this.$config.map.polyline.color || this.$config.primaryColor,
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$root.$on("fitView", () => {
|
||||
this.fitView();
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
"filteredLocationHistory",
|
||||
@@ -181,6 +202,21 @@ export default {
|
||||
]),
|
||||
...mapState(["lastLocations", "map"]),
|
||||
},
|
||||
watch: {
|
||||
lastLocations() {
|
||||
if (this.$config.onLocationChange.fitView) {
|
||||
this.fitView();
|
||||
}
|
||||
},
|
||||
filteredLocationHistory() {
|
||||
this.fitView();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$root.$on("fitView", () => {
|
||||
this.fitView();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
...mapMutations({
|
||||
setMapCenter: types.SET_MAP_CENTER,
|
||||
@@ -193,6 +229,7 @@ export default {
|
||||
if (
|
||||
(this.map.layers.line ||
|
||||
this.map.layers.points ||
|
||||
this.map.layers.poi ||
|
||||
this.map.layers.heatmap) &&
|
||||
this.filteredLocationHistoryLatLngs.length > 0
|
||||
) {
|
||||
@@ -230,15 +267,5 @@ export default {
|
||||
}));
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
lastLocations() {
|
||||
if (this.$config.onLocationChange.fitView) {
|
||||
this.fitView();
|
||||
}
|
||||
},
|
||||
filteredLocationHistory() {
|
||||
this.fitView();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
jest: true,
|
||||
},
|
||||
};
|
||||
@@ -1,32 +1,42 @@
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import createFetchMock from "vitest-fetch-mock";
|
||||
|
||||
import * as api from "@/api";
|
||||
|
||||
const fetchMocker = createFetchMock(vi);
|
||||
|
||||
describe("API", () => {
|
||||
beforeEach(() => {
|
||||
fetch.resetMocks();
|
||||
fetchMocker.enableMocks();
|
||||
fetchMocker.resetMocks();
|
||||
});
|
||||
|
||||
test("getVersion", async () => {
|
||||
fetch.mockResponse(JSON.stringify({ version: "1.2.3" }));
|
||||
fetchMocker.mockResponse(JSON.stringify({ version: "1.2.3" }));
|
||||
|
||||
const version = await api.getVersion();
|
||||
expect(version).toBe("1.2.3");
|
||||
|
||||
expect(fetch.mock.calls.length).toEqual(1);
|
||||
expect(fetch.mock.calls[0][0]).toEqual("http://localhost/api/0/version");
|
||||
expect(fetchMocker.mock.calls.length).toEqual(1);
|
||||
expect(fetchMocker.mock.calls[0][0]).toEqual(
|
||||
"http://localhost:3000/api/0/version"
|
||||
);
|
||||
});
|
||||
|
||||
test("getUsers", async () => {
|
||||
fetch.mockResponse(JSON.stringify({ results: ["foo", "bar"] }));
|
||||
fetchMocker.mockResponse(JSON.stringify({ results: ["foo", "bar"] }));
|
||||
|
||||
const users = await api.getUsers();
|
||||
expect(users).toEqual(["foo", "bar"]);
|
||||
|
||||
expect(fetch.mock.calls.length).toEqual(1);
|
||||
expect(fetch.mock.calls[0][0]).toEqual("http://localhost/api/0/list");
|
||||
expect(fetchMocker.mock.calls.length).toEqual(1);
|
||||
expect(fetchMocker.mock.calls[0][0]).toEqual(
|
||||
"http://localhost:3000/api/0/list"
|
||||
);
|
||||
});
|
||||
|
||||
test("getDevices", async () => {
|
||||
fetch.mockResponses(
|
||||
fetchMocker.mockResponses(
|
||||
[JSON.stringify({ results: ["phone", "tablet"] })],
|
||||
[JSON.stringify({ results: ["laptop"] })]
|
||||
);
|
||||
@@ -34,12 +44,12 @@ describe("API", () => {
|
||||
const devices = await api.getDevices(["foo", "bar"]);
|
||||
expect(devices).toEqual({ foo: ["phone", "tablet"], bar: ["laptop"] });
|
||||
|
||||
expect(fetch.mock.calls.length).toEqual(2);
|
||||
expect(fetch.mock.calls[0][0]).toEqual(
|
||||
"http://localhost/api/0/list?user=foo"
|
||||
expect(fetchMocker.mock.calls.length).toEqual(2);
|
||||
expect(fetchMocker.mock.calls[0][0]).toEqual(
|
||||
"http://localhost:3000/api/0/list?user=foo"
|
||||
);
|
||||
expect(fetch.mock.calls[1][0]).toEqual(
|
||||
"http://localhost/api/0/list?user=bar"
|
||||
expect(fetchMocker.mock.calls[1][0]).toEqual(
|
||||
"http://localhost:3000/api/0/list?user=bar"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -60,13 +70,15 @@ describe("API", () => {
|
||||
disptst: "1970-01-01 00:00:00",
|
||||
},
|
||||
];
|
||||
fetch.mockResponse(JSON.stringify(response));
|
||||
fetchMocker.mockResponse(JSON.stringify(response));
|
||||
|
||||
const lastLocation = await api.getLastLocations();
|
||||
expect(lastLocation).toEqual(response);
|
||||
|
||||
expect(fetch.mock.calls.length).toEqual(1);
|
||||
expect(fetch.mock.calls[0][0]).toEqual("http://localhost/api/0/last");
|
||||
expect(fetchMocker.mock.calls.length).toEqual(1);
|
||||
expect(fetchMocker.mock.calls[0][0]).toEqual(
|
||||
"http://localhost:3000/api/0/last"
|
||||
);
|
||||
});
|
||||
|
||||
test("getLastLocations with user", async () => {
|
||||
@@ -81,14 +93,14 @@ describe("API", () => {
|
||||
device: "tablet",
|
||||
},
|
||||
];
|
||||
fetch.mockResponse(JSON.stringify(response));
|
||||
fetchMocker.mockResponse(JSON.stringify(response));
|
||||
|
||||
const lastLocation = await api.getLastLocations("foo");
|
||||
expect(lastLocation).toEqual(response);
|
||||
|
||||
expect(fetch.mock.calls.length).toEqual(1);
|
||||
expect(fetch.mock.calls[0][0]).toEqual(
|
||||
"http://localhost/api/0/last?user=foo"
|
||||
expect(fetchMocker.mock.calls.length).toEqual(1);
|
||||
expect(fetchMocker.mock.calls[0][0]).toEqual(
|
||||
"http://localhost:3000/api/0/last?user=foo"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -100,14 +112,14 @@ describe("API", () => {
|
||||
device: "phone",
|
||||
},
|
||||
];
|
||||
fetch.mockResponse(JSON.stringify(response));
|
||||
fetchMocker.mockResponse(JSON.stringify(response));
|
||||
|
||||
const lastLocation = await api.getLastLocations("foo", "phone");
|
||||
expect(lastLocation).toEqual(response);
|
||||
|
||||
expect(fetch.mock.calls.length).toEqual(1);
|
||||
expect(fetch.mock.calls[0][0]).toEqual(
|
||||
"http://localhost/api/0/last?user=foo&device=phone"
|
||||
expect(fetchMocker.mock.calls.length).toEqual(1);
|
||||
expect(fetchMocker.mock.calls[0][0]).toEqual(
|
||||
"http://localhost:3000/api/0/last?user=foo&device=phone"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -137,7 +149,7 @@ describe("API", () => {
|
||||
],
|
||||
status: 200,
|
||||
};
|
||||
fetch.mockResponse(JSON.stringify(response));
|
||||
fetchMocker.mockResponse(JSON.stringify(response));
|
||||
|
||||
const locationHistory = await api.getUserDeviceLocationHistory(
|
||||
"foo",
|
||||
@@ -147,14 +159,14 @@ describe("API", () => {
|
||||
);
|
||||
expect(locationHistory).toEqual(response.data);
|
||||
|
||||
expect(fetch.mock.calls.length).toEqual(1);
|
||||
expect(fetch.mock.calls[0][0]).toEqual(
|
||||
"http://localhost/api/0/locations?from=1970-01-01T00%3A00%3A00&to=1970-12-31T23%3A59%3A59&user=foo&device=phone&format=json"
|
||||
expect(fetchMocker.mock.calls.length).toEqual(1);
|
||||
expect(fetchMocker.mock.calls[0][0]).toEqual(
|
||||
"http://localhost:3000/api/0/locations?from=1970-01-01T00%3A00%3A00&to=1970-12-31T23%3A59%3A59&user=foo&device=phone&format=json"
|
||||
);
|
||||
});
|
||||
|
||||
test("getLocationHistory", async () => {
|
||||
fetch.mockResponses(
|
||||
fetchMocker.mockResponses(
|
||||
[
|
||||
JSON.stringify({
|
||||
count: 1,
|
||||
@@ -203,15 +215,15 @@ describe("API", () => {
|
||||
bar: { laptop: [{ topic: "owntracks/bar/laptop" }] },
|
||||
});
|
||||
|
||||
expect(fetch.mock.calls.length).toEqual(3);
|
||||
expect(fetch.mock.calls[0][0]).toEqual(
|
||||
"http://localhost/api/0/locations?from=1970-01-01T00%3A00%3A00&to=1970-12-31T23%3A59%3A59&user=foo&device=phone&format=json"
|
||||
expect(fetchMocker.mock.calls.length).toEqual(3);
|
||||
expect(fetchMocker.mock.calls[0][0]).toEqual(
|
||||
"http://localhost:3000/api/0/locations?from=1970-01-01T00%3A00%3A00&to=1970-12-31T23%3A59%3A59&user=foo&device=phone&format=json"
|
||||
);
|
||||
expect(fetch.mock.calls[1][0]).toEqual(
|
||||
"http://localhost/api/0/locations?from=1970-01-01T00%3A00%3A00&to=1970-12-31T23%3A59%3A59&user=foo&device=tablet&format=json"
|
||||
expect(fetchMocker.mock.calls[1][0]).toEqual(
|
||||
"http://localhost:3000/api/0/locations?from=1970-01-01T00%3A00%3A00&to=1970-12-31T23%3A59%3A59&user=foo&device=tablet&format=json"
|
||||
);
|
||||
expect(fetch.mock.calls[2][0]).toEqual(
|
||||
"http://localhost/api/0/locations?from=1970-01-01T00%3A00%3A00&to=1970-12-31T23%3A59%3A59&user=bar&device=laptop&format=json"
|
||||
expect(fetchMocker.mock.calls[2][0]).toEqual(
|
||||
"http://localhost:3000/api/0/locations?from=1970-01-01T00%3A00%3A00&to=1970-12-31T23%3A59%3A59&user=bar&device=laptop&format=json"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import config from "@/config";
|
||||
import {
|
||||
getApiUrl,
|
||||
@@ -10,9 +12,9 @@ import {
|
||||
describe("getApiUrl", () => {
|
||||
test("without base URL", () => {
|
||||
// See testURL in jest.config.js
|
||||
expect(getApiUrl("foo").href).toBe("http://localhost/foo");
|
||||
expect(getApiUrl("/foo").href).toBe("http://localhost/foo");
|
||||
expect(getApiUrl("/foo/bar").href).toBe("http://localhost/foo/bar");
|
||||
expect(getApiUrl("foo").href).toBe("http://localhost:3000/foo");
|
||||
expect(getApiUrl("/foo").href).toBe("http://localhost:3000/foo");
|
||||
expect(getApiUrl("/foo/bar").href).toBe("http://localhost:3000/foo/bar");
|
||||
});
|
||||
|
||||
test("with base URL", () => {
|
||||
|
||||
19
vite.config.js
Normal file
19
vite.config.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue2";
|
||||
import version from "vite-plugin-package-version";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: "",
|
||||
plugins: [vue(), version()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": resolve(dirname(fileURLToPath(import.meta.url)), "./src"),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
},
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
const fs = require("fs");
|
||||
const webpack = require("webpack");
|
||||
const MomentLocalesPlugin = require("moment-locales-webpack-plugin");
|
||||
|
||||
const packageJson = fs.readFileSync("./package.json");
|
||||
const version = JSON.parse(packageJson).version;
|
||||
|
||||
module.exports = {
|
||||
publicPath: "",
|
||||
configureWebpack: {
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
"process.env": {
|
||||
PACKAGE_VERSION: `"${version}"`,
|
||||
},
|
||||
}),
|
||||
new MomentLocalesPlugin(),
|
||||
],
|
||||
},
|
||||
|
||||
pluginOptions: {
|
||||
i18n: {
|
||||
locale: "en",
|
||||
fallbackLocale: "en",
|
||||
localeDir: "locales",
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user