aboutsummaryrefslogtreecommitdiffstats
path: root/apps/weather_status/src
diff options
context:
space:
mode:
Diffstat (limited to 'apps/weather_status/src')
-rw-r--r--apps/weather_status/src/App.vue582
-rw-r--r--apps/weather_status/src/services/weatherStatusService.js127
-rw-r--r--apps/weather_status/src/weather-status.js24
3 files changed, 733 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>
diff --git a/apps/weather_status/src/services/weatherStatusService.js b/apps/weather_status/src/services/weatherStatusService.js
new file mode 100644
index 00000000000..3725f536d32
--- /dev/null
+++ b/apps/weather_status/src/services/weatherStatusService.js
@@ -0,0 +1,127 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import HttpClient from '@nextcloud/axios'
+import { generateOcsUrl } from '@nextcloud/router'
+
+/**
+ *
+ *
+ * @param {string} lat the latitude
+ * @param {string} lon the longitude
+ * @return {Promise<object>}
+ */
+const setLocation = async (lat, lon) => {
+ const url = generateOcsUrl('apps/weather_status/api/v1/location')
+ const response = await HttpClient.put(url, {
+ address: '',
+ lat,
+ lon,
+ })
+
+ return response.data.ocs.data
+}
+
+/**
+ *
+ * @param {string} address The location
+ * @return {Promise<object>}
+ */
+const setAddress = async (address) => {
+ const url = generateOcsUrl('apps/weather_status/api/v1/location')
+ const response = await HttpClient.put(url, {
+ address,
+ lat: null,
+ lon: null,
+ })
+
+ return response.data.ocs.data
+}
+
+/**
+ *
+ * @param {string} mode can be 1 browser or 2 custom
+ * @return {Promise<object>}
+ */
+const setMode = async (mode) => {
+ const url = generateOcsUrl('apps/weather_status/api/v1/mode')
+ const response = await HttpClient.put(url, {
+ mode,
+ })
+
+ return response.data.ocs.data
+}
+
+/**
+ *
+ * @return {Promise<object>}
+ */
+const usePersonalAddress = async () => {
+ const url = generateOcsUrl('apps/weather_status/api/v1/use-personal')
+ const response = await HttpClient.put(url)
+
+ return response.data.ocs.data
+}
+
+/**
+ * Fetches the location information for current user
+ *
+ * @return {Promise<object>}
+ */
+const getLocation = async () => {
+ const url = generateOcsUrl('apps/weather_status/api/v1/location')
+ const response = await HttpClient.get(url)
+
+ return response.data.ocs.data
+}
+
+/**
+ * Fetches the weather forecast
+ *
+ * @return {Promise<object>}
+ */
+const fetchForecast = async () => {
+ const url = generateOcsUrl('apps/weather_status/api/v1/forecast')
+ const response = await HttpClient.get(url)
+
+ return response.data.ocs.data
+}
+
+/**
+ * Fetches the location favorites
+ *
+ * @return {Promise<object>}
+ */
+const getFavorites = async () => {
+ const url = generateOcsUrl('apps/weather_status/api/v1/favorites')
+ const response = await HttpClient.get(url)
+
+ return response.data.ocs.data
+}
+
+/**
+ *
+ * @param {Array} favorites List of favorite addresses
+ * @return {Promise<object>}
+ */
+const saveFavorites = async (favorites) => {
+ const url = generateOcsUrl('apps/weather_status/api/v1/favorites')
+ const response = await HttpClient.put(url, {
+ favorites,
+ })
+
+ return response.data.ocs.data
+}
+
+export {
+ usePersonalAddress,
+ setMode,
+ getLocation,
+ setLocation,
+ setAddress,
+ fetchForecast,
+ getFavorites,
+ saveFavorites,
+}
diff --git a/apps/weather_status/src/weather-status.js b/apps/weather_status/src/weather-status.js
new file mode 100644
index 00000000000..45c73b87e4a
--- /dev/null
+++ b/apps/weather_status/src/weather-status.js
@@ -0,0 +1,24 @@
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { getCSPNonce } from '@nextcloud/auth'
+import Vue from 'vue'
+import App from './App.vue'
+
+// eslint-disable-next-line camelcase
+__webpack_nonce__ = getCSPNonce()
+
+Vue.prototype.t = t
+
+document.addEventListener('DOMContentLoaded', function() {
+ if (!OCA.Dashboard) {
+ return
+ }
+
+ OCA.Dashboard.registerStatus('weather', (el) => {
+ const Dashboard = Vue.extend(App)
+ return new Dashboard().$mount(el)
+ })
+})