aboutsummaryrefslogtreecommitdiffstats
path: root/apps/weather_status/src/App.vue
diff options
context:
space:
mode:
Diffstat (limited to 'apps/weather_status/src/App.vue')
-rw-r--r--apps/weather_status/src/App.vue582
1 files changed, 582 insertions, 0 deletions
diff --git a/apps/weather_status/src/App.vue b/apps/weather_status/src/App.vue
new file mode 100644
index 00000000000..34dc8e90efa
--- /dev/null
+++ b/apps/weather_status/src/App.vue
@@ -0,0 +1,582 @@
+<!--
+ - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<template>
+ <div id="weather-status-menu-item">
+ <NcActions class="weather-status-menu-item__subheader"
+ :aria-hidden="true"
+ :aria-label="currentWeatherMessage"
+ :menu-name="currentWeatherMessage">
+ <template #icon>
+ <NcLoadingIcon v-if="loading" />
+ <img v-else
+ :src="weatherIconUrl"
+ alt=""
+ class="weather-image">
+ </template>
+ <NcActionText v-if="gotWeather"
+ :aria-hidden="true">
+ <template #icon>
+ <NcLoadingIcon v-if="loading" />
+ <div v-else class="weather-action-image-container">
+ <img :src="futureWeatherIconUrl"
+ alt=""
+ class="weather-image">
+ </div>
+ </template>
+ {{ forecastMessage }}
+ </NcActionText>
+ <NcActionLink v-if="gotWeather"
+ target="_blank"
+ :aria-hidden="true"
+ :href="weatherLinkTarget"
+ :close-after-click="true">
+ <template #icon>
+ <NcIconSvgWrapper name="MapMarker"
+ :svg="mapMarkerSvg"
+ :size="20" />
+ </template>
+ {{ locationText }}
+ </NcActionLink>
+ <NcActionButton v-if="gotWeather"
+ :aria-hidden="true"
+ @click="onAddRemoveFavoriteClick">
+ <template #icon>
+ <NcIconSvgWrapper name="Star"
+ :svg="addRemoveFavoriteSvg"
+ :size="20"
+ class="favorite-color" />
+ </template>
+ {{ addRemoveFavoriteText }}
+ </NcActionButton>
+ <NcActionSeparator v-if="address && !errorMessage" />
+ <NcActionButton :close-after-click="true"
+ :aria-hidden="true"
+ @click="onBrowserLocationClick">
+ <template #icon>
+ <NcIconSvgWrapper name="Crosshairs"
+ :svg="crosshairsSvg"
+ :size="20" />
+ </template>
+ {{ t('weather_status', 'Detect location') }}
+ </NcActionButton>
+ <NcActionInput ref="addressInput"
+ :label="t('weather_status', 'Set custom address')"
+ :disabled="false"
+ icon="icon-rename"
+ :aria-hidden="true"
+ type="text"
+ value=""
+ @submit="onAddressSubmit" />
+ <template v-if="favorites.length > 0">
+ <NcActionCaption :name="t('weather_status', 'Favorites')" />
+ <NcActionButton v-for="favorite in favorites"
+ :key="favorite"
+ :aria-hidden="true"
+ @click="onFavoriteClick($event, favorite)">
+ <template #icon>
+ <NcIconSvgWrapper name="Star"
+ :svg="starSvg"
+ :size="20"
+ :class="{'favorite-color': address === favorite}" />
+ </template>
+ {{ favorite }}
+ </NcActionButton>
+ </template>
+ </NcActions>
+ </div>
+</template>
+
+<script>
+import { showError } from '@nextcloud/dialogs'
+import moment from '@nextcloud/moment'
+import { getLocale } from '@nextcloud/l10n'
+import { imagePath } from '@nextcloud/router'
+import NcActions from '@nextcloud/vue/components/NcActions'
+import NcActionButton from '@nextcloud/vue/components/NcActionButton'
+import NcActionCaption from '@nextcloud/vue/components/NcActionCaption'
+import NcActionInput from '@nextcloud/vue/components/NcActionInput'
+import NcActionLink from '@nextcloud/vue/components/NcActionLink'
+import NcActionSeparator from '@nextcloud/vue/components/NcActionSeparator'
+import NcActionText from '@nextcloud/vue/components/NcActionText'
+import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
+import * as network from './services/weatherStatusService.js'
+import crosshairsSvg from '@mdi/svg/svg/crosshairs.svg?raw'
+import mapMarkerSvg from '@mdi/svg/svg/map-marker.svg?raw'
+import starSvg from '@mdi/svg/svg/star.svg?raw'
+import starOutlineSvg from '@mdi/svg/svg/star-outline.svg?raw'
+
+const MODE_BROWSER_LOCATION = 1
+const MODE_MANUAL_LOCATION = 2
+const weatherOptions = {
+ clearsky_day: {
+ text: (temperature, unit, later = false) => later
+ ? t('weather_status', '{temperature} {unit} clear sky later today', { temperature, unit })
+ : t('weather_status', '{temperature} {unit} clear sky', { temperature, unit }),
+ },
+ clearsky_night: {
+ text: (temperature, unit, later = false) => later
+ ? t('weather_status', '{temperature} {unit} clear sky later today', { temperature, unit })
+ : t('weather_status', '{temperature} {unit} clear sky', { temperature, unit }),
+ },
+ cloudy: {
+ text: (temperature, unit, later = false) => later
+ ? t('weather_status', '{temperature} {unit} cloudy later today', { temperature, unit })
+ : t('weather_status', '{temperature} {unit} cloudy', { temperature, unit }),
+ },
+ snowandthunder: {
+ text: (temperature, unit, later = false) => later
+ ? t('weather_status', '{temperature} {unit} snow and thunder later today', { temperature, unit })
+ : t('weather_status', '{temperature} {unit} snow and thunder', { temperature, unit }),
+ },
+ snowshowersandthunder_day: {
+ text: (temperature, unit, later = false) => later
+ ? t('weather_status', '{temperature} {unit} snow showers and thunder later today', { temperature, unit })
+ : t('weather_status', '{temperature} {unit} snow showers and thunder', { temperature, unit }),
+ },
+ snowshowersandthunder_night: {
+ text: (temperature, unit, later = false) => later
+ ? t('weather_status', '{temperature} {unit} snow showers and thunder later today', { temperature, unit })
+ : t('weather_status', '{temperature} {unit} snow showers and thunder', { temperature, unit }),
+ },
+ snowshowersandthunder_polartwilight: {
+ text: (temperature, unit, later = false) => later
+ ? t('weather_status', '{temperature} {unit} snow showers, thunder and polar twilight later today', { temperature, unit })
+ : t('weather_status', '{temperature} {unit} snow showers, thunder and polar twilight', { temperature, unit }),
+ },
+ snowshowers_day: {
+ text: (temperature, unit, later = false) => later
+ ? t('weather_status', '{temperature} {unit} snow showers later today', { temperature, unit })
+ : t('weather_status', '{temperature} {unit} snow showers', { temperature, unit }),
+ },
+ snowshowers_night: {
+ text: (temperature, unit, later = false) => later
+ ? t('weather_status', '{temperature} {unit} snow showers later today', { temperature, unit })
+ : t('weather_status', '{temperature} {unit} snow showers', { temperature, unit }),
+ },
+ snowshowers_polartwilight: {
+ text: (temperature, unit, later = false) => later
+ ? t('weather_status', '{temperature} {unit} snow showers and polar twilight later today', { temperature, unit })
+ : t('weather_status', '{temperature} {unit} snow showers and polar twilight', { temperature, unit }),
+ },
+ snow: {
+ text: (temperature, unit, later = false) => later
+ ? t('weather_status', '{temperature} {unit} snow later today', { temperature, unit })
+ : t('weather_status', '{temperature} {unit} snow', { temperature, unit }),
+ },
+ fair_day: {
+ text: (temperature, unit, later = false) => later
+ ? t('weather_status', '{temperature} {unit} fair weather later today', { temperature, unit })
+ : t('weather_status', '{temperature} {unit} fair weather', { temperature, unit }),
+ },
+ fair_night: {
+ text: (temperature, unit, later = false) => later
+ ? t('weather_status', '{temperature} {unit} fair weather later today', { temperature, unit })
+ : t('weather_status', '{temperature} {unit} fair weather', { temperature, unit }),
+ },
+ partlycloudy_day: {
+ text: (temperature, unit, later = false) => later
+ ? t('weather_status', '{temperature} {unit} partly cloudy later today', { temperature, unit })
+ : t('weather_status', '{temperature} {unit} partly cloudy', { temperature, unit }),
+ },
+ partlycloudy_night: {
+ text: (temperature, unit, later = false) => later
+ ? t('weather_status', '{temperature} {unit} partly cloudy later today', { temperature, unit })
+ : t('weather_status', '{temperature} {unit} partly cloudy', { temperature, unit }),
+ },
+ fog: {
+ text: (temperature, unit, later = false) => later
+ ? t('weather_status', '{temperature} {unit} foggy later today', { temperature, unit })
+ : t('weather_status', '{temperature} {unit} foggy', { temperature, unit }),
+ },
+ lightrain: {
+ text: (temperature, unit, later = false) => later
+ ? t('weather_status', '{temperature} {unit} light rainfall later today', { temperature, unit })
+ : t('weather_status', '{temperature} {unit} light rainfall', { temperature, unit }),
+ },
+ rain: {
+ text: (temperature, unit, later = false) => later
+ ? t('weather_status', '{temperature} {unit} rainfall later today', { temperature, unit })
+ : t('weather_status', '{temperature} {unit} rainfall', { temperature, unit }),
+ },
+ heavyrain: {
+ text: (temperature, unit, later = false) => later
+ ? t('weather_status', '{temperature} {unit} heavy rainfall later today', { temperature, unit })
+ : t('weather_status', '{temperature} {unit} heavy rainfall', { temperature, unit }),
+ },
+ rainshowers_day: {
+ text: (temperature, unit, later = false) => later
+ ? t('weather_status', '{temperature} {unit} rainfall showers later today', { temperature, unit })
+ : t('weather_status', '{temperature} {unit} rainfall showers', { temperature, unit }),
+ },
+ rainshowers_night: {
+ text: (temperature, unit, later = false) => later
+ ? t('weather_status', '{temperature} {unit} rainfall showers later today', { temperature, unit })
+ : t('weather_status', '{temperature} {unit} rainfall showers', { temperature, unit }),
+ },
+ lightrainshowers_day: {
+ text: (temperature, unit, later = false) => later
+ ? t('weather_status', '{temperature} {unit} light rainfall showers later today', { temperature, unit })
+ : t('weather_status', '{temperature} {unit} light rainfall showers', { temperature, unit }),
+ },
+ lightrainshowers_night: {
+ text: (temperature, unit, later = false) => later
+ ? t('weather_status', '{temperature} {unit} light rainfall showers later today', { temperature, unit })
+ : t('weather_status', '{temperature} {unit} light rainfall showers', { temperature, unit }),
+ },
+ heavyrainshowers_day: {
+ text: (temperature, unit, later = false) => later
+ ? t('weather_status', '{temperature} {unit} heavy rainfall showers later today', { temperature, unit })
+ : t('weather_status', '{temperature} {unit} heavy rainfall showers', { temperature, unit }),
+ },
+ heavyrainshowers_night: {
+ text: (temperature, unit, later = false) => later
+ ? t('weather_status', '{temperature} {unit} heavy rainfall showers later today', { temperature, unit })
+ : t('weather_status', '{temperature} {unit} heavy rainfall showers', { temperature, unit }),
+ },
+}
+
+export default {
+ name: 'App',
+ components: {
+ NcActions,
+ NcActionButton,
+ NcActionCaption,
+ NcActionInput,
+ NcActionLink,
+ NcActionSeparator,
+ NcActionText,
+ NcLoadingIcon,
+ NcIconSvgWrapper,
+ },
+ data() {
+ return {
+ crosshairsSvg,
+ mapMarkerSvg,
+ starSvg,
+ starOutlineSvg,
+ locale: getLocale(),
+ loading: true,
+ errorMessage: '',
+ mode: MODE_BROWSER_LOCATION,
+ address: null,
+ lat: null,
+ lon: null,
+ // how many hours ahead do we want to see the forecast?
+ offset: 5,
+ forecasts: [],
+ loop: null,
+ favorites: [],
+ }
+ },
+ computed: {
+ useFahrenheitLocale() {
+ return ['en_US', 'en_MH', 'en_FM', 'en_PW', 'en_KY', 'en_LR'].includes(this.locale)
+ },
+ temperatureUnit() {
+ return this.useFahrenheitLocale ? '°F' : '°C'
+ },
+ locationText() {
+ return t('weather_status', 'More weather for {adr}', { adr: this.address })
+ },
+ temperature() {
+ return this.getTemperature(this.forecasts, 0)
+ },
+ futureTemperature() {
+ return this.getTemperature(this.forecasts, this.offset)
+ },
+ weatherCode() {
+ return this.getWeatherCode(this.forecasts, 0)
+ },
+ futureWeatherCode() {
+ return this.getWeatherCode(this.forecasts, this.offset)
+ },
+ weatherIconUrl() {
+ return this.getWeatherIconUrl(this.weatherCode)
+ },
+ futureWeatherIconUrl() {
+ return this.getWeatherIconUrl(this.futureWeatherCode)
+ },
+ /**
+ * The message displayed in the top right corner
+ *
+ * @return {string}
+ */
+ currentWeatherMessage() {
+ if (this.loading) {
+ return t('weather_status', 'Loading weather')
+ } else if (this.errorMessage) {
+ return this.errorMessage
+ } else if (this.gotWeather) {
+ return this.getWeatherMessage(this.weatherCode, this.temperature)
+ } else {
+ return t('weather_status', 'Set location for weather')
+ }
+ },
+ forecastMessage() {
+ if (this.loading) {
+ return t('weather_status', 'Loading weather')
+ } else if (this.gotWeather) {
+ return this.getWeatherMessage(this.futureWeatherCode, this.futureTemperature, true)
+ } else {
+ return t('weather_status', 'Set location for weather')
+ }
+ },
+ weatherLinkTarget() {
+ return 'https://www.windy.com/-Rain-thunder-rain?rain,' + this.lat + ',' + this.lon + ',11'
+ },
+ gotWeather() {
+ return this.address && !this.errorMessage
+ },
+ addRemoveFavoriteSvg() {
+ return this.currentAddressIsFavorite
+ ? starSvg
+ : starOutlineSvg
+ },
+ addRemoveFavoriteText() {
+ return this.currentAddressIsFavorite
+ ? t('weather_status', 'Remove from favorites')
+ : t('weather_status', 'Add as favorite')
+ },
+ currentAddressIsFavorite() {
+ return this.favorites.find((f) => {
+ return f === this.address
+ })
+ },
+ },
+ mounted() {
+ this.initWeatherStatus()
+ },
+ methods: {
+ async initWeatherStatus() {
+ try {
+ const loc = await network.getLocation()
+ this.lat = loc.lat
+ this.lon = loc.lon
+ this.address = loc.address
+ this.mode = loc.mode
+
+ if (this.mode === MODE_BROWSER_LOCATION) {
+ this.askBrowserLocation()
+ } else if (this.mode === MODE_MANUAL_LOCATION) {
+ this.startLoop()
+ }
+ const favs = await network.getFavorites()
+ this.favorites = favs
+ } catch (err) {
+ if (err?.code === 'ECONNABORTED') {
+ console.info('The weather status request was cancelled because the user navigates.')
+ return
+ }
+ if (err.response && err.response.status === 401) {
+ showError(t('weather_status', 'You are not logged in.'))
+ } else {
+ showError(t('weather_status', 'There was an error getting the weather status information.'))
+ }
+ console.error(err)
+ }
+ },
+ startLoop() {
+ clearInterval(this.loop)
+ if (this.lat && this.lon) {
+ this.loop = setInterval(() => this.getForecast(), 60 * 1000 * 60)
+ this.getForecast()
+ } else {
+ this.loading = false
+ }
+ },
+ askBrowserLocation() {
+ this.loading = true
+ this.errorMessage = ''
+ if (navigator.geolocation && window.isSecureContext) {
+ navigator.geolocation.getCurrentPosition((position) => {
+ console.debug('browser location success')
+ this.lat = position.coords.latitude
+ this.lon = position.coords.longitude
+ this.saveMode(MODE_BROWSER_LOCATION)
+ this.mode = MODE_BROWSER_LOCATION
+ this.saveLocation(this.lat, this.lon)
+ },
+ (error) => {
+ console.debug('location permission refused')
+ console.debug(error)
+ this.saveMode(MODE_MANUAL_LOCATION)
+ this.mode = MODE_MANUAL_LOCATION
+ // fallback on what we have if possible
+ if (this.lat && this.lon) {
+ this.startLoop()
+ } else {
+ this.usePersonalAddress()
+ }
+ })
+ } else {
+ console.debug('no secure context!')
+ this.saveMode(MODE_MANUAL_LOCATION)
+ this.mode = MODE_MANUAL_LOCATION
+ this.startLoop()
+ }
+ },
+ async getForecast() {
+ try {
+ this.forecasts = await network.fetchForecast()
+ } catch (err) {
+ this.errorMessage = t('weather_status', 'No weather information found')
+ console.debug(err)
+ }
+ this.loading = false
+ },
+ async setAddress(address) {
+ this.loading = true
+ this.errorMessage = ''
+ try {
+ const loc = await network.setAddress(address)
+ if (loc.success) {
+ this.lat = loc.lat
+ this.lon = loc.lon
+ this.address = loc.address
+ this.mode = MODE_MANUAL_LOCATION
+ this.startLoop()
+ } else {
+ this.errorMessage = t('weather_status', 'Location not found')
+ this.loading = false
+ }
+ } catch (err) {
+ if (err.response && err.response.status === 401) {
+ showError(t('weather_status', 'You are not logged in.'))
+ } else {
+ showError(t('weather_status', 'There was an error setting the location address.'))
+ }
+ this.loading = false
+ }
+ },
+ async saveLocation(lat, lon) {
+ try {
+ const loc = await network.setLocation(lat, lon)
+ this.address = loc.address
+ this.startLoop()
+ } catch (err) {
+ if (err.response && err.response.status === 401) {
+ showError(t('weather_status', 'You are not logged in.'))
+ } else {
+ showError(t('weather_status', 'There was an error setting the location.'))
+ }
+ console.debug(err)
+ }
+ },
+ async saveMode(mode) {
+ try {
+ await network.setMode(mode)
+ } catch (err) {
+ if (err.response && err.response.status === 401) {
+ showError(t('weather_status', 'You are not logged in.'))
+ } else {
+ showError(t('weather_status', 'There was an error saving the mode.'))
+ }
+ console.debug(err)
+ }
+ },
+ onBrowserLocationClick() {
+ this.askBrowserLocation()
+ },
+ async usePersonalAddress() {
+ this.loading = true
+ try {
+ const loc = await network.usePersonalAddress()
+ this.lat = loc.lat
+ this.lon = loc.lon
+ this.address = loc.address
+ this.mode = MODE_MANUAL_LOCATION
+ this.startLoop()
+ } catch (err) {
+ if (err.response && err.response.status === 401) {
+ showError(t('weather_status', 'You are not logged in.'))
+ } else {
+ showError(t('weather_status', 'There was an error using personal address.'))
+ }
+ console.debug(err)
+ this.loading = false
+ }
+ },
+ onAddressSubmit() {
+ const newAddress = this.$refs.addressInput.$el.querySelector('input[type="text"]').value
+ this.setAddress(newAddress)
+ },
+ getLocalizedTemperature(celcius) {
+ return this.useFahrenheitLocale
+ ? (celcius * (9 / 5)) + 32
+ : celcius
+ },
+ onAddRemoveFavoriteClick() {
+ const currentIsFavorite = this.currentAddressIsFavorite
+ if (currentIsFavorite) {
+ const i = this.favorites.indexOf(currentIsFavorite)
+ if (i !== -1) {
+ this.favorites.splice(i, 1)
+ }
+ } else {
+ this.favorites.push(this.address)
+ }
+ network.saveFavorites(this.favorites)
+ },
+ onFavoriteClick(e, favAddress) {
+ // clicked on the icon
+ if (e.target.classList.contains('action-button__icon')) {
+ const i = this.favorites.indexOf(favAddress)
+ if (i !== -1) {
+ this.favorites.splice(i, 1)
+ }
+ network.saveFavorites(this.favorites)
+ } else if (favAddress !== this.address) {
+ // clicked on the text
+ this.setAddress(favAddress)
+ }
+ },
+ formatTime(time) {
+ return moment(time).format('LT')
+ },
+ getTemperature(forecasts, offset = 0) {
+ return forecasts.length > offset ? forecasts[offset].data.instant.details.air_temperature : ''
+ },
+ getWeatherCode(forecasts, offset = 0) {
+ return forecasts.length > offset ? forecasts[offset].data.next_1_hours.summary.symbol_code : ''
+ },
+ getWeatherIconUrl(weatherCode) {
+ // those icons were obtained there: https://github.com/metno/weathericons/tree/main/weather/svg
+ return (weatherCode && weatherCode in weatherOptions)
+ ? imagePath('weather_status', 'met.no.icons/' + weatherCode + '.svg')
+ : imagePath('weather_status', 'met.no.icons/fair_day.svg')
+ },
+ getWeatherMessage(weatherCode, temperature, later = false) {
+ return weatherCode && weatherCode in weatherOptions
+ ? weatherOptions[weatherCode].text(
+ Math.round(this.getLocalizedTemperature(temperature)),
+ this.temperatureUnit,
+ later,
+ )
+ : t('weather_status', 'Unknown weather code')
+ },
+ },
+}
+</script>
+
+<style lang="scss">
+.weather-action-image-container {
+ width: var(--default-clickable-area);
+ height: var(--default-clickable-area);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.weather-image {
+ width: calc(var(--default-clickable-area) - 2 * var(--default-grid-baseline));
+}
+
+// Set color to primary element for current / active favorite address
+.favorite-color {
+ color: var(--color-favorite);
+}
+</style>