69 Commits

Author SHA1 Message Date
Jan-Piet Mens
f780bbede3 Add note about auth/tls
As this is turning into a bit of a faq, I think we should add this to the README
2024-12-27 08:27:57 +01:00
Jan-Piet Mens
7877aaa9f8 Merge pull request #134 from gene1wood/patch-1
Fix typo in config.example.js comment
2024-11-27 08:20:18 +01:00
Gene Wood
341ce4c353 Fix typo in config.example.js comment 2024-11-26 20:31:32 -08:00
Maxim Baz
5ecad2bf40 nginx: increase read timeout 2024-07-28 18:25:39 +01:00
Jan Jaden Schmidt
3365959ed3 Add more options to the timespan selector (#123)
* Add more options to the timespan selector

Add more options to the timespan selector.

* Add 'Yesterday', '3 days', '15 days', '3 months', '6 months', and '1 year' options to the `shortcuts` array in `src/components/AppHeader.vue`.
* Ensure the new options are added in the same format as the existing options.
* Verify that the new options are correctly displayed in the timespan selector.

* Add translations for timespan selector

Add translations for timespan selector in multiple locales

* **cs-CZ.json**: Add translations for 'Today', 'Yesterday', '3 days', '7 days', '15 days', '30 days', '3 months', '6 months', and '1 year'.
* **da-DK.json**: Add translations for 'Today', 'Yesterday', '3 days', '7 days', '15 days', '30 days', '3 months', '6 months', and '1 year'.
* **de-DE.json**: Add translations for 'Today', 'Yesterday', '3 days', '7 days', '15 days', '30 days', '3 months', '6 months', and '1 year'.
* **en-GB.json**: Add translations for 'Yesterday', '3 days', '15 days', '3 months', '6 months', and '1 year'.
* **en-US.json**: Add translations for 'Yesterday', '3 days', '15 days', '3 months', '6 months', and '1 year'.
* **es-ES.json**: Add translations for 'Today', 'Yesterday', '3 days', '7 days', '15 days', '30 days', '3 months', '6 months', and '1 year'.
* **fr-FR.json**: Add translations for 'Today', 'Yesterday', '3 days', '7 days', '15 days', '30 days', '3 months', '6 months', and '1 year'.
* **sk-SK.json**: Add translations for 'Today', 'Yesterday', '3 days', '7 days', '15 days', '30 days', '3 months', '6 months', and '1 year'.
* **tr-TR.json**: Add translations for 'Today', 'Yesterday', '3 days', '7 days', '15 days', '30 days', '3 months', '6 months', and '1 year'.
2024-07-24 11:08:58 +01:00
Linus Groh
15a40f9c6c Upgrade dependencies 2024-07-06 10:06:16 +01:00
Linus Groh
9839b5acdd Release 2.15.3 2024-06-15 16:25:06 +02:00
Linus Groh
0300e2fb4f Force relative path for config/config.js
Closes #118, again.
2024-06-15 16:23:31 +02:00
Linus Groh
4c680590a4 Add eslint-config-prettier as a dev dependency 2024-06-14 17:56:53 +02:00
Linus Groh
551b226fd0 Release 2.15.2 2024-06-14 17:54:46 +02:00
Linus Groh
132f15c52b Recreate npm lockfile
Error: Cannot find module @rollup/rollup-linux-arm-gnueabihf. npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). Please try `npm i` again after removing both package-lock.json and node_modules directory.
2024-06-14 17:53:57 +02:00
Linus Groh
c60bfb5368 Release 2.15.1 2024-06-14 17:47:24 +02:00
Linus Groh
31bf39795c Bump nginx to version 1.27 2024-06-14 17:43:44 +02:00
Linus Groh
d5c87a8727 Upgrade dependencies 2024-06-14 17:40:56 +02:00
Linus Groh
0492b355bf Clean up index.html a bit 2024-06-14 17:39:28 +02:00
Linus Groh
998a97131b Update vite config to emit relative paths
Closes #118.
2024-06-14 17:37:26 +02:00
Linus Groh
a44965226c Release 2.15.0 2024-06-10 19:58:13 +01:00
Linus Groh
554ce3f585 Implement POI map layer
Closes #107.
2024-06-10 19:53:49 +01:00
Linus Groh
05ae629217 Release 2.14.0 2024-06-09 23:07:26 +01:00
jduar
b141444b56 Implement new date/time range picker
Co-authored-by: Christophe Chapuis <chris.chapuis@gmail.com>
2024-06-09 23:04:48 +01:00
Linus Groh
723ce684ae Release 2.13.1
Oops, I forgot to bump the version...
2024-06-09 15:37:00 +01:00
Linus Groh
ed3e6125e9 Release 2.13.0 2024-06-09 15:00:45 +01:00
Linus Groh
8df1f86ab9 Remove the download modal
I have not used this once since adding it. Just use the API directly.
2024-06-09 14:49:30 +01:00
Linus Groh
b5442363d6 Upgrade dependencies 2024-06-09 14:45:31 +01:00
Andreas Baumgartner
35d55b57b1 Add action for uploading dist/ on release 2024-06-09 14:38:36 +01:00
Jiri Mencak
976bb403d1 Add Czech language 2024-05-05 19:21:29 +02:00
Linus Groh
cecf7e797d Fix i18n message keys 2024-02-27 22:06:31 +00:00
Linus Groh
91d99cd8da Migrate from vue-cli / webpack to vite 2024-02-27 22:01:37 +00:00
Linus Groh
06faa73b70 Show isolocal and tzname properties on the popup
Closes #108.
2024-02-26 23:08:39 +00:00
Linus Groh
7398da74c5 Fix popup close button style 2024-02-26 22:58:46 +00:00
Linus Groh
7b954dfbe3 Replace Twitter link with Mastodon
Closes #109.
2024-02-26 22:25:05 +00:00
Linus Groh
c569aced1e Switch from yarn to npm 2024-02-26 22:20:25 +00:00
Linus Groh
3fad44509e Happy new year I guess? 2024-02-26 22:17:36 +00:00
Linus Groh
aa13ddd832 Bump nginx to version 1.25 2024-02-26 22:17:36 +00:00
Linus Groh
6a2b113fb2 Bump node to version 20 2024-02-26 22:17:36 +00:00
Linus Groh
6f047ffa77 Reformat _base.scss with updated prettier 2024-02-26 22:17:36 +00:00
Linus Groh
d5d6c1c268 Upgrade dependencies 2024-02-26 22:17:36 +00:00
Linus Groh
4e86d8fac3 Add newline to end of README.md 2024-02-26 21:45:10 +00:00
aasami
1cb6e3519e Add Slovak language (#110) 2024-02-26 12:14:03 +00:00
Ramazan Sancar
f5389b84ab Add Turkish translations 2023-05-26 09:51:45 +01:00
Robin Meis
9bb2edb78d Add manifest.json to use frontend as PWA 2023-05-01 13:36:25 +01:00
Linus Groh
6361d8f415 Release 2.12.0 2022-09-06 20:48:59 +01:00
Linus Groh
791b756d80 Upgrade dependencies 2022-09-06 20:41:54 +01:00
Anton
f1ef82d7bb Add Danish translation 2022-09-05 19:23:56 +01:00
Linus Groh
aaef181141 Upgrade dependencies 2022-04-20 16:25:50 +02:00
atjn
b2273c071b Define a face style 2022-04-18 22:05:36 +02:00
Jonas Wunderlich
865c89b43c Updated Mapbox tile server URL in documentation example 2022-03-28 17:56:58 +01:00
Sascha Hagedorn
32c64d18f5 Fix markdown linting issue 2022-03-18 18:01:18 +00:00
Sascha Hagedorn
5a64c06af0 Add tileSize option 2022-03-18 09:39:46 +00:00
Sascha Hagedorn
8c3681b6ad Add zoomOffset option 2022-03-18 09:39:46 +00:00
Linus Groh
89899de565 Release 2.11.0 2022-03-15 23:14:51 +00:00
Linus Groh
a386c15de1 Fix jest configuration 2022-03-15 23:12:50 +00:00
Linus Groh
8ac24c99aa Fix DefinePlugin "Conflicting values for 'process.env'" warning 2022-03-15 22:46:56 +00:00
Linus Groh
f3cbf877f9 Upgrade dependencies 2022-03-15 22:46:37 +00:00
Sascha Hagedorn
f5c1c82010 Display address in device location popup
`addr` is part of the response for reverse geolocation lookups. `address` is already used in `LDeviceLocationPopup` and needs to be passed in from the map.

Closes #31
2022-03-14 22:27:52 +00:00
Linus Groh
3ea1d02c65 Show SSID and BSSID in location popup if available 2022-02-11 23:46:26 +00:00
Linus Groh
f91341b205 Add new properties to the OTLocation interface 2022-02-11 23:23:29 +00:00
Linus Groh
0c983d6206 Upgrade dependencies
Also rename a few components to match the multi-word recommendation and
add "Map" to the ignorelist of the vue/multi-word-component-names ESLint
rule.

A couple of major version bumps are currently blocked by upgrading the
project to Vue 3.
2022-02-11 22:23:16 +00:00
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
59 changed files with 6237 additions and 11917 deletions

View File

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

View File

@@ -6,9 +6,9 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '14'
- run: yarn install
- run: yarn build
node-version: '20'
- run: npm install
- run: npm run 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}

