28 Commits

Author SHA1 Message Date
Linus Groh
8dc9611a77 Release 2.0.0-beta.9 2020-02-06 21:30:41 +00:00
Linus Groh
c1f58c992e Support locale with language and region part 2020-02-06 21:26:21 +00:00
Linus Groh
6631929d6f Update screenshot 2020-02-06 21:13:11 +00:00
Linus Groh
36281db2e3 Add .github/FUNDING.yml 2020-02-06 20:35:08 +00:00
Linus Groh
5a8d261943 Fix typo in docs/config.md 2020-02-06 20:23:08 +00:00
Linus Groh
7b83349dc8 Improve CHANGELOG.md 2020-02-06 20:22:48 +00:00
Linus Groh
bc3670df99 Release 2.0.0-beta.8 2020-01-26 00:57:08 +00:00
Linus Groh
95613753a9 Enable ESLint max-len rule 2020-01-26 00:49:15 +00:00
Linus Groh
cfa3052a0a Show name and face on location history popups 2020-01-26 00:40:30 +00:00
Linus Groh
0bd84f4de5 s/@return/@returns 2020-01-26 00:38:36 +00:00
Linus Groh
85e51643bf Add missing alt/title to device face image 2020-01-25 23:37:41 +00:00
Linus Groh
6cbdf30580 Use computed prop for device name in location popup 2020-01-25 23:33:10 +00:00
Linus Groh
988b10de40 Release 2.0.0-beta.7 2020-01-24 21:20:22 +00:00
Linus Groh
639e96cae8 Upgrade dependencies 2020-01-24 20:43:21 +00:00
Linus Groh
df3dcb60d8 Update year in LICENSE 2020-01-05 11:52:40 +00:00
Linus Groh
09ce3b7861 Lint code on Travis CI 2019-12-16 21:20:31 +00:00
Linus Groh
04f0b65480 Separate linting and formatting 2019-12-16 21:16:33 +00:00
Linus Groh
d3e3b82a13 Lint/format markdown files 2019-12-16 21:06:01 +00:00
Linus Groh
769185ee5a Set no-console/no-debugger to "warn" in dev mode 2019-12-16 20:24:15 +00:00
Linus Groh
7b6641e70d Add jsconfig.json 2019-12-16 19:35:17 +00:00
Linus Groh
2827d85865 Release 2.0.0-beta.6 2019-12-14 19:39:56 +00:00
Linus Groh
693947c064 Fix heatmap 2019-12-14 19:35:24 +00:00
Linus Groh
a31c048060 Release 2.0.0-beta.5 2019-12-14 17:13:13 +00:00
Linus Groh
1f2be0aeb9 Upgrade dependencies 2019-12-14 17:10:22 +00:00
Linus Groh
92401eb6b1 Add $config Vue instance property 2019-12-14 16:59:12 +00:00
Linus Groh
8d2f22d3de Use configured locale for timestamp formatting 2019-12-14 16:48:18 +00:00
Linus Groh
1d106e45da Add Leaflet popup close button bg color transition 2019-12-14 16:33:17 +00:00
Linus Groh
1f07ae9266 Improve accessibility
* More title attributes
* Usage of aria-hidden and role attributes
* Focus style improvements
* Text contrast improvements
2019-12-14 16:32:10 +00:00
32 changed files with 901 additions and 473 deletions

View File

