34 Commits

Author SHA1 Message Date
Linus Groh
9e36d31997 Release 2.10.0 2021-11-28 18:04:58 +00:00
Linus Groh
5e37c7f4b8 Sort device location history entries by timestamp
Fixes #67.
2021-11-28 17:52:28 +00:00
Linus Groh
7dda60d457 Ensure all SCSS files are formatted with prettier 2021-11-28 17:33:32 +00:00
Linus Groh
228900ff9f Upgrade vue-js-modal to v2 2021-11-28 17:33:08 +00:00
Linus Groh
129446de1a Upgrade dependencies 2021-11-28 17:31:55 +00:00
Linus Groh
af6c308bd6 Rename master branch to main 2021-11-28 17:31:39 +00:00
Linus Groh
223e19a118 Upgrade dependencies 2021-08-30 16:02:53 +01:00
Linus Groh
1260814309 Upgrade dependencies 2021-08-30 16:02:53 +01:00
Andrew Rowson
cfffbe9472 Add trailing slashes to location blocks (#63)
On my configuration, having a trailing slash on `proxy_pass` but not on
the location block causes the sub-path after the `location` to be
appended to the `proxy_pass` url, causing a double-forward-slash.

E.g. going to `/api/0/list` actually ends up making a call to the proxy
of `http://otrecorder/api//0/list`, which fails for me.
2021-06-28 12:36:16 +01:00
Linus Groh
4031bda2f0 Reformat files with updated prettier 2021-05-28 17:37:35 +01:00
Linus Groh
69094e240e Upgrade dependencies 2021-05-28 17:37:08 +01:00
Linus Groh
dfa7a423fa Release 2.9.0 2021-05-01 22:46:02 +02:00
Linus Groh
411bc10b0b Upgrade dependencies 2021-05-01 22:32:17 +02:00
Linus Groh
a994051940 Upgrade dependencies 2021-04-28 20:30:06 +02:00
Linus Groh
d325543bc6 Replace "OwnTracks UI" with "OwnTracks Frontend"
"OwnTracks UI" was the name I initially used when developing this
separately, but since it's now known as the "OwnTracks frontend", let's
just call it that.
2021-03-24 19:00:35 +01:00
Linus Groh
80d3060fa8 Replace borales/actions-yarn with actions/setup-node 2021-03-24 18:26:52 +01:00
Linus Groh
e6c79ac606 Upgrade dependencies 2021-03-24 18:02:09 +01:00
Linus Groh
0b1271502f Upgrade dependencies 2021-02-24 20:18:56 +01:00
Linus Groh
fdddd8e035 Add cancel button to loading modal 2021-02-24 20:07:47 +01:00
Linus Groh
245c1295e5 Fix JSDoc return type annotations of async functions
These don't return the mentioned type directly, but a promise containing
this type, which has to be await'ed first. This was confusing my IDE's
tslint which suggested await would not be necessary.
2021-02-24 20:07:47 +01:00
Linus Groh
9786487646 Move .distance-travelled SCSS to scoped <style> 2021-02-24 20:07:47 +01:00
Linus Groh
b3529c211c Release 2.8.0 2021-02-19 18:57:19 +01:00
Linus Groh
55178c7cc8 Add "Elevation gain / loss" es-ES and fr-FR translations
Based on help from computers & friends, I'm sure both know better than I
do!
2021-02-19 18:54:27 +01:00
Linus Groh
2fcf2151fa Add elevation gain / loss to distance travelled stats
Closes #51.
2021-02-18 22:05:46 +01:00
Linus Groh
5c6370090f Release 2.7.0 2021-02-14 15:06:35 +01:00
Linus Groh
fc0189e5e2 Use official Docker Buildx action 2021-02-14 15:04:21 +01:00
Linus Groh
04fb50667b Update Dockerfile to use node:14 and nginx:1.18-alpine 2021-02-14 15:04:21 +01:00
Linus Groh
6359b4783c Add missing i18n for "Loading version..."
Hopefully DeepL got the translations for French and Spanish right...
2021-02-14 13:51:34 +01:00
Linus Groh
4679f7fbb7 Add en-GB translations 2021-02-14 13:23:29 +01:00
Linus Groh
b29cd12ed9 Use xx-XX format for translation files and default to en-US
Many languages have different variants, so instead of "en" we should be
using "en-US" - this will make it possible to add slightly different
British English translations, for example.
Note that this was already supported for the `locale` config option, we
were simply discarding the part after the dash when looking for the
right translation file.

Also make sure the en-US translations are actually American English,
I'll add en-GB separately.
2021-02-14 13:21:54 +01:00
Linus Groh
a9026c7a0a Upgrade dependencies 2021-02-14 12:49:46 +01:00
Elu43
27070812a4 Add French translations 2021-02-14 12:49:07 +01:00
Linus Groh
75e79fb0b1 Upgrade dependencies 2021-01-11 19:02:35 +01:00
Linus Groh
4bb9a20787 Fix incomplete Config interface in index.d.ts
Haven't looked at this in a while. :)
2021-01-11 18:53:34 +01:00
31 changed files with 2530 additions and 2083 deletions

View File

@@ -7,11 +7,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install dependencies
uses: borales/actions-yarn@v2.0.0
- uses: actions/setup-node@v2
with:
cmd: install
- name: Run production build
uses: borales/actions-yarn@v2.0.0
with:
cmd: build
node-version: '14'
- run: yarn install
- run: yarn build

View File

@@ -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}
@@ -49,8 +49,12 @@ jobs:
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
--build-arg VCS_REF=${GITHUB_SHA::8} \
${TAGS} --file ./docker/Dockerfile .
# https://github.com/docker/setup-qemu-action
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
# https://github.com/docker/setup-buildx-action
- name: Set up Docker Buildx
uses: crazy-max/ghaction-docker-buildx@v3
uses: docker/setup-buildx-action@v1
- name: Docker Buildx (build)
run: |
docker buildx build --output "type=image,push=false" ${{ steps.prepare.outputs.buildx_args }}

