139 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
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
Linus Groh
1a47fd1b6c Release 2.6.0 2020-12-29 19:39:37 +01:00
Linus Groh
163e0e3ec7 Upgrade dependencies 2020-12-29 19:21:38 +01:00
Andrew Rowson
8d8664a338 Configure Vue to not assume it's on the web root
Fixes #21.
2020-12-29 19:17:11 +01:00
Linus Groh
045e635c21 Add router.basePath config option for deployments at non-webroot
This is in preparation of configuring Vue with publicPath: "".
2020-12-29 19:17:11 +01:00
dependabot[bot]
7db7837dfd Bump ini from 1.3.5 to 1.3.8 2020-12-24 13:56:38 +01:00
Andrew Rowson
beb522c03e NGINX should listen on IPv6 as well 2020-12-24 13:15:34 +01:00
Linus Groh
658cb6b223 Release 2.5.1 2020-10-27 18:07:21 +00:00
Linus Groh
7ab98be4ad Add tests for baseUrl with trailing slash 2020-10-27 17:58:01 +00:00
Karmanyaah Malhotra
6d4d47b5a1 Fix getApiUrl() for baseUrl with trailing slash 2020-10-27 17:52:07 +00:00
Linus Groh
159470181c Upgrade dependencies 2020-10-15 08:45:08 +02:00
Linus Groh
b53a0be707 Upgrade dependencies 2020-09-20 21:34:43 +01:00
Linus Groh
4e70d3a3ad Update dependencies 2020-09-12 13:34:10 +01:00
Linus Groh
b42f6db024 Release 2.5.0 2020-09-07 21:56:37 +01:00
Linus Groh
3fbf0a2ff1 Upgrade dependencies 2020-09-07 21:46:17 +01:00
Linus Groh
206eb268fa Add onLocationChange.fitView config option
It defaults to false now, which is a change - this seems like a sensible
choice though. I can see how it would be annoying to have the map change
while looking at it just because *something* moved.

Fixes #41.
2020-09-07 21:30:47 +01:00
Linus Groh
d7266f48f1 Upgrade dependencies 2020-07-31 20:44:36 +02:00
Linus Groh
9ec17e3e9c Replace node-sass with sass (dart-sass) 2020-07-22 13:15:35 +02:00
Linus Groh
e320441b5e Upgrade dependencies 2020-07-22 13:11:59 +02:00
Linus Groh
1282d93769 Remove datepicker buttons mobile style overrides
This has been fixed upstream.
2020-06-24 14:49:30 +01:00
Linus Groh
b8661b11fb Upgrade dependencies 2020-06-24 14:44:46 +01:00
Linus Groh
4f5bfefc36 Add more badges 2020-06-14 16:27:12 +01:00
Linus Groh
a1faf8153b Replace Travis CI with GitHub Actions build/lint/test workflows (#39) 2020-06-14 16:11:20 +01:00
Linus Groh
23d73461bc Hardcode owntracks/frontend as DOCKER_IMAGE in docker.yml
As we were using the DOCKER_USERNAME secret rather than "owntracks", the
images all ended up in @jpmens' personal account :)
2020-06-14 15:28:41 +01:00
Linus Groh
90dac4022a Rename main.yml to docker.yml
I'll attempt to switch over from Travis CI to GitHub actions completely,
so "main" is not the best naming choice.
2020-06-14 15:24:58 +01:00
Linus Groh
58b22aeebe Update GitHub action Docker workflow name 2020-06-14 15:23:43 +01:00
wollew
cb79a4de81 Use GitHub Actions to build Docker images for multiple archs (#38)
Co-authored-by: Wolfgang Miller-Reichling <wolfgang.miller-reichling@1und1.de>
2020-06-14 14:26:01 +01:00
Linus Groh
c5f491b6fb Upgrade dependencies 2020-06-09 15:41:09 +01:00
dependabot[bot]
49a9b54a5e Bump websocket-extensions from 0.1.3 to 0.1.4 (#37)
Bumps [websocket-extensions](https://github.com/faye/websocket-extensions-node) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/faye/websocket-extensions-node/releases)
- [Changelog](https://github.com/faye/websocket-extensions-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/faye/websocket-extensions-node/compare/0.1.3...0.1.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-06-07 14:58:07 +01:00
Linus Groh
c449fcaf21 Update features list in README.md 2020-06-03 21:08:45 +01:00
Linus Groh
1746c359f4 Remove additional screenshots
They don't really add much value.
2020-06-03 20:37:25 +01:00
Linus Groh
6180e52f71 Fix vertical offset of non-pin popups 2020-06-03 20:33:26 +01:00
Linus Groh
0f0f29fcee Show regions for location on popup 2020-06-03 20:28:48 +01:00
Linus Groh
cb0694b032 Release 2.4.0 2020-06-01 18:44:07 +01:00
Linus Groh
d0f2a99302 Upgrade dependencies 2020-06-01 18:39:01 +01:00
Linus Groh
c8b0ec8b9e Upgrade dependencies
This includes a major version bump of ESLint and Prettier, so also some
reformatting.
2020-05-16 13:58:48 +01:00
Linus Groh
906eb2a1b4 Fix typos causing the minAccuracy filter to work incorrectly 2020-05-16 13:50:57 +01:00
Linus Groh
005aab715f Improve wording in docs 2020-05-11 19:22:13 +01:00
Linus Groh
b76cbdc2e6 Add filters.minAccuracy config option
This allows us to ignore location points which do not meet the configured
accuracy requirement.

Closes #35.
2020-05-11 19:15:56 +01:00
Linus Groh
bb87ec01d4 Release 2.3.1 2020-05-09 15:50:52 +01:00
Linus Groh
c3e6b775f9 Re-format config.md 2020-05-09 15:50:01 +01:00
Linus Groh
a9998e8e3b Release 2.3.0 2020-05-09 15:45:19 +01:00
Linus Groh
2c4ead262a Add api.fetchOptions to allow customising fetch() behaviour
This allows sending custom HTTP headers or including cookies in the
request!
2020-05-09 15:40:50 +01:00
Linus Groh
6b1d35be51 Upgrade dependencies 2020-05-09 14:18:14 +01:00
Linus Groh
34cc4895b0 Upgrade dependencies 2020-04-02 19:45:43 +01:00
Linus Groh
57caacb548 Release 2.2.0 2020-03-18 17:56:11 +00:00
Linus Groh
9783b6f27d Upgrade dependencies 2020-03-18 17:55:43 +00:00
Linus Groh
b262ff602c Improve mobile layout further
- Reduce header paddings
- Align buttons/dropdowns
2020-03-18 17:50:49 +00:00
69 changed files with 6886 additions and 12604 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,
},
},
],
};

14
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
name: Build
on: [push]
jobs:
build:
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

79
.github/workflows/docker.yml vendored Normal file
View File

@@ -0,0 +1,79 @@
name: Build Docker images
on:
schedule:
- cron: '0 3 * * *' # everyday at 3am
pull_request:
branches: main
push:
branches: main
tags:
- v*
release:
types: [published]
branches: main
tags:
- v*
jobs:
build-docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Prepare
id: prepare
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: |
DOCKER_IMAGE=owntracks/frontend
DOCKER_PLATFORMS=linux/amd64,linux/arm/v7,linux/arm64
VERSION=main
if [[ $GITHUB_REF == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/v}
fi
if [ "${{ github.event_name }}" = "schedule" ]; then
VERSION=nightly
fi
TAGS="--tag ${DOCKER_IMAGE}:${VERSION}"
if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
TAGS="$TAGS --tag ${DOCKER_IMAGE}:latest"
fi
echo ::set-output name=docker_image::${DOCKER_IMAGE}
echo ::set-output name=version::${VERSION}
echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} \
--build-arg VERSION=${VERSION} \
--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: docker/setup-buildx-action@v1
- name: Docker Buildx (build)
run: |
docker buildx build --output "type=image,push=false" ${{ steps.prepare.outputs.buildx_args }}
- name: Docker Login
if: success() && github.event_name != 'pull_request'
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin
- name: Docker Buildx (push)
if: success() && github.event_name != 'pull_request'
run: |
docker buildx build --output "type=image,push=true" ${{ steps.prepare.outputs.buildx_args }}
- name: Docker Check Manifest
if: always() && github.event_name != 'pull_request'
run: |
docker run --rm mplatform/mquery ${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.version }}
- name: Clear
if: always() && github.event_name != 'pull_request'
run: |
rm -f ${HOME}/.docker/config.json