View File

@@ -6,10 +6,11 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '14'
- run: yarn install
- run: yarn lint:js
- run: yarn lint:md
node-version: '20'
- run: npm install
- run: npm run lint:js
- run: npm run lint:md
- run: npm run lint:scss

View File

@@ -6,9 +6,9 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '14'
- run: yarn install
- run: yarn test
node-version: '20'
- run: npm install
- run: npm test

20
.github/workflows/upload-dist.yml vendored Normal file
View 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 }}

View File

@@ -2,6 +2,70 @@
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

View File

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

View File

@@ -2,13 +2,13 @@
![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)
![Screenshot](https://raw.githubusercontent.com/owntracks/frontend/master/docs/images/screenshot.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
@@ -31,9 +31,12 @@ 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
## Notes
Frontend has no provision for user management, authentication, or for configuring TLS; these are tasks which a HTTP server you use will have to provide, and an explanation on how to do that is beyond our scope; you should be able to find a myriad explanatory documents for the server you wish to use. Also note, that even if you set up authentication, any user successfully being able to access Frontend will be able to see all Recorder data, as the API of the latter doesn't distinguish querying users.
## Installation
### Docker
@@ -81,8 +84,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 +100,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 +116,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 +130,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 +144,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)

View File

@@ -1,3 +0,0 @@
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
};