View File

@@ -7,15 +7,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install dependencies
uses: borales/actions-yarn@v2.0.0
- uses: actions/setup-node@v2
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: '14'
- run: yarn install
- run: yarn lint:js
- run: yarn lint:md
- run: yarn lint:scss

View File

@@ -7,11 +7,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install dependencies
uses: borales/actions-yarn@v2.0.0
- uses: actions/setup-node@v2
with:
cmd: install
- name: Run unit tests
uses: borales/actions-yarn@v2.0.0
with:
cmd: test
node-version: '14'
- run: yarn install
- run: yarn test

View File

@@ -2,6 +2,30 @@
Dates are in UTC.
## 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
- Separate `en` translations into British English (`en-GB`) and American English (`en-US`, default)
- Add French translations ([#49](https://github.com/owntracks/frontend/pull/49), [@Elu43](https://github.com/Elu43))
- Update Docker image to use Node 14 and nginx 1.18
- Upgrade dependencies
## 2.6.0 (2020-12-29)
- Add `router.basePath` config option for non-webroot deployments

View File

@@ -1,14 +1,14 @@
# OwnTracks UI
# OwnTracks Frontend
![Version](https://img.shields.io/github/package-json/v/owntracks/frontend)
[![Docker Pulls](https://img.shields.io/docker/pulls/owntracks/frontend)](https://hub.docker.com/r/owntracks/frontend)
[![Build](https://github.com/owntracks/frontend/workflows/Build/badge.svg)](https://github.com/owntracks/frontend/actions?query=workflow%3ABuild+branch%3Amaster)
[![Tests](https://github.com/owntracks/frontend/workflows/Tests/badge.svg)](https://github.com/owntracks/frontend/actions?query=workflow%3ATests+branch%3Amaster)
[![Lint](https://github.com/owntracks/frontend/workflows/Lint/badge.svg)](https://github.com/owntracks/frontend/actions?query=workflow%3ALint+branch%3Amaster)
[![Build](https://github.com/owntracks/frontend/workflows/Build/badge.svg)](https://github.com/owntracks/frontend/actions?query=workflow%3ABuild+branch%3Amain)
[![Tests](https://github.com/owntracks/frontend/workflows/Tests/badge.svg)](https://github.com/owntracks/frontend/actions?query=workflow%3ATests+branch%3Amain)
[![Lint](https://github.com/owntracks/frontend/workflows/Lint/badge.svg)](https://github.com/owntracks/frontend/actions?query=workflow%3ALint+branch%3Amain)
[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
[![License](https://img.shields.io/github/license/owntracks/frontend?color=d63e97)](https://github.com/owntracks/frontend/blob/master/LICENSE)
[![License](https://img.shields.io/github/license/owntracks/frontend?color=d63e97)](https://github.com/owntracks/frontend/blob/main/LICENSE)
![OwnTracks UI](https://raw.githubusercontent.com/owntracks/frontend/master/docs/images/owntracks-ui.png)
![Screenshot](https://raw.githubusercontent.com/owntracks/frontend/main/docs/images/screenshot.png)
## 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.
![Map features](https://raw.githubusercontent.com/owntracks/frontend/master/docs/images/map-features.png)
![Map features](https://raw.githubusercontent.com/owntracks/frontend/main/docs/images/map-features.png)
## Features
@@ -53,7 +53,7 @@ compose config, and the service is named `otrecorder`):
version: "3"
services:
owntracks-ui:
owntracks-frontend:
image: owntracks/frontend
ports:
- 80:80
@@ -101,8 +101,10 @@ See [`docs/config.md`](docs/config.md) for all available options.
- 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 lint:scss` to lint SCSS files
- Run `yarn format:js` to format JavaScript/Vue files
- Run `yarn format:md` to format Markdown files
- Run `yarn format:scss` to format SCSS files
- Run `yarn test` to run unit tests
### CORS-Proxy
@@ -142,7 +144,7 @@ unused i18n entries, run:
$ yarn i18n:report
```
To add a new locale, copy `en.json` to `<locale>.json` in [`src/locales`](src/locales)
To add a new locale, copy `en-US.json` to `<locale>.json` in [`src/locales`](src/locales)
and start translating the individual strings. Make sure to [mention the new locale to the docs](docs/config.md#locale)!
For a specific example see commit [`b2edda4`](https://github.com/owntracks/frontend/commit/b2edda410f16633aa6fd9cd4e5250f2031536c7d)

View File

@@ -1,13 +1,13 @@
FROM node:10 as build
FROM node:14 as build
WORKDIR /usr/src/app
COPY package.json yarn.lock ./
RUN yarn install
COPY . ./
RUN yarn build
FROM nginx:1.17-alpine
LABEL version="2.6.0"
LABEL description="OwnTracks UI"
FROM nginx:1.18-alpine
LABEL version="2.10.0"
LABEL description="OwnTracks Frontend"
LABEL maintainer="Linus Groh <mail@linusgroh.de>"
ENV LISTEN_PORT=80 \
SERVER_HOST=otrecorder \

View File

@@ -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;

View File

@@ -171,14 +171,17 @@ formats.
Available languages:
- `de` (German)
- `en` (English)
- `es` (Spanish)
- `de-DE` (Standard German)
- `en-GB` (British English)
- `en-US` (American English)
- `es-ES` (Castilian Spanish)
- `fr-FR` (Standard French)
You can use formats like `en-GB`, `en-US`, `de-DE`.
Using a locale with non-existent translations is possible and will affect date/time formats, but
use `en-US` for translations.
- Type: [`String`]
- Default: `"en"`
- Default: `"en-US"`
### `map.attribution`
@@ -400,8 +403,7 @@ 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/v4/mapbox.dark/{z}/{x}/{y}@2x.png?access_token=xxxxxxxxxxxxxxxx",
},
};
```
@@ -496,6 +498,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`

View File

Before

Width:  |  Height:  |  Size: 653 KiB

After

Width:  |  Height:  |  Size: 653 KiB

View File

@@ -1,6 +1,6 @@
{
"name": "owntracks-ui",
"version": "2.6.0",
"name": "owntracks-frontend",
"version": "2.10.0",
"author": {
"name": "Linus Groh",
"email": "mail@linusgroh.de"
@@ -11,51 +11,53 @@
"cors-proxy": "node scripts/corsProxy.js",
"format:js": "vue-cli-service lint",
"format:md": "prettier --write '{*.md,docs/**/*.md,src/**/*.md}'",
"format:scss": "prettier --write 'src/**/*.scss'",
"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}'",
"lint:scss": "prettier --check 'src/**/*.scss'",
"test": "vue-cli-service test:unit"
},
"dependencies": {
"clipboard-copy": "^4.0.1",
"core-js": "^3.8.1",
"core-js": "^3.19.1",
"deepmerge": "^4.2.2",
"leaflet": "^1.7.1",
"leaflet.heat": "^0.2.0",
"moment": "^2.29.1",
"vue": "^2.6.12",
"vue": "^2.6.14",
"vue-ctk-date-time-picker": "^2.5.0",
"vue-feather-icons": "^5.1.0",
"vue-i18n": "^8.22.2",
"vue-js-modal": "^1.3.33",
"vue-i18n": "^8.26.7",
"vue-js-modal": "^2.0.1",
"vue-mq": "^1.0.1",
"vue-outside-events": "^1.1.3",
"vue-router": "^3.4.9",
"vue2-leaflet": "^2.6.0",
"vuex": "^3.6.0"
"vue-router": "^3.5.3",
"vue2-leaflet": "^2.7.1",
"vuex": "^3.6.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.9",
"@vue/cli-plugin-eslint": "~4.5.9",
"@vue/cli-plugin-unit-jest": "~4.5.9",
"@vue/cli-service": "~4.5.9",
"@vue/cli-plugin-babel": "~4.5.15",
"@vue/cli-plugin-eslint": "~4.5.15",
"@vue/cli-plugin-unit-jest": "~4.5.15",
"@vue/cli-service": "~4.5.15",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/test-utils": "1.1.2",
"@vue/test-utils": "1.3.0",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^26.6.3",
"cors-anywhere": "^0.4.3",
"eslint": "^7.16.0",
"eslint-plugin-prettier": "^3.3.0",
"eslint-plugin-vue": "^7.4.0",
"babel-jest": "^27.3.1",
"cors-anywhere": "^0.4.4",
"eslint": "^7.32.0",
"eslint-plugin-prettier": "^3.4.1",
"eslint-plugin-vue": "^7.17.0",
"jest-fetch-mock": "^3.0.3",
"lint-staged": "^10.5.3",
"lint-staged": "^12.1.2",
"moment-locales-webpack-plugin": "^1.2.0",
"prettier": "^2.2.1",
"sass": "^1.30.0",
"sass-loader": "^10.1.0",
"vue-cli-plugin-i18n": "~1.0.1",
"vue-template-compiler": "^2.6.12"
"prettier": "^2.5.0",
"sass": "^1.43.5",
"sass-loader": "^10.1.1",
"vue-cli-plugin-i18n": "~2.3.1",
"vue-template-compiler": "^2.6.14"
},
"license": "MIT",
"repository": {

View File

@@ -5,7 +5,7 @@
<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>
<title>OwnTracks Frontend</title>
</head>
<body>
<noscript>

View File

@@ -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
);
})
);

View File

@@ -118,10 +118,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) }}
<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
@@ -149,10 +158,24 @@
</header>
</template>
<style lang="scss" scoped>
.distance-travelled {
text-align: right;
line-height: 1.2;
.feather {
margin-top: 3px;
margin-right: 0 !important;
}
}
</style>
<script>
import moment from "moment";
import { mapActions, mapGetters, mapMutations, mapState } from "vuex";
import {
ArrowDownIcon,
ArrowUpIcon,
CalendarIcon,
CrosshairIcon,
DownloadIcon,
@@ -172,6 +195,8 @@ import { humanReadableDistance } from "@/util";
export default {
components: {
ArrowDownIcon,
ArrowUpIcon,
CalendarIcon,
CrosshairIcon,
DownloadIcon,
@@ -196,7 +221,14 @@ export default {
};
},
computed: {
...mapState(["users", "devices", "map", "distanceTravelled"]),
...mapState([
"users",
"devices",
"map",
"distanceTravelled",
"elevationGain",
"elevationLoss",
]),
selectedUser: {
get() {
return this.$store.state.selectedUser;

View File

@@ -9,7 +9,7 @@
<li>
<GithubIcon size="1x" aria-hidden="true" role="img" />
<a href="https://github.com/owntracks/recorder">owntracks/recorder</a>
({{ recorderVersion || "Loading version..." }})
({{ recorderVersion || $t("Loading version...") }})
</li>
<li>
<GlobeIcon size="1x" aria-hidden="true" role="img" />

View File

@@ -1,16 +1,29 @@
<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>
<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);
@@ -22,11 +35,15 @@
</style>
<script>
import { mapState } from "vuex";
import { LoaderIcon } from "vue-feather-icons";
export default {
components: {
LoaderIcon,
},
computed: {
...mapState(["requestAbortController"]),
},
};
</script>

View File

@@ -17,7 +17,7 @@ const DEFAULT_CONFIG = {
minAccuracy: null,
},
ignorePingLocation: true,
locale: "en",
locale: "en-US",
map: {
attribution:
'&copy; <a href="https://osm.org/copyright">OpenStreetMap</a> contributors',

View File

@@ -16,8 +16,8 @@ locales.keys().forEach((key) => {
});
export default new VueI18n({
locale: config.locale.split("-")[0],
fallbackLocale: "en",
locale: config.locale,
fallbackLocale: "en-US",
formatFallbackMessages: true,
messages,
});

17
src/index.d.ts vendored
View File

@@ -2,8 +2,12 @@
interface Config {
api: {
baseUrl: string;
fetchOptions: object;
};
endDateTime: Date;
filters: {
minAccuracy: number | null,
};
ignorePingLocation: boolean;
locale: string;
map: {
@@ -54,11 +58,16 @@ interface Config {
url: string;
};
onLocationChange: {
fitView: boolean;
reloadHistory: boolean;
};
primaryColor: Color;
selectedUser: User| null;
selectedDevice: Device| null;
router: {
basePath: string;
};
selectedUser: User | null;
selectedDevice: Device | null;
showDistanceTravelled: boolean;
startDateTime: Date;
verbose: boolean;
}
@@ -72,8 +81,8 @@ interface State {
devices: { User: Device[] };
lastLocations: OTLocation[];
locationHistory: LocationHistory;
selectedUser: User| null;
selectedDevice: Device| null;
selectedUser: User | null;
selectedDevice: Device | null;
startDateTime: string;
endDateTime: string;
map: {

View File

@@ -11,6 +11,7 @@
"Show all": "Alle anzeigen",
"Select device": "Gerät auswählen",
"Distance travelled": "Gereiste Entfernung",
"Elevation gain / loss": "Höhengewinn / -verlust",
"Download raw data": "Rohdaten herunterladen",
"Information": "Information",
"Show last known locations": "Zeige letzte bekannte Standorte",
@@ -20,10 +21,12 @@
"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",
"Loading data, please wait...": "Daten werden geladen, bitte warten...",
"Cancel": "Abbrechen",
"Image of {deviceName}": "Bild von {deviceName}",
"Timestamp": "Zeitstempel",
"Location": "Standort",

37
src/locales/en-GB.json Normal file
View File

@@ -0,0 +1,37 @@
{
"Automatically center the map view and zoom in to relevant data": "Automatically centre the map view and zoom in to relevant data",
"Fit view": "Fit view",
"Layer settings": "Layer settings",
"Show/hide layers": "Show/hide layers",
"Now": "Now",
"Select start date": "Select start date",
"to": "to",
"Select end date": "Select end date",
"Select user": "Select user",
"Show all": "Show all",
"Select device": "Select device",
"Distance travelled": "Distance travelled",
"Elevation gain / loss": "Elevation gain / loss",
"Download raw data": "Download raw data",
"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",
"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",
"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:"
}

View File

@@ -10,7 +10,8 @@
"Select user": "Select user",
"Show all": "Show all",
"Select device": "Select device",
"Distance travelled": "Distance travelled",
"Distance travelled": "Distance traveled",
"Elevation gain / loss": "Elevation gain / loss",
"Download raw data": "Download raw data",
"Information": "Information",
"Show last known locations": "Show last known locations",
@@ -20,10 +21,12 @@
"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",
"Loading data, please wait...": "Loading data, please wait...",
"Cancel": "Cancel",
"Image of {deviceName}": "Image of {deviceName}",
"Timestamp": "Timestamp",
"Location": "Location",

View File

@@ -11,6 +11,7 @@
"Show all": "Mostrar todos",
"Select device": "Seleccionar dispositivo",
"Distance travelled": "Distancia recorrida",
"Elevation gain / loss": "Aumento / disminución de la altura",
"Download raw data": "Descargar datos en crudo",
"Information": "Información",
"Show last known locations": "Mostrar última ubicación conocida",
@@ -20,10 +21,12 @@
"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",
"Loading data, please wait...": "Cargando datos, por favor, espera...",
"Cancel": "Cancelar",
"Image of {deviceName}": "Imágen de {deviceName}",
"Timestamp": "Fecha / Hora",
"Location": "Ubicación",

37
src/locales/fr-FR.json Normal file
View File

@@ -0,0 +1,37 @@
{
"Automatically center the map view and zoom in to relevant data": "Centrer automatiquement la vue de la carte et zoomer sur les données pertinentes",
"Fit view": "Vue d'ensemble",
"Layer settings": "Paramètres des couches",
"Show/hide layers": "Montrer/cacher certaines couches",
"Now": "Maintenant",
"Select start date": "Sélectionner une date de début",
"to": "à",
"Select end date": "Sélectionner une date de fin",
"Select user": "Sélectionner un utilisateur",
"Show all": "Tout afficher",
"Select device": "Sélectionner un appareil",
"Distance travelled": "Distance parcourue",
"Elevation gain / loss": "Augmentation / diminution de l'altitude",
"Download raw data": "Télécharger les données brutes",
"Information": "Informations",
"Show last known locations": "Afficher les dernières localisations connues",
"Show location history (line)": "Afficher l'historique de localisation (lignes)",
"Show location history (points)": "Afficher l'historique de localisation (points)",
"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",
"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:"
}

View File

@@ -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);
}
};

View File

@@ -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,

View File

@@ -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";

View File

@@ -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;
},
};

View File

@@ -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 {

View File

@@ -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);
}
}

4074
yarn.lock

File diff suppressed because it is too large Load Diff