@@ -5,8 +5,14 @@ module.exports = {
},
extends: ["plugin:vue/essential", "@vue/prettier"],
rules: {
"no-console": process.env.NODE_ENV === "production" ? "error" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
"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",
{

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
liberapay: owntracks.org

View File

@@ -3,4 +3,6 @@ node_js:
- 10
cache: yarn
script:
- yarn lint:js
- yarn lint:md
- yarn test

View File

@@ -1,24 +1,64 @@
# 2.0.0-beta.4 (2019-12-14)
# Changelog
- Add support for time selection
## 2.0.0-beta.9 (2020-02-06)
- Support locale with language and region part (`en-GB`)
- Update docs (screenshot, changelog improvements, typo fix)
- Add funding information
## 2.0.0-beta.8 (2020-01-26)
- Add friendly device name and face images to location history popups
- Add missing `alt`/`title` to device face image
- Fix all JSDoc `@return` directives to `@returns`
- Use computed prop for device name in location popup
- Enable ESLint `max-len` rule
## 2.0.0-beta.7 (2020-01-24)
This release doesn't really affect end-users but greatly improves the development experience.
- Add `jsconfig.json`
- Set `no-console`/`no-debugger` to `"warn"` in dev mode
- Linting and formatting:
- Separate npm scripts for linting and formatting
- Lint/format Markdown files
- Run lint on Travis CI
- Upgrade dependencies
## 2.0.0-beta.6 (2019-12-14)
- Fix heatmap - the upgrade of `vue2-leaflet` from 2.2.1 to 2.3.0 added an `activated` attribute to layers causing the heatmap to not show ([#18](https://github.com/owntracks/frontend/issues/18))
## 2.0.0-beta.5 (2019-12-14)
- Add Leaflet popup close button background color transition
- Add `$config` Vue instance property
- Improve accessibility ([#9](https://github.com/owntracks/frontend/issues/9))
- Use configured locale for timestamp formatting
- Upgrade dependencies
## 2.0.0-beta.4 (2019-12-14)
- Add support for time selection ([#10](https://github.com/owntracks/frontend/issues/10))
- New date/time picker component is properly translated/localised and keyboard accessible
- Config options are now `startDateTime`/`endDateTime` and format of URL parameters changed
- Changed default start/end date and time to use local timezone
- Fix missing translation of "[date] to [date]"
- Update i18n development notes in `README.md`
# 2.0.0-beta.3 (2019-12-13)
## 2.0.0-beta.3 (2019-12-13)
- Add i18 support (currently English and German, `locale` config option)
- Add custom checkbox focus style
- Fix layer dropdown issues
- Fix layer dropdown issues ([#1](https://github.com/owntracks/frontend/issues/1))
- Fix checkbox style issues
- Fix hover/focus inconsistencies
- Fix Docker image labels
- `README.md` enhancements
- Upgrade dependencies
# 2.0.0-beta.2 (2019-11-02)
## 2.0.0-beta.2 (2019-11-02)
- Add `onLocationChange.reloadHistory` config option
- Add Travis CI config
@@ -27,7 +67,7 @@
- Fix table of content links in config documentation
- Upgrade dependencies
# 2.0.0-beta.1 (2019-10-26)
## 2.0.0-beta.1 (2019-10-26)
- Convert codebase to Node.js based development workflow, including:
- Package management using yarn
@@ -43,22 +83,22 @@
- Custom checkbox styles
- Switch from Font Awesome 4 to Feather Icons
- Application now uses Vuex and Vue Router
- Add URL query parameters to load and preserve application state: `lat`, `lng`, `zoom`, `start`, `end`, `user`, `device` and `layers`.
- Add a loading indicator.
- Add 'download data' modal, currently supporting formatted and minified JSON.
- Add a verbose mode.
- Add CORS proxy script toeasily use a production instance of the OwnTracks recorder in development.
- Add unit tests for util and API functions.
- Add URL query parameters to load and preserve application state: `lat`, `lng`, `zoom`, `start`, `end`, `user`, `device` and `layers`
- Add a loading indicator
- Add 'download data' modal, currently supporting formatted and minified JSON
- Add a verbose mode
- Add CORS proxy script to easily use a production instance of the OwnTracks recorder in development
- Add unit tests for util and API functions
- Add documentation for all public funtions
- Add documentation for all configuration options.
- Add more configuration options, including setting the API base URL ([#4](https://github.com/owntracks/frontend/issues/4)) and hiding the `ping/ping` location ([#12](https://github.com/owntracks/frontend/issues/12)).
- Add documentation for all configuration options
- Add more configuration options, including setting the API base URL ([#4](https://github.com/owntracks/frontend/issues/4)) and hiding the `ping/ping` location ([#12](https://github.com/owntracks/frontend/issues/12))
# 1.1.0 (2019-10-26)
## 1.1.0 (2019-10-26)
- Add support for Docker. [#7](https://github.com/owntracks/frontend/pull/7), [@sharkoz](https://github.com/sharkoz)
- Move project to the OwnTracks organisation on GitHub. [#8](https://github.com/owntracks/frontend/pull/8), [@jpmens](https://github.com/jpmens)
- Enable compression in nginx configuration used in Docker image. [#11](https://github.com/owntracks/frontend/pull/11), [@sharkoz](https://github.com/sharkoz)
- Add support for Docker ([#7](https://github.com/owntracks/frontend/pull/7), [@sharkoz](https://github.com/sharkoz))
- Move project to the OwnTracks organisation on GitHub ([#8](https://github.com/owntracks/frontend/pull/8), [@jpmens](https://github.com/jpmens))
- Enable compression in nginx configuration used in Docker image ([#11](https://github.com/owntracks/frontend/pull/11), [@sharkoz](https://github.com/sharkoz))
# 1.0.0 (2019-06-18)
## 1.0.0 (2019-06-18)
- Initial release

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019 Linus Groh
Copyright (c) 2019-2020 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

@@ -46,7 +46,7 @@ Or you can use `docker-compose` (if you also run the OwnTracks Recorder with the
compose config, and the service is named `otrecorder`):
```yaml
version: '3'
version: "3"
services:
owntracks-ui:
@@ -95,7 +95,10 @@ See [`docs/config.md`](docs/config.md) for all available options.
- Run `yarn install` to install dependencies
- Run `yarn serve` to compile for development and start the hot-reload server
- Run `yarn lint` to lint and fix files
- 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
### CORS-Proxy
@@ -106,6 +109,7 @@ instance of OwnTracks for development without making changes to its CORS-Headers
```console
$ yarn cors-proxy
```
If you have [basic authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#Basic_authentication_scheme)
enabled, create a `.env` file with your credentials:
@@ -152,7 +156,6 @@ where German translations were added.
<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

@@ -6,7 +6,7 @@ COPY . ./
RUN yarn build
FROM nginx:1.17-alpine
LABEL version="2.0.0-beta.4"
LABEL version="2.0.0-beta.9"
LABEL description="OwnTracks UI"
LABEL maintainer="Linus Groh <mail@linusgroh.de>"
ENV LISTEN_PORT=80 \

View File

@@ -123,7 +123,9 @@ Remove the `ping/ping` location from the fetched data. This is useful when using
### `locale`
The language to use for the user interface. Available: `de` (German), `en` (English).
The locale to use for the user interface, this affects the language and date/time
formats. Available languages are `de` (German), `en` (English). You can use formats
like `en-GB`, `en-US`, `de-DE`.
- Type: [`String`]
- Default: `"en"`
@@ -362,7 +364,8 @@ and [this Wikipedia article](https://en.wikipedia.org/wiki/Tiled_web_map).
// Use dark HDPI tiles from Mapbox
window.owntracks.config = {
map: {
url: "https://api.mapbox.com/v4/mapbox.dark/{z}/{x}/{y}@2x.png?access_token=xxxxxxxxxxxxxxxx"
url:
"https://api.mapbox.com/v4/mapbox.dark/{z}/{x}/{y}@2x.png?access_token=xxxxxxxxxxxxxxxx"
}
};
```
@@ -399,7 +402,7 @@ Primary color for the user interface (navigation bar and various map elements).
### `selectedDevice`
Initial selected device. All devices will be shown by default if `null`. Will be ignored
if [`selectedUser`](#selectedUser) is `null`;
if [`selectedUser`](#selectedUser) is `null`.
Only data for the selected user/device will be fetched, so you can use this to limit the
amount of data fetched after page load.
@@ -456,9 +459,9 @@ Whether to enable verbose mode or not.
- Type: [`Boolean`]
- Default: `false`
[`Boolean`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[`Date`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date
[`Number`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number
[`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
[`boolean`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[`date`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date
[`number`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number
[`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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 452 KiB

After

Width:  |  Height:  |  Size: 653 KiB

9
jsconfig.json Normal file
View File

@@ -0,0 +1,9 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules"]
}

View File

@@ -1,6 +1,6 @@
{
"name": "owntracks-ui",
"version": "2.0.0-beta.4",
"version": "2.0.0-beta.9",
"author": {
"name": "Linus Groh",
"email": "mail@linusgroh.de"
@@ -8,49 +8,52 @@
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"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.4.8",
"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.6",
"vue": "^2.6.11",
"vue-ctk-date-time-picker": "^2.4.0",
"vue-feather-icons": "^5.0.0",
"vue-i18n": "^8.0.0",
"vue-i18n": "^8.15.3",
"vue-js-modal": "^1.3.31",
"vue-outside-events": "^1.1.3",
"vue-router": "^3.1.3",
"vue2-leaflet": "^2.2.1",
"vue-router": "^3.1.5",
"vue2-leaflet": "^2.4.2",
"vuex": "^3.1.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.1.1",
"@vue/cli-plugin-eslint": "^4.1.1",
"@vue/cli-plugin-unit-jest": "^4.1.1",
"@vue/cli-service": "^4.1.1",
"@vue/cli-plugin-babel": "^4.1.2",
"@vue/cli-plugin-eslint": "^4.1.2",
"@vue/cli-plugin-unit-jest": "^4.1.2",
"@vue/cli-service": "^4.1.2",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/test-utils": "1.0.0-beta.30",
"@vue/test-utils": "1.0.0-beta.31",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "^10.0.3",
"babel-jest": "^24.9.0",
"babel-jest": "^25.1.0",
"cors-anywhere": "^0.4.1",
"eslint": "^6.7.2",
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-vue": "^6.0.1",
"jest-fetch-mock": "^2.1.2",
"lint-staged": "^9.5.0",
"eslint": "^6.8.0",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-vue": "^6.1.2",
"jest-fetch-mock": "^3.0.1",
"lint-staged": "^10.0.2",
"moment-locales-webpack-plugin": "^1.1.2",
"node-sass": "^4.13.0",
"sass-loader": "^8.0.0",
"vue-cli-plugin-i18n": "^0.6.0",
"vue-template-compiler": "^2.5.21"
"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": {

View File

@@ -17,7 +17,6 @@
<script>
import { mapActions } from "vuex";
import config from "@/config";
import * as types from "@/store/mutation-types";
import { log } from "@/logging";
import AppHeader from "@/components/AppHeader";
@@ -30,7 +29,7 @@ export default {
created() {
document.documentElement.style.setProperty(
"--color-primary",
config.primaryColor
this.$config.primaryColor
);
this.populateStateFromQuery(this.$route.query);
this.loadData();

View File

@@ -18,19 +18,19 @@ import { getApiUrl } from "@/util";
*
* @param {String} path API resource path
* @param {QueryParams} [params] Query parameters
* @return {Promise} Promise returned by the fetch function
* @returns {Promise} Promise returned by the fetch function
*/
const fetchApi = (path, params = {}) => {
const url = getApiUrl(path);
Object.keys(params).forEach(key => url.searchParams.append(key, params[key]));
log("HTTP", `GET ${url.href}`);
return fetch(url).catch(error => log("HTTP", error, logLevels.ERROR));
return fetch(url.href).catch(error => log("HTTP", error, logLevels.ERROR));
};
/**
* Get the recorder's version.
*
* @return {String} Version
* @returns {String} Version
*/
export const getVersion = async () => {
const response = await fetchApi("/api/0/version");
@@ -42,7 +42,7 @@ export const getVersion = async () => {
/**
* Get all users.
*
* @return {Array.<User>} Array of usernames
* @returns {Array.<User>} Array of usernames
*/
export const getUsers = async () => {
const response = await fetchApi("/api/0/list");
@@ -55,7 +55,8 @@ export const getUsers = async () => {
* Get all devices for the provided users.
*
* @param {Array.<User>} users Array of usernames
* @return {Object.<User, Array.<Device>>} Object mapping each username to an array of device names
* @returns {Object.<User, Array.<Device>>}
* Object mapping each username to an array of device names
*/
export const getDevices = async users => {
const devices = {};
@@ -75,7 +76,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
* @return {Array.<LastLocation>} Array of last location objects
* @returns {Array.<LastLocation>} Array of last location objects
*/
export const getLastLocations = async (user, device) => {
const params = {};
@@ -97,7 +98,7 @@ 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
* @return {LocationHistory} Array of location history objects
* @returns {LocationHistory} Array of location history objects
*/
export const getUserDeviceLocationHistory = async (
user,
@@ -119,10 +120,12 @@ export const getUserDeviceLocationHistory = async (
/**
* Get the location history of multiple devices.
*
* @param {Object.<User, Array.<Device>>} devices Devices of which the history should be fetched
* @param {Object.<User, Array.<Device>>} devices
* 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
* @return {Object.<User, Object.<Device, LocationHistory>>} Array of location history objects
* @returns {Object.<User, Object.<Device, LocationHistory>>}
* Array of location history objects
*/
export const getLocationHistory = async (devices, start, end) => {
const locationHistory = {};
@@ -145,7 +148,8 @@ export const getLocationHistory = async (devices, start, end) => {
};
/**
* Connect to the WebSocket API, reconnect when necessary and handle received messages.
* Connect to the WebSocket API, reconnect when necessary and handle received
* messages.
*
* @param {webSocketLocationCallback} [callback] Callback for location messages
*/

View File

@@ -13,7 +13,7 @@
</button>
</div>
<div class="nav-item">
<LayersIcon size="1x" />
<LayersIcon size="1x" aria-hidden="true" role="img" />
<Dropdown :label="$t('Layer settings')" :title="$t('Show/hide layers')">
<label v-for="option in layerSettingsOptions" :key="option.layer">
<input
@@ -31,12 +31,12 @@
</Dropdown>
</div>
<div class="nav-item">
<CalendarIcon size="1x" />
<CalendarIcon size="1x" aria-hidden="true" role="img" />
<VueCtkDateTimePicker
v-model="startDateTime"
:format="DATE_TIME_FORMAT"
:color="config.primaryColor"
:locale="config.locale"
:color="$config.primaryColor"
:locale="$config.locale"
:max-date="endDateTime"
:button-now-translation="$t('Now')"
>
@@ -50,8 +50,8 @@
<VueCtkDateTimePicker
v-model="endDateTime"
:format="DATE_TIME_FORMAT"
:color="config.primaryColor"
:locale="config.locale"
:color="$config.primaryColor"
:locale="$config.locale"
:min-date="startDateTime"
:button-now-translation="$t('Now')"
>
@@ -63,7 +63,7 @@
</VueCtkDateTimePicker>
</div>
<div class="nav-item">
<UserIcon size="1x" />
<UserIcon size="1x" aria-hidden="true" role="img" />
<select
v-model="selectedUser"
class="dropdown-button button"
@@ -78,7 +78,7 @@
</select>
</div>
<div v-if="selectedUser" class="nav-item">
<SmartphoneIcon size="1x" />
<SmartphoneIcon size="1x" aria-hidden="true" role="img" />
<select
v-model="selectedDevice"
class="dropdown-button button"
@@ -104,7 +104,11 @@
:title="$t('Download raw data')"
@click="$modal.show('download')"
>
<DownloadIcon size="1x" />
<DownloadIcon
size="1x"
:aria-label="$t('Download raw data')"
role="img"
/>
</button>
</div>
<div class="nav-item">
@@ -113,7 +117,7 @@
:title="$t('Information')"
@click="$modal.show('information')"
>
<InfoIcon size="1x" />
<InfoIcon size="1x" :aria-label="$t('Information')" role="img" />
</button>
</div>
</nav>
@@ -135,7 +139,6 @@ 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 config from "@/config";
import { DATE_TIME_FORMAT } from "@/constants";
import * as types from "@/store/mutation-types";
@@ -153,7 +156,6 @@ export default {
data() {
return {
DATE_TIME_FORMAT,
config,
layerSettingsOptions: [
{ layer: "last", label: this.$t("Show last known locations") },
{ layer: "line", label: this.$t("Show location history (line)") },

View File

@@ -1,32 +1,36 @@
<template>
<LPopup>
<div v-if="name" class="device">{{ name }}</div>
<div v-else class="device">{{ user }}/{{ device }}</div>
<div class="device">{{ deviceName }}</div>
<div class="wrapper">
<img v-if="face" :src="faceImageDataURI" />
<img
v-if="face"
:src="faceImageDataURI"
:alt="$t('Image of {deviceName}', { deviceName })"
:title="$t('Image of {deviceName}', { deviceName })"
/>
<ul class="info-list">
<li>
<ClockIcon size="1x" />
{{ new Date(timestamp * 1000).toLocaleString() }}
<li :title="$t('Timestamp')">
<ClockIcon size="1x" aria-hidden="true" role="img" />
{{ new Date(timestamp * 1000).toLocaleString($config.locale) }}
</li>
<li>
<MapPinIcon size="1x" />
<li :title="$t('Location')">
<MapPinIcon size="1x" aria-hidden="true" role="img" />
{{ lat }}
<br />
{{ lon }}
<br />
{{ alt }}m
</li>
<li v-if="address">
<HomeIcon size="1x" />
<li v-if="address" :title="$t('Address')">
<HomeIcon size="1x" aria-hidden="true" role="img" />
{{ address }}
</li>
<li v-if="typeof battery === 'number'">
<BatteryIcon size="1x" />
<li v-if="typeof battery === 'number'" :title="$t('Battery')">
<BatteryIcon size="1x" aria-hidden="true" role="img" />
{{ battery }} %
</li>
<li v-if="typeof speed === 'number'">
<ZapIcon size="1x" />
<li v-if="typeof speed === 'number'" :title="$t('Speed')">
<ZapIcon size="1x" aria-hidden="true" role="img" />
{{ speed }} km/h
</li>
</ul>
@@ -122,13 +126,23 @@ export default {
},
computed: {
/**
* Return the face image as a data URI string which can be used for an image's src attribute
* Return the face image as a data URI string which can be used for an
* image's src attribute.
*
* @return {String} base64-encoded face image data URI
* @returns {String} base64-encoded face image data URI
*/
faceImageDataURI() {
return `data:image/png;base64,${this.face}`;
},
/**
* Return the device name for displaying with <user identifier>/<device
* identifier> as fallback.
*
* @returns {String} device name for displaying
*/
deviceName() {
return this.name ? this.name : `${this.user}/${this.device}`;
},
},
};
</script>

View File

@@ -58,6 +58,11 @@ const props = {
custom: true,
default: true,
},
activated: {
type: Boolean,
custom: true,
default: true,
},
};
export default {

View File

@@ -12,10 +12,18 @@
</label>
</div>
<div class="buttons">
<button class="button button-outline button-primary" @click="copy">
<button
class="button button-outline button-primary"
:title="$t('Copy to clipboard')"
@click="copy"
>
{{ $t("Copy to clipboard") }}
</button>
<button class="button button-primary" @click="download">
<button
class="button button-primary"
:title="$t('Download')"
@click="download"
>
{{ $t("Download") }}
</button>
</div>

View File

@@ -2,33 +2,33 @@
<modal name="information" adaptive>
<ul class="info-list">
<li>
<GithubIcon size="1x" />
<GithubIcon size="1x" aria-hidden="true" role="img" />
<a href="https://github.com/owntracks/frontend">
owntracks/frontend
</a>
({{ frontendVersion }})
</li>
<li>
<GithubIcon size="1x" />
<GithubIcon size="1x" aria-hidden="true" role="img" />
<a href="https://github.com/owntracks/recorder">
owntracks/recorder
</a>
({{ recorderVersion || "Loading version..." }})
</li>
<li>
<GlobeIcon size="1x" />
<GlobeIcon size="1x" aria-hidden="true" role="img" />
<a href="https://owntracks.org">
{{ $t("OwnTracks website") }}
</a>
</li>
<li>
<BookIcon size="1x" />
<BookIcon size="1x" aria-hidden="true" role="img" />
<a href="https://owntracks.org/booklet/">
{{ $t("OwnTracks documentation") }}
</a>
</li>
<li>
<TwitterIcon size="1x" />
<TwitterIcon size="1x" aria-hidden="true" role="img" />
<a href="https://twitter.com/OwnTracks">
{{ $t("OwnTracks on Twitter") }}
</a>

View File

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

View File

@@ -22,5 +22,11 @@
"OwnTracks website": "OwnTracks Webseite",
"OwnTracks documentation": "OwnTracks Dokumentation",
"OwnTracks on Twitter": "OwnTracks auf Twitter",
"Loading data, please wait...": "Daten werden geladen, bitte warten..."
"Loading data, please wait...": "Daten werden geladen, bitte warten...",
"Image of {deviceName}": "Bild von {deviceName}",
"Timestamp": "Zeitstempel",
"Location": "Standort",
"Address": "Adresse",
"Battery": "Akku",
"Speed": "Geschwindigkeit"
}

View File

@@ -22,5 +22,11 @@
"OwnTracks website": "OwnTracks website",
"OwnTracks documentation": "OwnTracks documentation",
"OwnTracks on Twitter": "OwnTracks on Twitter",
"Loading data, please wait...": "Loading data, please wait..."
"Loading data, please wait...": "Loading data, please wait...",
"Image of {deviceName}": "Image of {deviceName}",
"Timestamp": "Timestamp",
"Location": "Location",
"Address": "Address",
"Battery": "Battery",
"Speed": "Speed"
}

View File

@@ -1,5 +1,6 @@
import Vue from "vue";
import App from "@/App.vue";
import config from "@/config";
import i18n from "@/i18n";
import router from "@/router";
import store from "@/store";
@@ -11,6 +12,8 @@ Vue.use(VOutsideEvents);
Vue.config.productionTip = false;
Vue.prototype.$config = config;
new Vue({
i18n,
router,

View File

@@ -12,8 +12,9 @@ import { distanceBetweenCoordinates } from "@/util";
* array of all coordinates.
*
* @param {State} state
* @param {MultiLocationHistory} state.locationHistory Location history of selected users and devices
* @return {Array.<L.LatLng>} All coordinates
* @param {MultiLocationHistory} state.locationHistory
* Location history of selected users and devices
* @returns {Array.<L.LatLng>} All coordinates
*/
const locationHistoryLatLngs = state => {
const latLngs = [];
@@ -33,8 +34,9 @@ const locationHistoryLatLngs = state => {
* coordinates does not exceed `config.map.maxPointDistance`.
*
* @param {State} state
* @param {MultiLocationHistory} state.locationHistory Location history of selected users and devices
* @return {Array.<Array.<L.LatLng>>} Groups of coherent coordinates
* @param {MultiLocationHistory} state.locationHistory
* Location history of selected users and devices
* @returns {Array.<Array.<L.LatLng>>} Groups of coherent coordinates
*/
const locationHistoryLatLngGroups = state => {
const groups = [];

View File

@@ -172,14 +172,20 @@ pre {
padding: 8px 16px;
text-overflow: ellipsis;
white-space: nowrap;
transition: box-shadow 0.2s;
&:focus {
outline: none;
box-shadow: 0 0 0 5px rgba(0, 0, 0, 0.2);
}
&:focus::-moz-focus-inner {
border-color: transparent;
}
&.button-primary {
color: var(--color-primary-text);
background: var(--color-primary);
&:focus::-moz-focus-inner {
border-color: var(--color-primary-text)e;
}
}
&.button-outline {
@@ -204,7 +210,8 @@ pre {
&.button-outline,
&.button-flat {
transition: background 0.2s;
transition: background-color 0.2s, box-shadow 0.2s;
&:hover,
&:focus {
background: rgba(0, 0, 0, 0.2);

View File

@@ -5,5 +5,6 @@
.datepicker {
box-shadow: none !important;
filter: var(--drop-shadow);
margin-top: 5px;
}
}

View File

@@ -23,6 +23,7 @@
margin-top: 15px;
margin-right: 15px;
border-radius: 100px;
transition: background-color 0.2s;
&:hover,
&:focus {
@@ -35,4 +36,12 @@
.leaflet-popup-tip {
box-shadow: none;
}
.leaflet-control-container .leaflet-control-attribution {
background: var(--color-background);
a {
color: var(--color-primary);
}
}
}

View File

@@ -1,3 +1,5 @@
/* eslint max-len: 0 */
/**
* A coordinate with latitude and longitude.
*

View File

@@ -10,7 +10,7 @@ import { DATE_TIME_FORMAT, EARTH_RADIUS_IN_KM } from "@/constants";
* base URL configuration into account.
*
* @param {String} path Path to the API resource
* @return {URL} Final API URL
* @returns {URL} Final API URL
*/
export const getApiUrl = path => {
const normalizedBaseUrl = config.api.baseUrl.endsWith("/")
@@ -24,7 +24,7 @@ export const getApiUrl = path => {
* Check if the given string is an ISO 8601 YYYY-MM-DDTHH:MM:SS datetime.
*
* @param {String} s Input value to be tested
* @return {Boolean} Whether the input matches the expected format
* @returns {Boolean} Whether the input matches the expected format
*/
export const isIsoDateTime = s => moment(s, DATE_TIME_FORMAT, true).isValid();
@@ -32,7 +32,7 @@ export const isIsoDateTime = s => moment(s, DATE_TIME_FORMAT, true).isValid();
* Convert degrees to radians.
*
* @param {Number} degrees Angle in degrees
* @return {Number} Angle in radians
* @returns {Number} Angle in radians
*/
export const degreesToRadians = degrees => (degrees * Math.PI) / 180;
@@ -44,7 +44,7 @@ export const degreesToRadians = degrees => (degrees * Math.PI) / 180;
*
* @param {Coordinate} c1 First coordinate
* @param {Coordinate} c2 Second coordinate
* @return {Number} Distance in meters
* @returns {Number} Distance in meters
*/
export const distanceBetweenCoordinates = (c1, c2) => {
const r = EARTH_RADIUS_IN_KM * 1000;

View File

@@ -66,7 +66,11 @@
<template v-for="(userDevices, user) in locationHistory">
<template v-for="(deviceLocations, device) in userDevices">
<LCircleMarker
v-for="(l, n) in deviceLocations"
v-for="(l, n) in deviceLocationsWithNameAndFace(
user,
device,
deviceLocations
)"
:key="`${user}-${device}-${n}`"
:lat-lng="[l.lat, l.lon]"
v-bind="circleMarker"
@@ -74,6 +78,8 @@
<LDeviceLocationPopup
:user="user"
:device="device"
:name="l.name"
:face="l.face"
:timestamp="l.tst"
:lat="l.lat"
:lon="l.lon"
@@ -118,7 +124,6 @@ import markerIcon2x from "leaflet/dist/images/marker-icon-2x.png";
import markerShadow from "leaflet/dist/images/marker-shadow.png";
import * as types from "@/store/mutation-types";
import config from "@/config";
import LHeatmap from "@/components/LHeatmap";
import LDeviceLocationPopup from "@/components/LDeviceLocationPopup";
@@ -145,26 +150,27 @@ export default {
},
data() {
return {
attribution: config.map.attribution,
attribution: this.$config.map.attribution,
center: this.$store.state.map.center,
controls: config.map.controls,
heatmap: config.map.heatmap,
maxZoom: config.map.maxZoom,
maxNativeZoom: config.map.maxNativeZoom,
url: config.map.url,
controls: this.$config.map.controls,
heatmap: this.$config.map.heatmap,
maxZoom: this.$config.map.maxZoom,
maxNativeZoom: this.$config.map.maxNativeZoom,
url: this.$config.map.url,
zoom: this.$store.state.map.zoom,
circle: {
...config.map.circle,
color: config.map.circle.color || config.primaryColor,
fillColor: config.map.circle.fillColor || config.primaryColor,
...this.$config.map.circle,
color: this.$config.map.circle.color || this.$config.primaryColor,
fillColor:
this.$config.map.circle.fillColor || this.$config.primaryColor,
},
circleMarker: {
...config.map.circleMarker,
color: config.map.circleMarker.color || config.primaryColor,
...this.$config.map.circleMarker,
color: this.$config.map.circleMarker.color || this.$config.primaryColor,
},
polyline: {
...config.map.polyline,
color: config.map.polyline.color || config.primaryColor,
...this.$config.map.polyline,
color: this.$config.map.polyline.color || this.$config.primaryColor,
},
};
},
@@ -202,6 +208,29 @@ export default {
});
}
},
/**
* Find a the last location object for a user/device combination from the
* local cache and backfill name and face attributes to each item from the
* passed array of location objects.
*
* @param {User} user Username
* @param {Device} device Device name
* @param {LocationHistory} deviceLocations Device name
* @returns {LocationHistory} Updated location history
*/
deviceLocationsWithNameAndFace(user, device, deviceLocations) {
const lastLocation = this.lastLocations.find(
l => l.username === user && l.device === device
);
if (!lastLocation) {
return deviceLocations;
}
return deviceLocations.map(l => ({
...l,
name: lastLocation.name,
face: lastLocation.face,
}));
},
},
};
</script>

View File

@@ -12,9 +12,7 @@ describe("API", () => {
expect(version).toBe("1.2.3");
expect(fetch.mock.calls.length).toEqual(1);
expect(fetch.mock.calls[0][0].href).toEqual(
"http://localhost/api/0/version"
);
expect(fetch.mock.calls[0][0]).toEqual("http://localhost/api/0/version");
});
test("getUsers", async () => {
@@ -24,7 +22,7 @@ describe("API", () => {
expect(users).toEqual(["foo", "bar"]);
expect(fetch.mock.calls.length).toEqual(1);
expect(fetch.mock.calls[0][0].href).toEqual("http://localhost/api/0/list");
expect(fetch.mock.calls[0][0]).toEqual("http://localhost/api/0/list");
});
test("getDevices", async () => {
@@ -37,10 +35,10 @@ describe("API", () => {
expect(devices).toEqual({ foo: ["phone", "tablet"], bar: ["laptop"] });
expect(fetch.mock.calls.length).toEqual(2);
expect(fetch.mock.calls[0][0].href).toEqual(
expect(fetch.mock.calls[0][0]).toEqual(
"http://localhost/api/0/list?user=foo"
);
expect(fetch.mock.calls[1][0].href).toEqual(
expect(fetch.mock.calls[1][0]).toEqual(
"http://localhost/api/0/list?user=bar"
);
});
@@ -68,7 +66,7 @@ describe("API", () => {
expect(lastLocation).toEqual(response);
expect(fetch.mock.calls.length).toEqual(1);
expect(fetch.mock.calls[0][0].href).toEqual("http://localhost/api/0/last");
expect(fetch.mock.calls[0][0]).toEqual("http://localhost/api/0/last");
});
test("getLastLocations with user", async () => {
@@ -89,7 +87,7 @@ describe("API", () => {
expect(lastLocation).toEqual(response);
expect(fetch.mock.calls.length).toEqual(1);
expect(fetch.mock.calls[0][0].href).toEqual(
expect(fetch.mock.calls[0][0]).toEqual(
"http://localhost/api/0/last?user=foo"
);
});
@@ -108,7 +106,7 @@ describe("API", () => {
expect(lastLocation).toEqual(response);
expect(fetch.mock.calls.length).toEqual(1);
expect(fetch.mock.calls[0][0].href).toEqual(
expect(fetch.mock.calls[0][0]).toEqual(
"http://localhost/api/0/last?user=foo&device=phone"
);
});
@@ -150,7 +148,7 @@ describe("API", () => {
expect(locationHistory).toEqual(response.data);
expect(fetch.mock.calls.length).toEqual(1);
expect(fetch.mock.calls[0][0].href).toEqual(
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"
);
});
@@ -206,13 +204,13 @@ describe("API", () => {
});
expect(fetch.mock.calls.length).toEqual(3);
expect(fetch.mock.calls[0][0].href).toEqual(
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(fetch.mock.calls[1][0].href).toEqual(
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(fetch.mock.calls[2][0].href).toEqual(
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"
);
});

View File

@@ -1 +1 @@
global.fetch = require("jest-fetch-mock");
require("jest-fetch-mock").enableMocks();

914
yarn.lock

File diff suppressed because it is too large Load Diff