View File

@@ -1,12 +1,12 @@
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.9.0"
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 \

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

View File

@@ -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.
@@ -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,9 +389,25 @@ to disable.
- Type: [`Number`]
- Default: `21`
### `map.poiMarker`
POI marker configuration. See [Vue2Leaflet `l-circle-marker` documentation](https://korigan.github.io/Vue2Leaflet/#/components/l-circle-marker/)
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
Location point marker configuration. `color` defaults to `primaryColor` if `null`. See
[Vue2Leaflet `l-polyline` documentation](https://korigan.github.io/Vue2Leaflet/#/components/l-polyline/)
for all possible values.
@@ -391,6 +420,14 @@ 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)
@@ -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.

47
eslint.config.js Normal file
View 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"],
},
],
},
},
];

View File

@@ -1,10 +1,10 @@
<!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">
<link rel="icon" href="/favicon.ico">
<link rel="manifest" crossorigin="use-credentials" href="/manifest.json">
<title>OwnTracks Frontend</title>
</head>
<body>
@@ -12,7 +12,7 @@
<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>

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,65 +1,62 @@
{
"name": "owntracks-frontend",
"version": "2.9.0",
"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.11.1",
"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.24.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.7.0",
"vuex": "^3.6.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.12",
"@vue/cli-plugin-eslint": "~4.5.12",
"@vue/cli-plugin-unit-jest": "~4.5.12",
"@vue/cli-service": "~4.5.12",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/test-utils": "1.2.0",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^26.6.3",
"cors-anywhere": "^0.4.4",
"eslint": "^7.25.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-vue": "^7.9.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.12",
"sass-loader": "^10.1.1",
"vue-cli-plugin-i18n": "~2.1.0",
"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"
}
}