16
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: Lint
on: [push]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install
- run: npm run lint:js
- run: npm run lint:md
- run: npm run lint:scss

14
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
name: Tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
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

@@ -1,8 +0,0 @@
language: node_js
node_js:
- 10
cache: yarn
script:
- yarn lint:js
- yarn lint:md
- yarn test

View File

@@ -2,6 +2,133 @@
Dates are in UTC.
## 2.15.3 (2024-06-15)
- Force relative path for `config/config.js` even if it doesn't exist at build time
## 2.15.2 (2024-06-14)
- Fix npm lockfile
## 2.15.1 (2024-06-14)
- Update `index.html` to emit relative paths again, allowing deployment under a subpath
- Update Docker image to use nginx 1.27
- Upgrade dependencies
## 2.15.0 (2024-06-10)
- Implement POI map layer (see [Booklet](https://owntracks.org/booklet/features/poi/))
- Use the `map.poiMarker` config option to tweak the appearance, defaults to a red circle slightly larger than the default location points
- Use `map.layers.poi` to change the layer visibility, defaults to `true`
## 2.14.0 (2024-06-09)
- Implement new date/time range picker ([#116](https://github.com/owntracks/frontend/pull/116), [@jduar](https://github.com/jduar) / [@Tofee](https://github.com/Tofee))
## 2.13.1 (2024-06-09)
- Bump versions, just to make sure the frontend shows the right one
## 2.13.0 (2024-06-09)
- Enable use of the frontend as a progressive web app (PWA) ([#98](https://github.com/owntracks/frontend/pull/98), [@RobinMeis](https://github.com/RobinMeis))
- Add Turkish translations ([#94](https://github.com/owntracks/frontend/pull/94), [@ramazansancar](https://github.com/ramazansancar))
- Add Slovak translations ([#110](https://github.com/owntracks/frontend/pull/110), [@aasami](https://github.com/aasami))
- Add Czech translations ([#115](https://github.com/owntracks/frontend/pull/115), [@jmencak](https://github.com/jmencak))
- Add action for uploading dist/ on release ([#114](https://github.com/owntracks/frontend/pull/114), [@abaumg](https://github.com/abaumg))
- Replace outdated Twitter link with Mastodon
- Remove the download modal
- Show isolocal and tzname properties on the popup
- Various changes to the underlying frontend build system:
- Bump node to version 20
- Switch from yarn to npm
- Migrate from vue-cli / webpack to vite
- Upgrade dependencies
## 2.12.0 (2022-09-06)
- Add Danish translation ([#87](https://github.com/owntracks/frontend/pull/87), [@atjn](https://github.com/atjn))
- Ensure correct display of larger (192x192) face images ([#83](https://github.com/owntracks/frontend/pull/83), [@atjn](https://github.com/atjn))
- Add `map.tileSize` and `map.zoomOffset` options ([#75](https://github.com/owntracks/frontend/pull/75), [@saesh](https://github.com/saesh))
- Upgrade dependencies
## 2.11.0 (2022-03-16)
- Show WiFi SSID and BSSID in location popup, if available
- Show address in location popup, if available ([#73](https://github.com/owntracks/frontend/pull/73), [@saesh](https://github.com/saesh))
- Upgrade dependencies
## 2.10.0 (2021-11-28)
- Ensure location history line segments are drawn in chronological order ([#67](https://github.com/owntracks/frontend/issues/67))
- Add trailing slashes to paths used by Docker nginx config ([#63](https://github.com/owntracks/frontend/pull/63), [@growse](https://github.com/growse))
- Upgrade dependencies
## 2.9.0 (2021-05-01)
- Add a cancel button to the loading data modal
- Replace remaining uses of "OwnTracks UI" with "OwnTracks Frontend"
- Upgrade dependencies
## 2.8.0 (2021-02-19)
- Add elevation gain / loss to "distance travelled" calculation ([#51](https://github.com/owntracks/frontend/issues/51))
## 2.7.0 (2021-02-14)
- Rename translation files from `xx` to `xx-XX` format to allow different language variants
- 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
- Configure Vue to not assume it's on the web root ([#47](https://github.com/owntracks/frontend/pull/47), [@growse](https://github.com/growse))
- Update Docker NGINX config to listen on IPv6 as well ([#46](https://github.com/owntracks/frontend/pull/46), [@growse](https://github.com/growse))
- Upgrade dependencies
## 2.5.1 (2020-10-27)
- Fix incorrect handling of `api.baseUrl` with trailing slash ([#44](https://github.com/owntracks/frontend/pull/44), [@karmanyaahm](https://github.com/karmanyaahm))
- Upgrade dependencies
## 2.5.0 (2020-09-07)
- Add `filters.fitView` config option - this will prevent the map from re-fitting automatically by default when a live location changes ([#41](https://github.com/owntracks/frontend/issues/41))
- Show regions for location on popup
- Fix vertical offset of non-pin popups
- Build Docker images for multiple architectures (linux/amd64, linux/arm/v7, linux/arm64) using GitHub Actions ([#38](https://github.com/owntracks/frontend/pull/38), [@wollew](https://github.com/wollew))
- Replace Travis CI with GitHub Actions build/lint/test workflows ([#39](https://github.com/owntracks/frontend/pull/39))
- Replace node-sass with sass (dart-sass)
- Upgrade dependencies
## 2.4.0 (2020-06-01)
- Add `filters.minAccuracy` config option - this allows ignoring location points which do
not meet the configured accuracy requirement ([#35](https://github.com/owntracks/frontend/issues/35))
- Upgrade dependencies
## 2.3.1 (2020-05-09)
- Fix linting issue in `config.md`
## 2.3.0 (2020-05-09)
- Add `api.fetchOptions` config option - this allows sending custom HTTP headers or including
cookies in the request
- Upgrade dependencies
## 2.2.0 (2020-03-18)
- Improve mobile layout further:
- Reduce header paddings
- Align buttons/dropdowns
- Upgrade dependencies
## 2.1.0 (2020-03-18)
- Replace default Leaflet marker with a custom one ([#2](https://github.com/owntracks/frontend/issues/2))

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

@@ -1,11 +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 Status](https://travis-ci.org/owntracks/frontend.svg?branch=master)](https://travis-ci.org/owntracks/frontend)
[![License](https://img.shields.io/github/license/owntracks/frontend?color=d63e97)](https://github.com/owntracks/frontend/blob/master/LICENSE)
[![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/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
@@ -13,23 +16,27 @@ 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
- Last known (i.e. live) locations:
- Accuracy visualization (circle)
- Device friendly name and icon
- Detailed information (if available): time, latitude, longitude, height, battery and
speed
- Detailed information (if available): time, latitude, longitude, height, battery,
speed and regions
- Location history (data points, line or both)
- Location heatmap
- Quickly fit all shown objects on the map into view
- Display data in a specific date range
- Filter by user and device
- Download selected location data as JSON
- Display data in a specific date and time range
- Filter by user or specific device
- Calculation of distance travelled
- 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
@@ -49,7 +56,7 @@ compose config, and the service is named `otrecorder`):
version: "3"
services:
owntracks-ui:
owntracks-frontend:
image: owntracks/frontend
ports:
- 80:80
@@ -77,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
@@ -93,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
@@ -107,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)
@@ -121,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`
@@ -135,27 +144,15 @@ 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.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)
where German translations were added.
## Screenshots
<p align="center">
<img src="https://raw.githubusercontent.com/owntracks/frontend/master/docs/images/loading.gif" alt="Loading...">
<br>
<br>
<img src="https://raw.githubusercontent.com/owntracks/frontend/master/docs/images/downloader.png" alt="Download location data">
<br>
<br>
<img src="https://raw.githubusercontent.com/owntracks/frontend/master/docs/images/info.png" alt="Info">
</p>
## Contributing
Please feel free to open an issue and discuss your ideas and report bugs. If you think

View File

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

View File

@@ -1,13 +1,13 @@
FROM node:10 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.17-alpine
LABEL version="2.1.0"
LABEL description="OwnTracks UI"
FROM nginx:1.27-alpine
LABEL version="2.15.3"
LABEL description="OwnTracks Frontend"
LABEL maintainer="Linus Groh <mail@linusgroh.de>"
ENV LISTEN_PORT=80 \
SERVER_HOST=otrecorder \

View File

@@ -8,11 +8,12 @@ http {
}
server {
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;
@@ -30,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

@@ -24,7 +24,10 @@ window.owntracks.config = {};
- `api`
- [`baseUrl`](#apibaseurl)
- [`fetchOptions`](#apifetchoptions)
- [`endDateTime`](#enddatetime)
- `filters`
- [`minAccuracy`](#filtersminaccuracy)
- [`ignorePingLocation`](#ignorepinglocation)
- [`locale`](#locale)
- `map`
@@ -50,15 +53,20 @@ 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`
- [`fitView`](#onlocationchangefitview)
- [`reloadHistory`](#onlocationchangereloadhistory)
- [`primaryColor`](#primarycolor)
- `router`
- [`basePath`](#routerbasepath)
- [`selectedDevice`](#selecteddevice)
- [`selectedUser`](#selecteduser)
- [`showDistanceTravelled`](#showdistancetravelled)
@@ -76,16 +84,36 @@ Base URL for the recorder's HTTP and WebSocket API. Keep CORS in mind.
// API requests will be made to https://owntracks.example.com/api/0/...
window.owntracks.config = {
api: {
baseUrl: "https://owntracks.example.com"
}
baseUrl: "https://owntracks.example.com",
},
};
```
```js
// API requests will be made to https://example.com/owntracks/api/0/...
window.owntracks.config = {
api: {
baseUrl: "https://example.com/owntracks/"
}
baseUrl: "https://example.com/owntracks/",
},
};
```
### `api.fetchOptions`
Options for API requests (made with `fetch()`). See [`fetch()` docs on MDN] for details.
You can use this for example to send custom HTTP headers or to include cookies in the request.
- Type: [`Object`]
- Default: `{}`
- Example:
```js
// Include credentials (e.g. cookies)
window.owntracks.config = {
api: {
fetchOptions: {
credentials: "include",
},
},
};
```
@@ -99,7 +127,27 @@ Initial end date and time (browser timezone) for fetched data.
```js
// Data will be fetched up to 1970-01-01
window.owntracks.config = {
endDateTime: new Date(1970, 1, 1)
endDateTime: new Date(1970, 1, 1),
};
```
### `filters.minAccuracy`
Minimum accuracy in meters for location points to be rendered & included in the travelled distance.
This filter is disabled by default as accuracies can vary across devices an locations, but you're
encouraged to set it as it can be a simple way to remove outliers and vastly improve the travelled
distance calculation.
- Type: [`Number`] or `null`
- Default: `null`
- Example:
```js
// Don't include location points with an accuracy exceeding 100 meters
window.owntracks.config = {
filters: {
minAccuracy: 100,
},
};
```
@@ -114,7 +162,7 @@ Remove the `ping/ping` location from the fetched data. This is useful when using
```js
// Don't ignore ping/ping location. Not sure why you'd do this :)
window.owntracks.config = {
ignorePingLocation: false
ignorePingLocation: false,
};
```
@@ -125,14 +173,21 @@ formats.
Available languages:
- `de` (German)
- `en` (English)
- `es` (Spanish)
- `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)
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`
@@ -145,8 +200,8 @@ Attribution for map tiles.
// Make sure to add proper attribution!
window.owntracks.config = {
map: {
attribution: "Map tiles &copy; MyTileServerProvider"
}
attribution: "Map tiles &copy; MyTileServerProvider",
},
};
```
@@ -284,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`
@@ -313,8 +375,8 @@ splitting into separate lines.
// Don't connect points with a distance of more than 1km
window.owntracks.config = {
map: {
maxPointDistance: 1000
}
maxPointDistance: 1000,
},
};
```
@@ -327,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.
@@ -342,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)
@@ -354,12 +440,28 @@ 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.
This can be useful if you're showing live locations and don't want them to "leave" the map.
- Type: [`Boolean`]
- Default: `false`
### `onLocationChange.reloadHistory`
Whether to reload the location history (of selected date range) or not when a location
@@ -378,7 +480,23 @@ Primary color for the user interface (navigation bar and various map elements).
```js
// Set the UI's primary color to 'rebeccapurple'
window.owntracks.config = {
primaryColor: "rebeccapurple"
primaryColor: "rebeccapurple",
};
```
### `router.basePath`
Base path of the application deployment.
- Type: [`String`]
- Default: `"/"`
- Example:
```js
// Frontend will be reachable at https://example.com/owntracks
window.owntracks.config = {
router: {
basePath: "/owntracks",
},
};
```
@@ -397,7 +515,7 @@ amount of data fetched after page load.
// Select the device 'phone' from user 'foo' by default
window.owntracks.config = {
selectedUser: "foo",
selectedDevice: "phone"
selectedDevice: "phone",
};
```
@@ -414,7 +532,7 @@ amount of data fetched after page load.
```js
// Select all devices from user 'foo' by default
window.owntracks.config = {
selectedUser: "foo"
selectedUser: "foo",
};
```
@@ -425,6 +543,8 @@ header bar. `maxPointDistance` is being takein into account, if a distance betwe
subsequent points is greater than `maxPointDistance`, it will not contibute to the
calculated travelled distance.
This also includes a calculation of elevation gain / loss.
- Type: [`Boolean`]
- Default: `true`
@@ -441,7 +561,7 @@ Initial start date and time (browser timezone) for fetched data.
startDateTime.setHours(0, 0, 0, 0);
startDateTime.setDate(1);
window.owntracks.config = {
startDateTime
startDateTime,
};
```
@@ -458,3 +578,4 @@ Whether to enable verbose mode or not.
[`object`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object
[`string`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String
[css `<color>`]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
[`fetch()` docs on mdn]: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 370 KiB

View File

Before

Width:  |  Height:  |  Size: 653 KiB

After

Width:  |  Height:  |  Size: 653 KiB

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,18 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>OwnTracks UI</title>
<link rel="icon" href="/favicon.ico">
<link rel="manifest" crossorigin="use-credentials" href="/manifest.json">
<title>OwnTracks Frontend</title>
</head>
<body>
<noscript>
<strong>We're sorry but OwnTracks doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<script src="<%= BASE_URL %>config/config.js"></script>
<!-- built files will be auto injected -->
<script src="./config/config.js"></script>
<script type="module" src="/src/main.js"></script>
</body>
</html>

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,64 +1,62 @@
{
"name": "owntracks-ui",
"version": "2.1.0",
"name": "owntracks-frontend",
"version": "2.15.3",
"license": "MIT",
"author": {
"name": "Linus Groh",
"email": "mail@linusgroh.de"
},
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"cors-proxy": "node scripts/corsProxy.js",
"format:js": "vue-cli-service lint",
"format:md": "prettier --write '{*.md,docs/**/*.md,src/**/*.md}'",
"i18n:report": "vue-cli-service i18n:report --src './src/**/*.?(js|vue)' --locales './src/locales/**/*.json'",
"lint:js": "vue-cli-service lint --no-fix",
"lint:md": "prettier --check '{*.md,docs/**/*.md,src/**/*.md}'",
"test": "vue-cli-service test:unit"
},
"dependencies": {
"clipboard-copy": "^3.1.0",
"core-js": "^3.6.4",
"deepmerge": "^4.2.2",
"leaflet": "^1.6.0",
"leaflet.heat": "^0.2.0",
"moment": "^2.24.0",
"vue": "^2.6.11",
"vue-ctk-date-time-picker": "^2.4.0",
"vue-feather-icons": "^5.0.0",
"vue-i18n": "^8.15.5",
"vue-js-modal": "^1.3.33",
"vue-mq": "^1.0.1",
"vue-outside-events": "^1.1.3",
"vue-router": "^3.1.6",
"vue2-leaflet": "^2.5.2",
"vuex": "^3.1.3"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.2.3",
"@vue/cli-plugin-eslint": "^4.2.3",
"@vue/cli-plugin-unit-jest": "^4.2.3",
"@vue/cli-service": "^4.2.3",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/test-utils": "1.0.0-beta.32",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^25.1.0",
"cors-anywhere": "^0.4.1",
"eslint": "^6.8.0",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-vue": "^6.2.2",
"jest-fetch-mock": "^3.0.3",
"lint-staged": "^10.0.8",
"moment-locales-webpack-plugin": "^1.1.2",
"node-sass": "^4.13.1",
"sass-loader": "^8.0.2",
"vue-cli-plugin-i18n": "^0.6.1",
"vue-template-compiler": "^2.6.11"
},
"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",
@@ -34,7 +28,7 @@ export default {
this.populateStateFromQuery(this.$route.query);
this.loadData();
// Update URL query params when relevant values changes
this.$store.subscribe(mutation => {
this.$store.subscribe((mutation) => {
if (
[
types.SET_SELECTED_USER,
@@ -73,7 +67,7 @@ export default {
selectedDevice: device,
} = this.$store.state;
const activeLayers = Object.keys(map.layers).filter(
key => map.layers[key] === true
(key) => map.layers[key] === true
);
const query = {
lat: map.center.lat,
@@ -95,3 +89,7 @@ export default {
},
};
</script>
<style lang="scss">
@import "styles/main";
</style>

View File

@@ -1,3 +1,4 @@
import config from "@/config";
import { log, logLevels } from "@/logging";
import { getApiUrl, getLocationHistoryCount } from "@/util";
@@ -6,19 +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).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");
@@ -31,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");
@@ -45,13 +57,13 @@ 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 => {
export const getDevices = async (users) => {
const devices = {};
await Promise.all(
users.map(async user => {
users.map(async (user) => {
const response = await fetchApi(`/api/0/list`, { user });
const json = await response.json();
const userDevices = json.results;
@@ -60,7 +72,7 @@ export const getDevices = async users => {
);
log("API", () => {
const devicesCount = Object.keys(devices)
.map(user => devices[user].length)
.map((user) => devices[user].length)
.reduce((a, b) => a + b, 0);
return (
`[getDevices] Fetched ${devicesCount} ` +
@@ -75,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 = {};
@@ -102,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",
() =>
@@ -136,20 +158,22 @@ 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 => {
Object.keys(devices).map(async (user) => {
locationHistory[user] = {};
await Promise.all(
devices[user].map(async device => {
devices[user].map(async (device) => {
locationHistory[user][device] = await getUserDeviceLocationHistory(
user,
device,
start,
end
end,
fetchOptions
);
})
);
@@ -171,7 +195,7 @@ export const getLocationHistory = async (devices, start, end) => {
*
* @param {WebSocketLocationCallback} [callback] Callback for location messages
*/
export const connectWebsocket = async callback => {
export const connectWebsocket = async (callback) => {
let url = getApiUrl("/ws/last");
url.protocol = url.protocol.replace("http", "ws");
url = url.href;
@@ -181,16 +205,17 @@ export const connectWebsocket = async callback => {
log("WS", "Connected");
ws.send("LAST");
};
ws.onclose = event => {
ws.onclose = (event) => {
log(
"WS",
`Disconnected unexpectedly (reason: ${event.reason ||
"unknown"}). Reconnecting in one second.`,
`Disconnected unexpectedly (reason: ${
event.reason || "unknown"
}). Reconnecting in one second.`,
logLevels.WARNING
);
setTimeout(connectWebsocket, 1000);
};
ws.onmessage = async msg => {
ws.onmessage = async (msg) => {
if (msg.data) {
try {
const data = JSON.parse(msg.data);

View File

@@ -1,5 +1,5 @@
<template>
<header>
<header :class="$mq === 'sm' ? 'header-sm' : null">
<div v-if="$mq === 'sm'" class="header-item">
<button
class="button button-flat button-icon"
@@ -32,7 +32,10 @@
</div>
<div class="nav-item">
<LayersIcon size="1x" aria-hidden="true" role="img" />
<Dropdown :label="$t('Layer settings')" :title="$t('Show/hide layers')">
<DropdownButton
:label="$t('Layer settings')"
:title="$t('Show/hide layers')"
>
<label v-for="option in layerSettingsOptions" :key="option.layer">
<input
type="checkbox"
@@ -46,39 +49,35 @@
/>
{{ option.label }}
</label>
</Dropdown>
</DropdownButton>
</div>
<div class="nav-item">
<CalendarIcon size="1x" aria-hidden="true" role="img" />
<VueCtkDateTimePicker
v-model="startDateTime"
:format="DATE_TIME_FORMAT"
:color="$config.primaryColor"
:locale="$config.locale"
:max-date="endDateTime"
:button-now-translation="$t('Now')"
<date-picker
v-model="dateTimeRange"
type="datetime"
format="YYYY-MM-DD HH:mm"
:editable="false"
:clearable="false"
:confirm="true"
:show-second="false"
:range="true"
range-separator=" "
:shortcuts="shortcuts"
:show-time-panel="showTimeRangePanel"
:disabled-date="(date, _) => date > new Date()"
@change="handleDateTimeRangeChange"
>
<button
type="button"
class="dropdown-button button"
:title="$t('Select start date')"
/>
</VueCtkDateTimePicker>
<span>{{ $t("to") }}</span>
<VueCtkDateTimePicker
v-model="endDateTime"
:format="DATE_TIME_FORMAT"
:color="$config.primaryColor"
:locale="$config.locale"
:min-date="startDateTime"
:button-now-translation="$t('Now')"
>
<button
type="button"
class="dropdown-button button"
:title="$t('Select end date')"
/>
</VueCtkDateTimePicker>
<template v-slot:footer>
<button
class="mx-btn toggle-date-btn"
type="button"
@click="toggleTimeRangePanel"
>
{{ showTimeRangePanel ? $t("Select date") : $t("Select time") }}
</button>
</template>
</date-picker>
</div>
<div class="nav-item">
<UserIcon size="1x" aria-hidden="true" role="img" />
@@ -90,7 +89,7 @@
<option :value="null">
{{ $t("Show all") }}
</option>
<option v-for="user in users" :value="user" :key="user">
<option v-for="user in users" :key="user" :value="user">
{{ user }}
</option>
</select>
@@ -107,8 +106,8 @@
</option>
<option
v-for="device in devices[selectedUser]"
:value="device"
:key="`${selectedUser}-${device}`"
:value="device"
>
{{ device }}
</option>
@@ -118,23 +117,19 @@
<nav class="header-item header-item-right">
<div
v-if="$config.showDistanceTravelled && distanceTravelled"
class="nav-item"
:title="$t('Distance travelled')"
class="nav-item distance-travelled"
>
{{ humanReadableDistance(distanceTravelled) }}
</div>
<div class="nav-item">
<button
class="button button-flat button-icon"
:title="$t('Download raw data')"
@click="$modal.show('download')"
>
<DownloadIcon
size="1x"
:aria-label="$t('Download raw data')"
role="img"
/>
</button>
<span :title="$t('Distance travelled')">
{{ humanReadableDistance(distanceTravelled) }}
</span>
<br />
<span :title="$t('Elevation gain / loss')">
<ArrowUpIcon size="0.8x" role="img" />
{{ humanReadableDistance(elevationGain) }}
/
<ArrowDownIcon size="0.8x" role="img" />
{{ humanReadableDistance(elevationLoss) }}
</span>
</div>
<div class="nav-item">
<button
@@ -153,35 +148,38 @@
import moment from "moment";
import { mapActions, mapGetters, mapMutations, mapState } from "vuex";
import {
ArrowDownIcon,
ArrowUpIcon,
CalendarIcon,
CrosshairIcon,
DownloadIcon,
InfoIcon,
LayersIcon,
MenuIcon,
SmartphoneIcon,
UserIcon,
} from "vue-feather-icons";
import VueCtkDateTimePicker from "vue-ctk-date-time-picker";
import "vue-ctk-date-time-picker/dist/vue-ctk-date-time-picker.css";
import Dropdown from "@/components/Dropdown";
import DatePicker from "vue2-datepicker";
import "vue2-datepicker/index.css";
import DropdownButton from "@/components/DropdownButton.vue";
import { DATE_TIME_FORMAT } from "@/constants";
import * as types from "@/store/mutation-types";
import { humanReadableDistance } from "@/util";
export default {
components: {
ArrowDownIcon,
ArrowUpIcon,
CalendarIcon,
CrosshairIcon,
DownloadIcon,
DatePicker,
InfoIcon,
LayersIcon,
MenuIcon,
SmartphoneIcon,
UserIcon,
VueCtkDateTimePicker,
Dropdown,
DropdownButton,
},
data() {
return {
@@ -191,12 +189,121 @@ export default {
{ layer: "line", label: this.$t("Show location history (line)") },
{ layer: "points", label: this.$t("Show location history (points)") },
{ layer: "heatmap", label: this.$t("Show location heatmap") },
{ layer: "poi", label: this.$t("Show points of interest") },
],
showMobileNav: false,
shortcuts: [
{
text: this.$t("Today"),
onClick() {
const end = new Date();
end.setHours(23, 59, 59, 0);
const start = new Date();
start.setHours(0, 0, 0, 0);
return [start, end];
},
},
{
text: this.$t("Yesterday"),
onClick() {
const end = new Date();
end.setDate(end.getDate() - 1);
end.setHours(23, 59, 59, 0);
const start = new Date(end);
start.setHours(0, 0, 0, 0);
return [start, end];
},
},
{
text: this.$t("3 days"),
onClick() {
const end = new Date();
end.setHours(23, 59, 59, 0);
const start = new Date();
start.setDate(end.getDate() - 3);
start.setHours(0, 0, 0, 0);
return [start, end];
},
},
{
text: this.$t("7 days"),
onClick() {
const end = new Date();
end.setHours(23, 59, 59, 0);
const start = new Date();
start.setDate(end.getDate() - 7);
start.setHours(0, 0, 0, 0);
return [start, end];
},
},
{
text: this.$t("15 days"),
onClick() {
const end = new Date();
end.setHours(23, 59, 59, 0);
const start = new Date();
start.setDate(end.getDate() - 15);
start.setHours(0, 0, 0, 0);
return [start, end];
},
},
{
text: this.$t("30 days"),
onClick() {
const end = new Date();
end.setHours(23, 59, 59, 0);
const start = new Date();
start.setDate(end.getDate() - 30);
start.setHours(0, 0, 0, 0);
return [start, end];
},
},
{
text: this.$t("3 months"),
onClick() {
const end = new Date();
end.setHours(23, 59, 59, 0);
const start = new Date();
start.setMonth(end.getMonth() - 3);
start.setHours(0, 0, 0, 0);
return [start, end];
},
},
{
text: this.$t("6 months"),
onClick() {
const end = new Date();
end.setHours(23, 59, 59, 0);
const start = new Date();
start.setMonth(end.getMonth() - 6);
start.setHours(0, 0, 0, 0);
return [start, end];
},
},
{
text: this.$t("1 year"),
onClick() {
const end = new Date();
end.setHours(23, 59, 59, 0);
const start = new Date();
start.setFullYear(end.getFullYear() - 1);
start.setHours(0, 0, 0, 0);
return [start, end];
},
},
],
showTimeRangePanel: false,
};
},
computed: {
...mapState(["users", "devices", "map", "distanceTravelled"]),
...mapState([
"users",
"devices",
"map",
"distanceTravelled",
"elevationGain",
"elevationLoss",
]),
selectedUser: {
get() {
return this.$store.state.selectedUser;
@@ -213,34 +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)
);
},
},
@@ -256,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

@@ -1,5 +1,5 @@
<template>
<LPopup>
<LPopup :options="options">
<div class="device">{{ deviceName }}</div>
<div class="wrapper">
<img
@@ -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,37 +40,27 @@
<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">
{{ $t("Regions:") }}
{{ regions.join(", ") }}
</div>
</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;
}
}
</style>
<script>
import {
BatteryIcon,
ClockIcon,
HomeIcon,
MapPinIcon,
WifiIcon,
ZapIcon,
} from "vue-feather-icons";
import { LPopup } from "vue2-leaflet";
@@ -75,6 +72,7 @@ export default {
ClockIcon,
HomeIcon,
MapPinIcon,
WifiIcon,
ZapIcon,
LPopup,
},
@@ -99,6 +97,14 @@ export default {
type: Number,
default: 0,
},
isoLocal: {
type: String,
default: "",
},
timeZone: {
type: String,
default: "",
},
lat: {
type: Number,
default: 0,
@@ -123,6 +129,18 @@ export default {
type: Number,
default: null,
},
regions: {
type: Array,
default: () => [],
},
wifi: {
type: Object,
default: () => {},
},
options: {
type: Object,
default: () => {},
},
},
computed: {
/**
@@ -146,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>`
@@ -94,7 +88,7 @@ export default {
this.parentContainer.addLayer(this, !this.visible);
this.$watch(
"latLng",
newVal => {
(newVal) => {
this.mapObject.setLatLngs(newVal);
},
{ deep: true }
@@ -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

@@ -3,17 +3,13 @@
<ul class="info-list">
<li>
<GithubIcon size="1x" aria-hidden="true" role="img" />
<a href="https://github.com/owntracks/frontend">
owntracks/frontend
</a>
<a href="https://github.com/owntracks/frontend">owntracks/frontend</a>
({{ frontendVersion }})
</li>
<li>
<GithubIcon size="1x" aria-hidden="true" role="img" />
<a href="https://github.com/owntracks/recorder">
owntracks/recorder
</a>
({{ recorderVersion || "Loading version..." }})
<a href="https://github.com/owntracks/recorder">owntracks/recorder</a>
({{ recorderVersion || $t("Loading version...") }})
</li>
<li>
<GlobeIcon size="1x" aria-hidden="true" role="img" />
@@ -28,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>
@@ -39,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

@@ -1,16 +1,43 @@
<template>
<modal name="loading" :click-to-close="false" adaptive>
<LoaderIcon class="loader" size="1.5x" />
<LoaderIcon class="loader-icon" size="1.5x" />
{{ $t("Loading data, please wait...") }}
<br />
<button
type="button"
class="button button-primary button-cancel"
@click="requestAbortController.abort()"
>
{{ $t("Cancel") }}
</button>
</modal>
</template>
<script>
import { mapState } from "vuex";
import { LoaderIcon } from "vue-feather-icons";
export default {
components: {
LoaderIcon,
},
computed: {
...mapState(["requestAbortController"]),
},
};
</script>
<style scoped>
.loader {
.loader-icon {
animation: spinning 2s linear infinite;
margin-right: 5px;
}
.button-cancel {
display: block;
margin: 20px auto 0;
}
@keyframes spinning {
from {
transform: rotate(0deg);
@@ -20,13 +47,3 @@
}
}
</style>
<script>
import { LoaderIcon } from "vue-feather-icons";
export default {
components: {
LoaderIcon,
},
};
</script>

View File

@@ -10,10 +10,14 @@ startDateTime.setHours(0, 0, 0, 0);
const DEFAULT_CONFIG = {
api: {
baseUrl: `${window.location.protocol}//${window.location.host}`,
fetchOptions: {},
},
endDateTime,
filters: {
minAccuracy: null,
},
ignorePingLocation: true,
locale: "en",
locale: "en-US",
map: {
attribution:
'&copy; <a href="https://osm.org/copyright">OpenStreetMap</a> contributors',
@@ -51,21 +55,34 @@ 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,
reloadHistory: false,
},
primaryColor: "#3f51b5",
router: {
basePath: "/",
},
selectedDevice: null,
selectedUser: null,
showDistanceTravelled: true,

View File

@@ -3,21 +3,35 @@ 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.split("-")[0],
fallbackLocale: "en",
locale: config.locale,
fallbackLocale: "en-US",
formatFallbackMessages: true,
messages,
});

43
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: {
@@ -42,23 +46,37 @@ 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;
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 +90,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: {
@@ -85,6 +103,7 @@ interface State {
heatmap: boolean;
last: boolean;
line: boolean;
poi: boolean;
points: boolean;
};
zoom: number;
@@ -127,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 */
@@ -138,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
@@ -148,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
*
@@ -181,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

@@ -11,7 +11,7 @@
"Show all": "Alle anzeigen",
"Select device": "Gerät auswählen",
"Distance travelled": "Gereiste Entfernung",
"Download raw data": "Rohdaten herunterladen",
"Elevation gain / loss": "Höhengewinn / -verlust",
"Information": "Information",
"Show last known locations": "Zeige letzte bekannte Standorte",
"Show location history (line)": "Zeige Standortverlauf (Linie)",
@@ -19,15 +19,27 @@
"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}",
"Timestamp": "Zeitstempel",
"Location": "Standort",
"Address": "Adresse",
"Battery": "Akku",
"Speed": "Geschwindigkeit"
"Speed": "Geschwindigkeit",
"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"
}

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

@@ -0,0 +1,48 @@
{
"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",
"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",
"Loading version...": "Loading version...",
"OwnTracks website": "OwnTracks website",
"OwnTracks documentation": "OwnTracks documentation",
"OwnTracks on Mastodon": "OwnTracks on Mastodon",
"Loading data, please wait...": "Loading data, please wait...",
"Cancel": "Cancel",
"Image of {deviceName}": "Image of {deviceName}",
"Timestamp": "Timestamp",
"Location": "Location",
"Address": "Address",
"Battery": "Battery",
"Speed": "Speed",
"Regions:": "Regions:",
"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

@@ -10,24 +10,39 @@
"Select user": "Select user",
"Show all": "Show all",
"Select device": "Select device",
"Distance travelled": "Distance travelled",
"Download raw data": "Download raw data",
"Distance travelled": "Distance traveled",
"Elevation gain / loss": "Elevation gain / loss",
"Information": "Information",
"Show last known locations": "Show last known locations",
"Show location history (line)": "Show location history (line)",
"Show location history (points)": "Show location history (points)",
"Show location heatmap": "Show location heatmap",
"Show points of interest": "Show points of interest",
"Minify JSON": "Minify JSON",
"Copy to clipboard": "Copy to clipboard",
"Download": "Download",
"Loading version...": "Loading version...",
"OwnTracks website": "OwnTracks website",
"OwnTracks documentation": "OwnTracks documentation",
"OwnTracks on Twitter": "OwnTracks on Twitter",
"OwnTracks on Mastodon": "OwnTracks on Mastodon",
"Loading data, please wait...": "Loading data, please wait...",
"Cancel": "Cancel",
"Image of {deviceName}": "Image of {deviceName}",
"Timestamp": "Timestamp",
"Location": "Location",
"Address": "Address",
"Battery": "Battery",
"Speed": "Speed"
"Speed": "Speed",
"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

@@ -11,7 +11,7 @@
"Show all": "Mostrar todos",
"Select device": "Seleccionar dispositivo",
"Distance travelled": "Distancia recorrida",
"Download raw data": "Descargar datos en crudo",
"Elevation gain / loss": "Aumento / disminución de la altura",
"Information": "Información",
"Show last known locations": "Mostrar última ubicación conocida",
"Show location history (line)": "Mostrar historial (línea)",
@@ -19,15 +19,27 @@
"Show location heatmap": "Mostra mapa de calor",
"Minify JSON": "Reducir JSON",
"Copy to clipboard": "Copiar al portapapeles",
"Download": "Descarga",
"Loading version...": "Cargando versión...",
"OwnTracks website": "OwnTracks - Sitio web",
"OwnTracks documentation": "OwnTracks - documentación",
"OwnTracks on Twitter": "OwnTracks en Twitter",
"OwnTracks on Mastodon": "OwnTracks en Mastodon",
"Loading data, please wait...": "Cargando datos, por favor, espera...",
"Cancel": "Cancelar",
"Image of {deviceName}": "Imágen de {deviceName}",
"Timestamp": "Fecha / Hora",
"Location": "Ubicación",
"Address": "Dirección",
"Battery": "Bateria",
"Speed": "Velocidad"
"Speed": "Velocidad",
"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"
}

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

@@ -0,0 +1,45 @@
{
"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",
"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",
"Loading version...": "Chargement de la version...",
"OwnTracks website": "Site d'OwnTracks",
"OwnTracks documentation": "Documentation d'OwnTracks",
"OwnTracks on Mastodon": "OwnTracks sur Mastodon",
"Loading data, please wait...": "Chargement des données, merci de patienter ...",
"Cancel": "Annuler",
"Image of {deviceName}": "Image de {deviceName}",
"Timestamp": "Horodatage",
"Location": "Localisation",
"Address": "Addresse",
"Battery": "Batterie",
"Speed": "Vitesse",
"Regions:": "Régions:",
"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

@@ -29,5 +29,5 @@ new Vue({
i18n,
router,
store,
render: h => h(App),
render: (h) => h(App),
}).$mount("#app");

View File

@@ -1,12 +1,13 @@
import Vue from "vue";
import Router from "vue-router";
import Map from "./views/Map.vue";
import config from "@/config";
import Map from "@/views/Map.vue";
Vue.use(Router);
export default new Router({
mode: "history",
base: process.env.BASE_URL,
base: config.router.basePath,
routes: [
{
path: "/",

View File

@@ -43,7 +43,7 @@ const populateStateFromQuery = ({ state, commit }, query) => {
}
if (query.layers) {
const activeLayers = query.layers.split(",");
Object.keys(state.map.layers).forEach(layer => {
Object.keys(state.map.layers).forEach((layer) => {
const visibility = activeLayers.includes(layer);
if (state.map.layers[layer] !== visibility) {
commit(types.SET_MAP_LAYER_VISIBILITY, { layer, visibility });
@@ -116,33 +116,41 @@ const getLastLocations = async ({ commit, state }) => {
// Remove ping/ping from the owntracks/recorder Docker image
// https://github.com/owntracks/frontend/issues/12
lastLocations = lastLocations.filter(
l => !(l.username === "ping" && l.device === "ping")
(l) => !(l.username === "ping" && l.device === "ping")
);
}
commit(types.SET_LAST_LOCATIONS, lastLocations);
};
const _getDistanceTravelled = locationHistory => {
const _getTravelStats = (locationHistory) => {
const start = Date.now();
let distanceTravelled = 0;
Object.keys(locationHistory).forEach(user => {
Object.keys(locationHistory[user]).forEach(device => {
let elevationGain = 0;
let elevationLoss = 0;
Object.keys(locationHistory).forEach((user) => {
Object.keys(locationHistory[user]).forEach((device) => {
let lastLatLng = null;
locationHistory[user][device].forEach(coordinate => {
const latLng = L.latLng(coordinate.lat, coordinate.lon);
locationHistory[user][device].forEach((location) => {
if (
config.filters.minAccuracy !== null &&
location.acc > config.filters.minAccuracy
)
return;
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;
@@ -150,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 };
};
/**
@@ -176,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

@@ -4,20 +4,46 @@ import config from "@/config";
import { distanceBetweenCoordinates } from "@/util";
/**
* From the selected users' and devices' location histories, create an
* array of all coordinates.
* Apply filters to the selected users' and devices' location histories.
*
* @param {State} state
* @param {LocationHistory} state.locationHistory
* Location history of selected users and devices
* @returns {LocationHistory} Filtered location history
*/
const filteredLocationHistory = (state) => {
const locationHistory = {};
Object.keys(state.locationHistory).forEach((user) => {
locationHistory[user] = {};
Object.keys(state.locationHistory[user]).forEach((device) => {
locationHistory[user][device] = [];
state.locationHistory[user][device].forEach((location) => {
if (
config.filters.minAccuracy !== null &&
location.acc > config.filters.minAccuracy
)
return;
locationHistory[user][device].push(location);
});
});
});
return locationHistory;
};
/**
* From the selected users' and devices' location histories, create an
* array of all coordinates.
*
* @param {State} state
* @returns {L.LatLng[]} All coordinates
*/
const locationHistoryLatLngs = state => {
const filteredLocationHistoryLatLngs = (state) => {
const latLngs = [];
Object.keys(state.locationHistory).forEach(user => {
Object.keys(state.locationHistory[user]).forEach(device => {
state.locationHistory[user][device].forEach(coordinate => {
latLngs.push(L.latLng(coordinate.lat, coordinate.lon));
const locationHistory = filteredLocationHistory(state);
Object.keys(locationHistory).forEach((user) => {
Object.keys(locationHistory[user]).forEach((device) => {
locationHistory[user][device].forEach((location) => {
latLngs.push(L.latLng(location.lat, location.lon));
});
});
});
@@ -30,17 +56,16 @@ const locationHistoryLatLngs = state => {
* coordinates does not exceed `config.map.maxPointDistance`.
*
* @param {State} state
* @param {LocationHistory} state.locationHistory
* Location history of selected users and devices
* @returns {L.LatLng[][]} Groups of coherent coordinates
*/
const locationHistoryLatLngGroups = state => {
const filteredLocationHistoryLatLngGroups = (state) => {
const groups = [];
Object.keys(state.locationHistory).forEach(user => {
Object.keys(state.locationHistory[user]).forEach(device => {
const locationHistory = filteredLocationHistory(state);
Object.keys(locationHistory).forEach((user) => {
Object.keys(locationHistory[user]).forEach((device) => {
let latLngs = [];
state.locationHistory[user][device].forEach(coordinate => {
const latLng = L.latLng(coordinate.lat, coordinate.lon);
locationHistory[user][device].forEach((location) => {
const latLng = L.latLng(location.lat, location.lon);
// Skip if group splitting is disabled or this is the first
// coordinate in the current group
if (
@@ -68,6 +93,7 @@ const locationHistoryLatLngGroups = state => {
};
export default {
locationHistoryLatLngs,
locationHistoryLatLngGroups,
filteredLocationHistory,
filteredLocationHistoryLatLngs,
filteredLocationHistoryLatLngGroups,
};

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

@@ -9,12 +9,14 @@
--color-background: #fff;
--color-primary: #3f51b5;
--color-primary-text: #fff;
--color-separator: #ddd;
--drop-shadow: drop-shadow(0 10px 10px rgb(0, 0, 0, 0.2));
--dropdown-arrow: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2225%22%20height%3D%2210%22%3E%3Cpath%20fill%3D%22%23333%22%20fill-opacity%3D%221%22%20stroke%3D%22none%22%20d%3D%22M0%2C0%20L0%2C0%20L1%2C0%20L1%2C6%20L7%2C6%20L7%2C7%20L0%2C7%20z%22%20transform%3D%22rotate(-45%205%200)%22%20%2F%3E%3C%2Fsvg%3E");
--pin-width: 32px;
}
html, body {
html,
body {
height: 100%;
}
@@ -84,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 {
@@ -98,14 +101,21 @@ 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;
color: var(--color-primary-text);
background: var(--color-primary);
&.header-sm {
padding: 10px;
.header-item:not(.nav-sm) .nav-item:not(:first-child) {
margin-left: 10px;
}
}
.header-item {
display: flex;
align-items: center;
@@ -152,19 +162,56 @@ pre {
left: 0;
margin: 0;
overflow-x: auto;
padding: 30px;
padding: 20px;
position: absolute;
right: 0;
top: 76px;
top: 56px;
z-index: 1;
.nav-item {
align-items: center;
display: flex;
flex-wrap: wrap;
margin-left: auto;
margin-right: auto;
max-width: 400px;
&:not(:first-child) {
margin-left: 0;
margin-top: 20px;
}
> .button,
> .mx-datepicker,
> .mx-input,
> .dropdown {
flex: 1;
}
> .dropdown .dropdown-button {
width: 100%;
}
.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 {
flex-basis: 100%;
margin: 0;
}
@@ -257,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,19 +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: 415px) {
// Fix buttons being off screen
.datepicker-buttons-container {
bottom: 0;
position: absolute;
width: 100%;
}
@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

@@ -4,7 +4,10 @@
.leaflet-popup {
filter: var(--drop-shadow);
margin-bottom: calc(var(--pin-width) * 1.5 + 20px);
&--for-pin {
margin-bottom: calc(var(--pin-width) * 1.5 + 20px);
}
.leaflet-popup-content-wrapper {
border-radius: 3px;
@@ -12,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

@@ -10,9 +10,9 @@ import { DATE_TIME_FORMAT, EARTH_RADIUS_IN_KM } from "@/constants";
* @param {String} path Path to the API resource
* @returns {URL} Final API URL
*/
export const getApiUrl = path => {
export const getApiUrl = (path) => {
const normalizedBaseUrl = config.api.baseUrl.endsWith("/")
? config.api.baseUrl.slice(1)
? config.api.baseUrl.slice(0, -1)
: config.api.baseUrl;
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
return new URL(`${normalizedBaseUrl}${normalizedPath}`);
@@ -24,7 +24,7 @@ export const getApiUrl = path => {
* @param {String} s Input value to be tested
* @returns {Boolean} Whether the input matches the expected format
*/
export const isIsoDateTime = s => moment(s, DATE_TIME_FORMAT, true).isValid();
export const isIsoDateTime = (s) => moment(s, DATE_TIME_FORMAT, true).isValid();
/**
* Convert degrees to radians.
@@ -32,7 +32,7 @@ export const isIsoDateTime = s => moment(s, DATE_TIME_FORMAT, true).isValid();
* @param {Number} degrees Angle in degrees
* @returns {Number} Angle in radians
*/
export const degreesToRadians = degrees => (degrees * Math.PI) / 180;
export const degreesToRadians = (degrees) => (degrees * Math.PI) / 180;
/**
* Calculate the distance between two coordinates. Uses the haversine formula,
@@ -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.
*
@@ -91,7 +73,7 @@ export const download = (text, filename, mimeType = "text/plain") => {
* @param {Number} distance Distance in meters
* @returns {String} Formatted string including unit
*/
export const humanReadableDistance = distance => {
export const humanReadableDistance = (distance) => {
let unit = "m";
if (Math.abs(distance) >= 1000) {
distance = distance / 1000;
@@ -108,11 +90,11 @@ export const humanReadableDistance = distance => {
* @param {LocationHistory} locationHistory Location history
* @returns {Number} Total number of locations
*/
export const getLocationHistoryCount = locationHistory =>
export const getLocationHistoryCount = (locationHistory) =>
Object.keys(locationHistory)
.map(user =>
.map((user) =>
Object.keys(locationHistory[user])
.map(device => locationHistory[user][device].length)
.map((device) => locationHistory[user][device].length)
.reduce((a, b) => a + b, 0)
)
.reduce((a, b) => a + b, 0);

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,58 +102,25 @@
: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 }"
: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 locationHistoryLatLngGroups"
:key="i"
:lat-lngs="group"
v-bind="polyline"
/>
</template>
<template v-if="map.layers.points">
<template v-for="(userDevices, user) in locationHistory">
<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"
></LDeviceLocationPopup>
</LCircleMarker>
</template>
</template>
</template>
<template v-if="map.layers.heatmap">
<LHeatmap
v-if="locationHistoryLatLngs.length"
:lat-lng="locationHistoryLatLngs"
v-if="filteredLocationHistoryLatLngs.length"
:lat-lng="filteredLocationHistoryLatLngs"
:max="heatmap.max"
:radius="heatmap.radius"
:blur="heatmap.blur"
@@ -118,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: {
@@ -137,6 +162,7 @@ export default {
LPolyline,
LDeviceLocationPopup,
LHeatmap,
LTooltip,
},
data() {
return {
@@ -147,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,
@@ -159,21 +187,36 @@ 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,
},
};
},
computed: {
...mapGetters([
"filteredLocationHistory",
"filteredLocationHistoryLatLngs",
"filteredLocationHistoryLatLngGroups",
]),
...mapState(["lastLocations", "map"]),
},
watch: {
lastLocations() {
if (this.$config.onLocationChange.fitView) {
this.fitView();
}
},
filteredLocationHistory() {
this.fitView();
},
},
mounted() {
this.$root.$on("fitView", () => {
this.fitView();
});
},
computed: {
...mapGetters(["locationHistoryLatLngs", "locationHistoryLatLngGroups"]),
...mapState(["lastLocations", "locationHistory", "map"]),
},
methods: {
...mapMutations({
setMapCenter: types.SET_MAP_CENTER,
@@ -186,14 +229,15 @@ export default {
if (
(this.map.layers.line ||
this.map.layers.points ||
this.map.layers.poi ||
this.map.layers.heatmap) &&
this.locationHistoryLatLngs.length > 0
this.filteredLocationHistoryLatLngs.length > 0
) {
this.$refs.map.mapObject.fitBounds(
new L.LatLngBounds(this.locationHistoryLatLngs)
new L.LatLngBounds(this.filteredLocationHistoryLatLngs)
);
} else if (this.map.layers.last && this.lastLocations.length > 0) {
const locations = this.lastLocations.map(l => L.latLng(l.lat, l.lon));
const locations = this.lastLocations.map((l) => L.latLng(l.lat, l.lon));
this.$refs.map.mapObject.fitBounds(new L.LatLngBounds(locations), {
maxZoom: this.maxNativeZoom,
});
@@ -211,25 +255,17 @@ export default {
*/
deviceLocationsWithNameAndFace(user, device, deviceLocations) {
const lastLocation = this.lastLocations.find(
l => l.username === user && l.device === device
(l) => l.username === user && l.device === device
);
if (!lastLocation) {
return deviceLocations;
}
return deviceLocations.map(l => ({
return deviceLocations.map((l) => ({
...l,
name: lastLocation.name,
face: lastLocation.face,
}));
},
},
watch: {
lastLocations() {
this.fitView();
},
locationHistory() {
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", () => {
@@ -22,6 +24,13 @@ describe("getApiUrl", () => {
expect(getApiUrl("/foo/bar").href).toBe(
"http://example.com/owntracks/foo/bar"
);
config.api.baseUrl = "http://example.com/owntracks/";
expect(getApiUrl("foo").href).toBe("http://example.com/owntracks/foo");
expect(getApiUrl("/foo").href).toBe("http://example.com/owntracks/foo");
expect(getApiUrl("/foo/bar").href).toBe(
"http://example.com/owntracks/foo/bar"
);
});
});

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,27 +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 = {
configureWebpack: {
plugins: [
new webpack.DefinePlugin({
"process.env": {
PACKAGE_VERSION: `"${version}"`,
},
}),
new MomentLocalesPlugin(),
],
},
pluginOptions: {
i18n: {
locale: "en",
fallbackLocale: "en",
localeDir: "locales",
},
},
};

11823
yarn.lock

File diff suppressed because it is too large Load Diff