View File

@@ -1,5 +0,0 @@
module.exports = {
plugins: {
autoprefixer: {},
},
};

189
public/OwnTracks.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 35 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

20
public/manifest.json Normal file
View 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"
}

View File

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

View File

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

View File

@@ -136,7 +136,11 @@ export const getUserDeviceLocationHistory = async (
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",
() =>

View File

@@ -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>
@@ -132,19 +131,6 @@
{{ humanReadableDistance(elevationLoss) }}
</span>
</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>
</div>
<div class="nav-item">
<button
class="button button-flat button-icon"
@@ -158,18 +144,6 @@
</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";
@@ -178,17 +152,17 @@ import {
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";
@@ -199,14 +173,13 @@ export default {
ArrowUpIcon,
CalendarIcon,
CrosshairIcon,
DownloadIcon,
DatePicker,
InfoIcon,
LayersIcon,
MenuIcon,
SmartphoneIcon,
UserIcon,
VueCtkDateTimePicker,
Dropdown,
DropdownButton,
},
data() {
return {
@@ -216,8 +189,110 @@ 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: {
@@ -245,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)
);
},
},
@@ -286,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>

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"]),
},

View File

@@ -13,6 +13,20 @@
</modal>
</template>
<script>
import { mapState } from "vuex";
import { LoaderIcon } from "vue-feather-icons";
export default {
components: {
LoaderIcon,
},
computed: {
...mapState(["requestAbortController"]),
},
};
</script>
<style scoped>
.loader-icon {
animation: spinning 2s linear infinite;
@@ -33,17 +47,3 @@
}
}
</style>
<script>
import { mapState } from "vuex";
import { LoaderIcon } from "vue-feather-icons";
export default {
components: {
LoaderIcon,
},
computed: {
...mapState(["requestAbortController"]),
},
};
</script>

View File

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

View File

@@ -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
View File

@@ -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
View 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
View 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"
}

View File

@@ -12,7 +12,6 @@
"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",
"Show location history (line)": "Zeige Standortverlauf (Linie)",
@@ -20,11 +19,10 @@
"Show location heatmap": "Zeige Standort-Heatmap",
"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}",
@@ -33,5 +31,15 @@
"Address": "Adresse",
"Battery": "Akku",
"Speed": "Geschwindigkeit",
"Regions:": "Regionen:"
"Regions:": "Regionen:",
"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"
}

View File

@@ -12,19 +12,18 @@
"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",
"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}",
@@ -33,5 +32,17 @@
"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"
}

View File

@@ -12,19 +12,18 @@
"Select device": "Select device",
"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",
"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}",
@@ -33,5 +32,17 @@
"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"
}

View File

@@ -12,7 +12,6 @@
"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",
"Show location history (line)": "Mostrar historial (línea)",
@@ -20,11 +19,10 @@
"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}",
@@ -33,5 +31,15 @@
"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"
}

View File

@@ -12,7 +12,6 @@
"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)",
@@ -20,11 +19,10 @@
"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}",
@@ -33,5 +31,15 @@
"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
View 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"
}

45
src/locales/tr-TR.json Normal file
View File

@@ -0,0 +1,45 @@
{
"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",
"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",
"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"
}

View File

@@ -201,9 +201,8 @@ const getLocationHistory = async ({ commit, state }) => {
}
commit(types.SET_LOCATION_HISTORY, locationHistory);
if (config.showDistanceTravelled) {
const { distanceTravelled, elevationGain, elevationLoss } = _getTravelStats(
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

@@ -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: {},

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

View File

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

View File

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

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

View File

@@ -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.
*

View File

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

View File

@@ -1,5 +0,0 @@
module.exports = {
env: {
jest: true,
},
};

View File

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

View File

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

View File

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

11249
yarn.lock

File diff suppressed because it is too large Load Diff