Signed-off-by: Julien Veyssier <eneiluj@posteo.net>pull/22124/head
@ -0,0 +1,21 @@ |
||||
<?xml version="1.0"?> |
||||
<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance" |
||||
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd"> |
||||
<id>weather_status</id> |
||||
<name>Weather status</name> |
||||
<summary>Weather status in your dashboard</summary> |
||||
<description><![CDATA[Weather status integrated in the dashboard app. |
||||
User's position can be automatically determined or manually defined. A 6 hours forecast is then displayed. |
||||
This status can also be integrated in other places like the Calendar app.]]></description> |
||||
<version>1.0.0</version> |
||||
<licence>agpl</licence> |
||||
<author mail="eneiluj@posteo.net">Julien Veyssier</author> |
||||
<namespace>WeatherStatus</namespace> |
||||
<default_enable/> |
||||
<category>integration</category> |
||||
<category>dashboard</category> |
||||
<bugs>https://github.com/nextcloud/server</bugs> |
||||
<dependencies> |
||||
<nextcloud min-version="20" max-version="20"/> |
||||
</dependencies> |
||||
</info> |
||||
@ -0,0 +1,34 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/** |
||||
* @copyright Copyright (c) 2020, Julien Veyssier |
||||
* |
||||
* @author Julien Veyssier <eneiluj@posteo.net> |
||||
* |
||||
* @license AGPL-3.0 |
||||
* |
||||
* This code is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU Affero General Public License, version 3, |
||||
* as published by the Free Software Foundation. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU Affero General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU Affero General Public License, version 3, |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/> |
||||
* |
||||
*/ |
||||
|
||||
return [ |
||||
'ocs' => [ |
||||
['name' => 'WeatherStatus#setMode', 'url' => '/api/v1/mode', 'verb' => 'PUT'], |
||||
['name' => 'WeatherStatus#usePersonalAddress', 'url' => '/api/v1/use-personal', 'verb' => 'PUT'], |
||||
['name' => 'WeatherStatus#getLocation', 'url' => '/api/v1/location', 'verb' => 'GET'], |
||||
['name' => 'WeatherStatus#setLocation', 'url' => '/api/v1/location', 'verb' => 'PUT'], |
||||
['name' => 'WeatherStatus#getForecast', 'url' => '/api/v1/forecast', 'verb' => 'GET'], |
||||
], |
||||
]; |
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1009 B |
|
After Width: | Height: | Size: 666 B |
|
After Width: | Height: | Size: 235 B |
|
After Width: | Height: | Size: 242 B |
|
After Width: | Height: | Size: 806 B |
|
After Width: | Height: | Size: 815 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 642 B |
|
After Width: | Height: | Size: 863 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 703 B |
|
After Width: | Height: | Size: 748 B |
@ -0,0 +1,72 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/** |
||||
* @copyright Copyright (c) 2020, Julien Veyssier |
||||
* |
||||
* @author Julien Veyssier <eneiluj@posteo.net> |
||||
* |
||||
* @license AGPL-3.0 |
||||
* |
||||
* This code is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU Affero General Public License, version 3, |
||||
* as published by the Free Software Foundation. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU Affero General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU Affero General Public License, version 3, |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/> |
||||
* |
||||
*/ |
||||
|
||||
namespace OCA\WeatherStatus\AppInfo; |
||||
|
||||
use OCA\WeatherStatus\Capabilities; |
||||
use OCP\AppFramework\App; |
||||
use OCP\AppFramework\Bootstrap\IBootContext; |
||||
use OCP\AppFramework\Bootstrap\IBootstrap; |
||||
use OCP\AppFramework\Bootstrap\IRegistrationContext; |
||||
use OCP\Dashboard\RegisterWidgetEvent; |
||||
use OCP\EventDispatcher\IEventDispatcher; |
||||
use OCP\EventDispatcher\Event; |
||||
use OCP\Util; |
||||
|
||||
/** |
||||
* Class Application |
||||
* |
||||
* @package OCA\WeatherStatus\AppInfo |
||||
*/ |
||||
class Application extends App implements IBootstrap { |
||||
|
||||
/** @var string */ |
||||
public const APP_ID = 'weather_status'; |
||||
|
||||
/** |
||||
* Application constructor. |
||||
* |
||||
* @param array $urlParams |
||||
*/ |
||||
public function __construct(array $urlParams = []) { |
||||
parent::__construct(self::APP_ID, $urlParams); |
||||
|
||||
$dispatcher = $this->getContainer()->query(IEventDispatcher::class); |
||||
$dispatcher->addListener(RegisterWidgetEvent::class, function (Event $e) { |
||||
Util::addScript(self::APP_ID, 'weather-status'); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* @inheritDoc |
||||
*/ |
||||
public function register(IRegistrationContext $context): void { |
||||
// Register OCS Capabilities |
||||
$context->registerCapability(Capabilities::class); |
||||
} |
||||
|
||||
public function boot(IBootContext $context): void { |
||||
} |
||||
} |
||||
@ -0,0 +1,55 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/** |
||||
* @copyright Copyright (c) 2020, Julien Veyssier |
||||
* |
||||
* @author Julien Veyssier <eneiluj@posteo.net> |
||||
* |
||||
* @license AGPL-3.0 |
||||
* |
||||
* This code is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU Affero General Public License, version 3, |
||||
* as published by the Free Software Foundation. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU Affero General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU Affero General Public License, version 3, |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/> |
||||
* |
||||
*/ |
||||
namespace OCA\WeatherStatus; |
||||
|
||||
use OCP\Capabilities\ICapability; |
||||
|
||||
use OCA\WeatherStatus\AppInfo\Application; |
||||
|
||||
/** |
||||
* Class Capabilities |
||||
* |
||||
* @package OCA\UserStatus |
||||
*/ |
||||
class Capabilities implements ICapability { |
||||
|
||||
/** |
||||
* Capabilities constructor. |
||||
* |
||||
*/ |
||||
public function __construct() { |
||||
} |
||||
|
||||
/** |
||||
* @inheritDoc |
||||
*/ |
||||
public function getCapabilities() { |
||||
return [ |
||||
Application::APP_ID => [ |
||||
'enabled' => true, |
||||
], |
||||
]; |
||||
} |
||||
} |
||||
@ -0,0 +1,124 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/** |
||||
* @copyright Copyright (c) 2020, Julien Veyssier |
||||
* |
||||
* @author Julien Veyssier <eneiluj@posteo.net> |
||||
* |
||||
* @license AGPL-3.0 |
||||
* |
||||
* This code is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU Affero General Public License, version 3, |
||||
* as published by the Free Software Foundation. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU Affero General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU Affero General Public License, version 3, |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/> |
||||
* |
||||
*/ |
||||
namespace OCA\WeatherStatus\Controller; |
||||
|
||||
use OCA\WeatherStatus\Service\WeatherStatusService; |
||||
use OCP\AppFramework\Http; |
||||
use OCP\AppFramework\Http\DataResponse; |
||||
use OCP\AppFramework\OCSController; |
||||
use OCP\ILogger; |
||||
use OCP\IRequest; |
||||
|
||||
class WeatherStatusController extends OCSController { |
||||
|
||||
/** @var string */ |
||||
private $userId; |
||||
|
||||
/** @var ILogger */ |
||||
private $logger; |
||||
|
||||
/** @var WeatherStatusService */ |
||||
private $service; |
||||
|
||||
public function __construct(string $appName, |
||||
IRequest $request, |
||||
ILogger $logger, |
||||
WeatherStatusService $service, |
||||
string $userId) { |
||||
parent::__construct($appName, $request); |
||||
$this->userId = $userId; |
||||
$this->logger = $logger; |
||||
$this->service = $service; |
||||
} |
||||
|
||||
/** |
||||
* @NoAdminRequired |
||||
* |
||||
* Try to use the address set in user personal settings as weather location |
||||
* |
||||
* @return DataResponse with success state and address information |
||||
*/ |
||||
public function usePersonalAddress(): DataResponse { |
||||
return new DataResponse($this->service->usePersonalAddress()); |
||||
} |
||||
|
||||
/** |
||||
* @NoAdminRequired |
||||
* |
||||
* Change the weather status mode. There are currently 2 modes: |
||||
* - ask the browser |
||||
* - use the user defined address |
||||
* |
||||
* @param int $mode New mode |
||||
* @return DataResponse success state |
||||
*/ |
||||
public function setMode(int $mode): DataResponse { |
||||
return new DataResponse($this->service->setMode($mode)); |
||||
} |
||||
|
||||
/** |
||||
* @NoAdminRequired |
||||
* |
||||
* Set address and resolve it to get coordinates |
||||
* or directly set coordinates and get address with reverse geocoding |
||||
* |
||||
* @param string|null $address Any approximative or exact address |
||||
* @param float|null $lat Latitude in decimal degree format |
||||
* @param float|null $lon Longitude in decimal degree format |
||||
* @return DataResponse with success state and address information |
||||
*/ |
||||
public function setLocation(?string $address, ?float $lat, ?float $lon): DataResponse { |
||||
$currentWeather = $this->service->setLocation($address, $lat, $lon); |
||||
return new DataResponse($currentWeather); |
||||
} |
||||
|
||||
/** |
||||
* @NoAdminRequired |
||||
* |
||||
* Get stored user location |
||||
* |
||||
* @return DataResponse which contains coordinates, formatted address and current weather status mode |
||||
*/ |
||||
public function getLocation(): DataResponse { |
||||
$location = $this->service->getLocation(); |
||||
return new DataResponse($location); |
||||
} |
||||
|
||||
/** |
||||
* @NoAdminRequired |
||||
* |
||||
* Get forecast for current location |
||||
* |
||||
* @return DataResponse which contains success state and filtered forecast data |
||||
*/ |
||||
public function getForecast(): DataResponse { |
||||
$forecast = $this->service->getForecast(); |
||||
if (isset($forecast['success']) && $forecast['success'] === false) { |
||||
return new DataResponse($forecast, Http::STATUS_NOT_FOUND); |
||||
} else { |
||||
return new DataResponse($forecast); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,429 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/** |
||||
* @copyright Copyright (c) 2020, Julien Veyssier |
||||
* |
||||
* @author Julien Veyssier <eneiluj@posteo.net> |
||||
* |
||||
* @license AGPL-3.0 |
||||
* |
||||
* This code is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU Affero General Public License, version 3, |
||||
* as published by the Free Software Foundation. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU Affero General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU Affero General Public License, version 3, |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/> |
||||
* |
||||
*/ |
||||
|
||||
namespace OCA\WeatherStatus\Service; |
||||
|
||||
use OCP\IConfig; |
||||
use OCP\IL10N; |
||||
use OCP\App\IAppManager; |
||||
use OCP\Accounts\IAccountManager; |
||||
use OCP\Accounts\PropertyDoesNotExistException; |
||||
use OCP\IUserManager; |
||||
use OCP\Http\Client\IClientService; |
||||
use OCP\Http\Client\IClient; |
||||
use OCP\ICacheFactory; |
||||
use OCP\ICache; |
||||
use OCP\ILogger; |
||||
|
||||
use OCA\WeatherStatus\AppInfo\Application; |
||||
|
||||
/** |
||||
* Class WeatherStatusService |
||||
* |
||||
* @package OCA\WeatherStatus\Service |
||||
*/ |
||||
class WeatherStatusService { |
||||
public const MODE_BROWSER_LOCATION = 1; |
||||
public const MODE_MANUAL_LOCATION = 2; |
||||
|
||||
/** @var IClientService */ |
||||
private $clientService; |
||||
|
||||
/** @var IClient */ |
||||
private $client; |
||||
|
||||
/** @var IConfig */ |
||||
private $config; |
||||
|
||||
/** @var IL10N */ |
||||
private $l10n; |
||||
|
||||
/** @var ILogger */ |
||||
private $logger; |
||||
|
||||
/** @var IAccountManager */ |
||||
private $accountManager; |
||||
|
||||
/** @var IUserManager */ |
||||
private $userManager; |
||||
|
||||
/** @var IAppManager */ |
||||
private $appManager; |
||||
|
||||
/** @var ICacheFactory */ |
||||
private $cacheFactory; |
||||
|
||||
/** @var ICache */ |
||||
private $cache; |
||||
|
||||
/** @var string */ |
||||
private $userId; |
||||
|
||||
/** @var string */ |
||||
private $version; |
||||
|
||||
/** |
||||
* WeatherStatusService constructor |
||||
* |
||||
* @param IClientService $clientService |
||||
* @param IConfig $config |
||||
* @param IL10N $l10n |
||||
* @param ILogger $logger |
||||
* @param IAccountManager $accountManager |
||||
* @param IUserManager $userManager |
||||
* @param IAppManager $appManager |
||||
* @param ICacheFactory $cacheFactory |
||||
* @param string $userId |
||||
*/ |
||||
public function __construct(IClientService $clientService, |
||||
IConfig $config, |
||||
IL10N $l10n, |
||||
ILogger $logger, |
||||
IAccountManager $accountManager, |
||||
IUserManager $userManager, |
||||
IAppManager $appManager, |
||||
ICacheFactory $cacheFactory, |
||||
string $userId) { |
||||
$this->config = $config; |
||||
$this->userId = $userId; |
||||
$this->l10n = $l10n; |
||||
$this->logger = $logger; |
||||
$this->accountManager = $accountManager; |
||||
$this->userManager = $userManager; |
||||
$this->appManager = $appManager; |
||||
$this->version = $appManager->getAppVersion(Application::APP_ID); |
||||
$this->clientService = $clientService; |
||||
$this->client = $clientService->newClient(); |
||||
if ($cacheFactory->isAvailable()) { |
||||
$this->cache = $cacheFactory->createDistributed(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Change the weather status mode. There are currently 2 modes: |
||||
* - ask the browser |
||||
* - use the user defined address |
||||
* @param int $mode New mode |
||||
* @return array success state |
||||
*/ |
||||
public function setMode(int $mode): array { |
||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'mode', strval($mode)); |
||||
return ['success' => true]; |
||||
} |
||||
|
||||
/** |
||||
* Try to use the address set in user personal settings as weather location |
||||
* |
||||
* @return array with success state and address information |
||||
*/ |
||||
public function usePersonalAddress(): array { |
||||
$account = $this->accountManager->getAccount($this->userManager->get($this->userId)); |
||||
try { |
||||
$address = $account->getProperty('address')->getValue(); |
||||
} catch (PropertyDoesNotExistException $e) { |
||||
return ['success' => false]; |
||||
} |
||||
if ($address === '') { |
||||
return ['success' => false]; |
||||
} |
||||
return $this->setAddress($address); |
||||
} |
||||
|
||||
/** |
||||
* Set address and resolve it to get coordinates |
||||
* or directly set coordinates and get address with reverse geocoding |
||||
* |
||||
* @param string|null $address Any approximative or exact address |
||||
* @param float|null $lat Latitude in decimal degree format |
||||
* @param float|null $lon Longitude in decimal degree format |
||||
* @return array with success state and address information |
||||
*/ |
||||
public function setLocation(?string $address, ?float $lat, ?float $lon): array { |
||||
if (!is_null($lat) && !is_null($lon)) { |
||||
// store coordinates |
||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'lat', strval($lat)); |
||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'lon', strval($lon)); |
||||
// resolve and store formatted address |
||||
$address = $this->resolveLocation($lat, $lon); |
||||
$address = $address ? $address : $this->l10n->t('Unknown address'); |
||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'address', $address); |
||||
// get and store altitude |
||||
$altitude = $this->getAltitude($lat, $lon); |
||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'altitude', strval($altitude)); |
||||
return [ |
||||
'address' => $address, |
||||
'success' => true, |
||||
]; |
||||
} elseif ($address) { |
||||
return $this->setAddress($address); |
||||
} else { |
||||
return ['success' => false]; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Provide address information from coordinates |
||||
* |
||||
* @param float $lat Latitude in decimal degree format |
||||
* @param float $lon Longitude in decimal degree format |
||||
*/ |
||||
private function resolveLocation(float $lat, float $lon): ?string { |
||||
$params = [ |
||||
'lat' => number_format($lat, 2), |
||||
'lon' => number_format($lon, 2), |
||||
'addressdetails' => 1, |
||||
'format' => 'json', |
||||
]; |
||||
$url = 'https://nominatim.openstreetmap.org/reverse'; |
||||
$result = $this->requestJSON($url, $params); |
||||
return $this->formatOsmAddress($result); |
||||
} |
||||
|
||||
/** |
||||
* Get altitude from coordinates |
||||
* |
||||
* @param float $lat Latitude in decimal degree format |
||||
* @param float $lon Longitude in decimal degree format |
||||
* @return float altitude in meter |
||||
*/ |
||||
private function getAltitude(float $lat, float $lon): float { |
||||
$params = [ |
||||
'locations' => $lat . ',' . $lon, |
||||
]; |
||||
$url = 'https://api.opentopodata.org/v1/srtm30m'; |
||||
$result = $this->requestJSON($url, $params); |
||||
$altitude = 0; |
||||
if (isset($result['results']) && is_array($result['results']) && count($result['results']) > 0 |
||||
&& is_array($result['results'][0]) && isset($result['results'][0]['elevation'])) { |
||||
$altitude = floatval($result['results'][0]['elevation']); |
||||
} |
||||
return $altitude; |
||||
} |
||||
|
||||
/** |
||||
* @return string Formatted address from JSON nominatim result |
||||
*/ |
||||
private function formatOsmAddress(array $json): ?string { |
||||
if (isset($json['address']) && isset($json['display_name'])) { |
||||
$jsonAddr = $json['address']; |
||||
$cityAddress = ''; |
||||
// priority : city, town, village, municipality |
||||
if (isset($jsonAddr['city'])) { |
||||
$cityAddress .= $jsonAddr['city']; |
||||
} elseif (isset($jsonAddr['town'])) { |
||||
$cityAddress .= $jsonAddr['town']; |
||||
} elseif (isset($jsonAddr['village'])) { |
||||
$cityAddress .= $jsonAddr['village']; |
||||
} elseif (isset($jsonAddr['municipality'])) { |
||||
$cityAddress .= $jsonAddr['municipality']; |
||||
} else { |
||||
return $json['display_name']; |
||||
} |
||||
// post code |
||||
if (isset($jsonAddr['postcode'])) { |
||||
$cityAddress .= ', ' . $jsonAddr['postcode']; |
||||
} |
||||
// country |
||||
if (isset($jsonAddr['country'])) { |
||||
$cityAddress .= ', ' . $jsonAddr['country']; |
||||
return $cityAddress; |
||||
} else { |
||||
return $json['display_name']; |
||||
} |
||||
} elseif (isset($json['display_name'])) { |
||||
return $json['display_name']; |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* Set address and resolve it to get coordinates |
||||
* |
||||
* @param string $address Any approximative or exact address |
||||
* @return array with success state and address information (coordinates and formatted address) |
||||
*/ |
||||
public function setAddress(string $address): array { |
||||
$addressInfo = $this->searchForAddress($address); |
||||
if (isset($addressInfo['display_name']) && isset($addressInfo['lat']) && isset($addressInfo['lon'])) { |
||||
$formattedAddress = $this->formatOsmAddress($addressInfo); |
||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'address', $formattedAddress); |
||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'lat', strval($addressInfo['lat'])); |
||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'lon', strval($addressInfo['lon'])); |
||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'mode', strval(self::MODE_MANUAL_LOCATION)); |
||||
// get and store altitude |
||||
$altitude = $this->getAltitude(floatval($addressInfo['lat']), floatval($addressInfo['lon'])); |
||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'altitude', strval($altitude)); |
||||
return [ |
||||
'lat' => $addressInfo['lat'], |
||||
'lon' => $addressInfo['lon'], |
||||
'address' => $formattedAddress, |
||||
'success' => true, |
||||
]; |
||||
} else { |
||||
return ['success' => false]; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Ask nominatim information about an unformatted address |
||||
* |
||||
* @param string Unformatted address |
||||
* @return array Full Nominatim result for the given address |
||||
*/ |
||||
private function searchForAddress(string $address): array { |
||||
$params = [ |
||||
'format' => 'json', |
||||
'addressdetails' => '1', |
||||
'extratags' => '1', |
||||
'namedetails' => '1', |
||||
'limit' => '1', |
||||
]; |
||||
$url = 'https://nominatim.openstreetmap.org/search/' . $address; |
||||
$results = $this->requestJSON($url, $params); |
||||
if (count($results) > 0) { |
||||
return $results[0]; |
||||
} |
||||
return ['error' => $this->l10n->t('No result.')]; |
||||
} |
||||
|
||||
/** |
||||
* Get stored user location |
||||
* |
||||
* @return array which contains coordinates, formatted address and current weather status mode |
||||
*/ |
||||
public function getLocation(): array { |
||||
$lat = $this->config->getUserValue($this->userId, Application::APP_ID, 'lat', ''); |
||||
$lon = $this->config->getUserValue($this->userId, Application::APP_ID, 'lon', ''); |
||||
$address = $this->config->getUserValue($this->userId, Application::APP_ID, 'address', ''); |
||||
$mode = $this->config->getUserValue($this->userId, Application::APP_ID, 'mode', self::MODE_MANUAL_LOCATION); |
||||
return [ |
||||
'lat' => $lat, |
||||
'lon' => $lon, |
||||
'address' => $address, |
||||
'mode' => intval($mode), |
||||
]; |
||||
} |
||||
|
||||
/** |
||||
* Get forecast for current location |
||||
* |
||||
* @return array which contains success state and filtered forecast data |
||||
*/ |
||||
public function getForecast(): array { |
||||
$lat = $this->config->getUserValue($this->userId, Application::APP_ID, 'lat', ''); |
||||
$lon = $this->config->getUserValue($this->userId, Application::APP_ID, 'lon', ''); |
||||
$alt = $this->config->getUserValue($this->userId, Application::APP_ID, 'altitude', ''); |
||||
if (!is_numeric($alt)) { |
||||
$alt = 0; |
||||
} |
||||
if (is_numeric($lat) && is_numeric($lon)) { |
||||
return $this->forecastRequest(floatval($lat), floatval($lon), floatval($alt)); |
||||
} else { |
||||
return ['success' => false]; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Actually make the request to the forecast service |
||||
* |
||||
* @param float $lat Latitude of requested forecast, in decimal degree format |
||||
* @param float $lon Longitude of requested forecast, in decimal degree format |
||||
* @param float $altitude Altitude of requested forecast, in meter |
||||
* @param int $nbValues Number of forecast values (hours) |
||||
* @return array Filtered forecast data |
||||
*/ |
||||
private function forecastRequest(float $lat, float $lon, float $altitude, int $nbValues = 10): array { |
||||
$params = [ |
||||
'lat' => number_format($lat, 2), |
||||
'lon' => number_format($lon, 2), |
||||
'altitude' => $altitude, |
||||
]; |
||||
$url = 'https://api.met.no/weatherapi/locationforecast/2.0/compact'; |
||||
$weather = $this->requestJSON($url, $params); |
||||
if (isset($weather['properties']) && isset($weather['properties']['timeseries']) && is_array($weather['properties']['timeseries'])) { |
||||
return array_slice($weather['properties']['timeseries'], 0, $nbValues); |
||||
} |
||||
return ['error' => $this->l10n->t('Malformed JSON data.')]; |
||||
} |
||||
|
||||
/** |
||||
* Make a HTTP GET request and parse JSON result. |
||||
* Request results are cached until the 'Expires' response header says so |
||||
* |
||||
* @param string $url Base URL to query |
||||
* @param array $params GET parameters |
||||
* @return array which contains the error message or the parsed JSON result |
||||
*/ |
||||
private function requestJSON(string $url, array $params = []): array { |
||||
if (isset($this->cache)) { |
||||
$cacheKey = $url . '|' . implode(',', $params) . '|' . implode(',', array_keys($params)); |
||||
if ($this->cache->hasKey($cacheKey)) { |
||||
return $this->cache->get($cacheKey); |
||||
} |
||||
} |
||||
try { |
||||
$options = [ |
||||
'headers' => [ |
||||
'User-Agent' => 'NextcloudWeatherStatus/' . $this->version . ' nextcloud.com' |
||||
], |
||||
]; |
||||
|
||||
$reqUrl = $url; |
||||
if (count($params) > 0) { |
||||
$paramsContent = http_build_query($params); |
||||
$reqUrl = $url . '?' . $paramsContent; |
||||
} |
||||
|
||||
$response = $this->client->get($reqUrl, $options); |
||||
$body = $response->getBody(); |
||||
$headers = $response->getHeaders(); |
||||
$respCode = $response->getStatusCode(); |
||||
|
||||
if ($respCode >= 400) { |
||||
return ['error' => $this->l10n->t('Error')]; |
||||
} else { |
||||
$json = json_decode($body, true); |
||||
if (isset($this->cache)) { |
||||
// default cache duration is one hour |
||||
$cacheDuration = 60 * 60; |
||||
if (isset($headers['Expires']) && count($headers['Expires']) > 0) { |
||||
// if the Expires response header is set, use it to define cache duration |
||||
$expireTs = (new \Datetime($headers['Expires'][0]))->getTimestamp(); |
||||
$nowTs = (new \Datetime())->getTimestamp(); |
||||
$duration = $expireTs - $nowTs; |
||||
if ($duration > $cacheDuration) { |
||||
$cacheDuration = $duration; |
||||
} |
||||
} |
||||
$this->cache->set($cacheKey, $json, $cacheDuration); |
||||
} |
||||
return $json; |
||||
} |
||||
} catch (\Exception $e) { |
||||
$this->logger->warning($url . 'API error : ' . $e, ['app' => Application::APP_ID]); |
||||
return ['error' => $e->getMessage()]; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,503 @@ |
||||
<!-- |
||||
- @copyright Copyright (c) 2020 Julien Veyssier <eneiluj@posteo.net> |
||||
- @author Julien Veyssier <eneiluj@posteo.net> |
||||
- |
||||
- @license GNU AGPL version 3 or any later version |
||||
- |
||||
- This program is free software: you can redistribute it and/or modify |
||||
- it under the terms of the GNU Affero General Public License as |
||||
- published by the Free Software Foundation, either version 3 of the |
||||
- License, or (at your option) any later version. |
||||
- |
||||
- This program is distributed in the hope that it will be useful, |
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
- GNU Affero General Public License for more details. |
||||
- |
||||
- You should have received a copy of the GNU Affero General Public License |
||||
- along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
- |
||||
--> |
||||
|
||||
<template> |
||||
<li :class="{ inline }"> |
||||
<div id="weather-status-menu-item"> |
||||
<Actions |
||||
class="weather-status-menu-item__subheader" |
||||
:default-icon="weatherIcon" |
||||
:menu-title="visibleMessage"> |
||||
<ActionLink v-if="address && !errorMessage" |
||||
icon="icon-address" |
||||
target="_blank" |
||||
:href="weatherLinkTarget" |
||||
:close-after-click="true"> |
||||
{{ locationText }} |
||||
</ActionLink> |
||||
<ActionSeparator v-if="address && !errorMessage" /> |
||||
<ActionButton |
||||
icon="icon-crosshair" |
||||
:close-after-click="true" |
||||
@click="onBrowserLocationClick"> |
||||
{{ t('weather_status', 'Detect location') }} |
||||
</ActionButton> |
||||
<ActionInput |
||||
ref="addressInput" |
||||
:disabled="false" |
||||
icon="icon-rename" |
||||
type="text" |
||||
value="" |
||||
@submit="onAddressSubmit"> |
||||
{{ t('weather_status', 'Set custom address') }} |
||||
</ActionInput> |
||||
</Actions> |
||||
</div> |
||||
</li> |
||||
</template> |
||||
|
||||
<script> |
||||
import { showError } from '@nextcloud/dialogs' |
||||
import moment from '@nextcloud/moment' |
||||
import { getLocale } from '@nextcloud/l10n' |
||||
import Actions from '@nextcloud/vue/dist/Components/Actions' |
||||
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton' |
||||
import ActionInput from '@nextcloud/vue/dist/Components/ActionInput' |
||||
import ActionLink from '@nextcloud/vue/dist/Components/ActionLink' |
||||
import ActionSeparator from '@nextcloud/vue/dist/Components/ActionSeparator' |
||||
import * as network from './services/weatherStatusService' |
||||
|
||||
const MODE_BROWSER_LOCATION = 1 |
||||
const MODE_MANUAL_LOCATION = 2 |
||||
const weatherOptions = { |
||||
clearsky_day: { |
||||
icon: 'icon-clearsky-day', |
||||
text: t('weather_status', 'Clear sky'), |
||||
}, |
||||
clearsky_night: { |
||||
icon: 'icon-clearsky-night', |
||||
text: t('weather_status', 'Clear sky'), |
||||
}, |
||||
cloudy: { |
||||
icon: 'icon-cloudy', |
||||
text: t('weather_status', 'Cloudy'), |
||||
}, |
||||
fair_day: { |
||||
icon: 'icon-fair-day', |
||||
text: t('weather_status', 'Fair day'), |
||||
}, |
||||
fair_night: { |
||||
icon: 'icon-fair-night', |
||||
text: t('weather_status', 'Fair night'), |
||||
}, |
||||
partlycloudy_day: { |
||||
icon: 'icon-partlycloudy-day', |
||||
text: t('weather_status', 'Partly cloudy'), |
||||
}, |
||||
partlycloudy_night: { |
||||
icon: 'icon-partlycloudy-night', |
||||
text: t('weather_status', 'Partly cloudy'), |
||||
}, |
||||
fog: { |
||||
icon: 'icon-fog', |
||||
text: t('weather_status', 'Foggy'), |
||||
}, |
||||
lightrain: { |
||||
icon: 'icon-lightrain', |
||||
text: t('weather_status', 'Light rain'), |
||||
}, |
||||
rain: { |
||||
icon: 'icon-rain', |
||||
text: t('weather_status', 'Rain'), |
||||
}, |
||||
heavyrain: { |
||||
icon: 'icon-heavyrain', |
||||
text: t('weather_status', 'Heavy rain'), |
||||
}, |
||||
rainshowers_day: { |
||||
icon: 'icon-rainshowers-day', |
||||
text: t('weather_status', 'Rain showers'), |
||||
}, |
||||
rainshowers_night: { |
||||
icon: 'icon-rainshowers-night', |
||||
text: t('weather_status', 'Rain showers'), |
||||
}, |
||||
lightrainshowers_day: { |
||||
icon: 'icon-light-rainshowers-day', |
||||
text: t('weather_status', 'Light rain showers'), |
||||
}, |
||||
lightrainshowers_night: { |
||||
icon: 'icon-light-rainshowers-night', |
||||
text: t('weather_status', 'Light rain showers'), |
||||
}, |
||||
heavyrainshowers_day: { |
||||
icon: 'icon-heavy-rainshowers-day', |
||||
text: t('weather_status', 'Heavy rain showers'), |
||||
}, |
||||
heavyrainshowers_night: { |
||||
icon: 'icon-heavy-rainshowers-night', |
||||
text: t('weather_status', 'Heavy rain showers'), |
||||
}, |
||||
} |
||||
|
||||
export default { |
||||
name: 'App', |
||||
components: { |
||||
Actions, ActionButton, ActionInput, ActionLink, ActionSeparator, |
||||
}, |
||||
props: { |
||||
inline: { |
||||
type: Boolean, |
||||
default: false, |
||||
}, |
||||
}, |
||||
data() { |
||||
return { |
||||
locale: getLocale(), |
||||
loading: true, |
||||
errorMessage: '', |
||||
mode: MODE_BROWSER_LOCATION, |
||||
address: null, |
||||
lat: null, |
||||
lon: null, |
||||
forecasts: [], |
||||
loop: null, |
||||
} |
||||
}, |
||||
computed: { |
||||
useFahrenheitLocale() { |
||||
return ['en_US', 'en_MH', 'en_FM', 'en_PW', 'en_KY', 'en_LR'].includes(this.locale) |
||||
}, |
||||
strUnit() { |
||||
return this.useFahrenheitLocale ? '°F' : '°C' |
||||
}, |
||||
locationText() { |
||||
return t('weather_status', 'More weather for {adr}', { adr: this.address }) |
||||
}, |
||||
sixHoursTempForecast() { |
||||
return this.forecasts.length > 5 ? this.forecasts[5].data.instant.details.air_temperature : '' |
||||
}, |
||||
sixHoursWeatherForecast() { |
||||
return this.forecasts.length > 5 ? this.forecasts[5].data.next_1_hours.summary.symbol_code : '' |
||||
}, |
||||
sixHoursFormattedTime() { |
||||
if (this.forecasts.length > 5) { |
||||
const date = moment(this.forecasts[5].time) |
||||
return t('weather_status', 'at {time}', { time: date.format('LT') }) |
||||
} |
||||
return '' |
||||
}, |
||||
weatherIcon() { |
||||
if (this.loading) { |
||||
return 'icon-loading-small' |
||||
} else { |
||||
return this.sixHoursWeatherForecast && this.sixHoursWeatherForecast in weatherOptions |
||||
? weatherOptions[this.sixHoursWeatherForecast].icon |
||||
: 'icon-fair-day' |
||||
} |
||||
}, |
||||
weatherText() { |
||||
return this.sixHoursWeatherForecast && this.sixHoursWeatherForecast in weatherOptions |
||||
? weatherOptions[this.sixHoursWeatherForecast].text + ' ' + this.sixHoursFormattedTime |
||||
: '???' |
||||
}, |
||||
/** |
||||
* The message displayed in the top right corner |
||||
* |
||||
* @returns {String} |
||||
*/ |
||||
visibleMessage() { |
||||
if (this.loading) { |
||||
return t('weather_status', 'Loading weather') |
||||
} else if (this.errorMessage) { |
||||
return this.errorMessage |
||||
} else { |
||||
return this.sixHoursWeatherForecast |
||||
? t('weather_status', '{temperature} {unit} {weatherDescription}', { |
||||
temperature: this.getLocalizedTemperature(this.sixHoursTempForecast), |
||||
unit: this.strUnit, |
||||
weatherDescription: this.weatherText, |
||||
}) |
||||
: t('weather_status', 'Set location for weather') |
||||
} |
||||
}, |
||||
weatherLinkTarget() { |
||||
return 'https://www.windy.com/-Rain-thunder-rain?rain,' + this.lat + ',' + this.lon + ',11' |
||||
}, |
||||
}, |
||||
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() |
||||
} |
||||
} catch (err) { |
||||
showError(t('weather_status', 'There was an error getting the weather status information.')) |
||||
console.debug(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) { |
||||
showError(t('weather_status', 'There was an error setting the location address.')) |
||||
console.debug(err) |
||||
this.loading = false |
||||
} |
||||
}, |
||||
async saveLocation(lat, lon) { |
||||
try { |
||||
const loc = await network.setLocation(lat, lon) |
||||
this.address = loc.address |
||||
this.startLoop() |
||||
} catch (err) { |
||||
showError(t('weather_status', 'There was an error setting the location.')) |
||||
console.debug(err) |
||||
} |
||||
}, |
||||
async saveMode(mode) { |
||||
try { |
||||
await network.setMode(mode) |
||||
} catch (err) { |
||||
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) { |
||||
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).toFixed(1) |
||||
: celcius |
||||
}, |
||||
}, |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss"> |
||||
.icon-clearsky-day { |
||||
background-image: url('./../img/sun.svg'); |
||||
} |
||||
.icon-clearsky-night { |
||||
background-image: url('./../img/moon.svg'); |
||||
} |
||||
.icon-cloudy { |
||||
background-image: url('./../img/cloud-cloud.svg'); |
||||
} |
||||
.icon-fair-day { |
||||
background-image: url('./../img/sun-small-cloud.svg'); |
||||
} |
||||
.icon-fair-night { |
||||
background-image: url('./../img/moon-small-cloud.svg'); |
||||
} |
||||
.icon-partlycloudy-day { |
||||
background-image: url('./../img/sun-cloud.svg'); |
||||
} |
||||
.icon-partlycloudy-night { |
||||
background-image: url('./../img/moon-cloud.svg'); |
||||
} |
||||
.icon-fog { |
||||
background-image: url('./../img/fog.svg'); |
||||
} |
||||
.icon-lightrain { |
||||
background-image: url('./../img/light-rain.svg'); |
||||
} |
||||
.icon-rain { |
||||
background-image: url('./../img/rain.svg'); |
||||
} |
||||
.icon-heavyrain { |
||||
background-image: url('./../img/heavy-rain.svg'); |
||||
} |
||||
.icon-light-rainshowers-day { |
||||
background-image: url('./../img/sun-cloud-light-rain.svg'); |
||||
} |
||||
.icon-light-rainshowers-night { |
||||
background-image: url('./../img/moon-cloud-light-rain.svg'); |
||||
} |
||||
.icon-rainshowers-day { |
||||
background-image: url('./../img/sun-cloud-rain.svg'); |
||||
} |
||||
.icon-rainshowers-night { |
||||
background-image: url('./../img/moon-cloud-rain.svg'); |
||||
} |
||||
.icon-heavy-rainshowers-day { |
||||
background-image: url('./../img/sun-cloud-heavy-rain.svg'); |
||||
} |
||||
.icon-heavy-rainshowers-night { |
||||
background-image: url('./../img/moon-cloud-heavy-rain.svg'); |
||||
} |
||||
.icon-crosshair { |
||||
background-color: var(--color-main-text); |
||||
padding: 0 !important; |
||||
mask: url(./../img/cross.svg) no-repeat; |
||||
mask-size: 18px 18px; |
||||
mask-position: center; |
||||
-webkit-mask: url(./../img/cross.svg) no-repeat; |
||||
-webkit-mask-size: 18px 18px; |
||||
-webkit-mask-position: center; |
||||
min-width: 44px !important; |
||||
min-height: 44px !important; |
||||
} |
||||
|
||||
li:not(.inline) .weather-status-menu-item { |
||||
&__header { |
||||
display: block; |
||||
align-items: center; |
||||
color: var(--color-main-text); |
||||
padding: 10px 12px 5px 12px; |
||||
box-sizing: border-box; |
||||
opacity: 1; |
||||
white-space: nowrap; |
||||
width: 100%; |
||||
text-align: center; |
||||
max-width: 250px; |
||||
text-overflow: ellipsis; |
||||
min-width: 175px; |
||||
} |
||||
|
||||
&__subheader { |
||||
width: 100%; |
||||
|
||||
> button { |
||||
background-color: var(--color-main-background); |
||||
background-size: 16px; |
||||
border: 0; |
||||
border-radius: 0; |
||||
font-weight: normal; |
||||
font-size: 0.875em; |
||||
padding-left: 40px; |
||||
|
||||
&:hover, |
||||
&:focus { |
||||
box-shadow: inset 4px 0 var(--color-primary-element); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
body .inline .weather-status-menu-item__subheader > button { |
||||
background-color: rgba(255, 255, 255, 0.8); |
||||
} |
||||
|
||||
body.theme--dark .inline .weather-status-menu-item__subheader > button { |
||||
background-color: rgba(24, 24, 24, 0.8) !important; |
||||
} |
||||
|
||||
.inline .weather-status-menu-item__subheader { |
||||
width: 100%; |
||||
|
||||
> button { |
||||
background-size: 16px; |
||||
border: 0; |
||||
border-radius: var(--border-radius-pill); |
||||
font-weight: normal; |
||||
font-size: 0.875em; |
||||
padding-left: 40px; |
||||
|
||||
&:hover, |
||||
&:focus { |
||||
background-color: var(--color-background-hover); |
||||
} |
||||
|
||||
&.icon-loading-small { |
||||
&::after { |
||||
left: 21px; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
li { |
||||
list-style-type: none; |
||||
} |
||||
</style> |
||||
@ -0,0 +1,116 @@ |
||||
/** |
||||
* @copyright Copyright (c) 2020, Julien Veyssier |
||||
* |
||||
* @author Julien Veyssier <eneiluj@posteo.net> |
||||
* |
||||
* @license GNU AGPL version 3 or any later version |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU Affero General Public License as |
||||
* published by the Free Software Foundation, either version 3 of the |
||||
* License, or (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU Affero General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU Affero General Public License |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
* |
||||
*/ |
||||
import HttpClient from '@nextcloud/axios' |
||||
import { generateOcsUrl } from '@nextcloud/router' |
||||
|
||||
/** |
||||
* |
||||
* |
||||
* @param {String} lat the latitude |
||||
* @param {String} lon the longitude |
||||
* @returns {Promise<Object>} |
||||
*/ |
||||
const setLocation = async(lat, lon) => { |
||||
const url = generateOcsUrl('apps/weather_status/api/v1', 2) + 'location' |
||||
const response = await HttpClient.put(url, { |
||||
address: '', |
||||
lat, |
||||
lon, |
||||
}) |
||||
|
||||
return response.data.ocs.data |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @param {String} address The location |
||||
* @returns {Promise<Object>} |
||||
*/ |
||||
const setAddress = async(address) => { |
||||
const url = generateOcsUrl('apps/weather_status/api/v1', 2) + '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 |
||||
* @returns {Promise<Object>} |
||||
*/ |
||||
const setMode = async(mode) => { |
||||
const url = generateOcsUrl('apps/weather_status/api/v1', 2) + 'mode' |
||||
const response = await HttpClient.put(url, { |
||||
mode, |
||||
}) |
||||
|
||||
return response.data.ocs.data |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @returns {Promise<Object>} |
||||
*/ |
||||
const usePersonalAddress = async() => { |
||||
const url = generateOcsUrl('apps/weather_status/api/v1', 2) + 'use-personal' |
||||
const response = await HttpClient.put(url) |
||||
|
||||
return response.data.ocs.data |
||||
} |
||||
|
||||
/** |
||||
* Fetches the location information for current user |
||||
* |
||||
* @returns {Promise<Object>} |
||||
*/ |
||||
const getLocation = async() => { |
||||
const url = generateOcsUrl('apps/weather_status/api/v1', 2) + 'location' |
||||
const response = await HttpClient.get(url) |
||||
|
||||
return response.data.ocs.data |
||||
} |
||||
|
||||
/** |
||||
* Fetches the weather forecast |
||||
* |
||||
* @param {String} address The location |
||||
* @returns {Promise<Object>} |
||||
*/ |
||||
const fetchForecast = async() => { |
||||
const url = generateOcsUrl('apps/weather_status/api/v1', 2) + 'forecast' |
||||
const response = await HttpClient.get(url) |
||||
|
||||
return response.data.ocs.data |
||||
} |
||||
|
||||
export { |
||||
usePersonalAddress, |
||||
setMode, |
||||
getLocation, |
||||
setLocation, |
||||
setAddress, |
||||
fetchForecast, |
||||
} |
||||
@ -0,0 +1,28 @@ |
||||
import Vue from 'vue' |
||||
import { getRequestToken } from '@nextcloud/auth' |
||||
import { generateUrl } from '@nextcloud/router' |
||||
import App from './App' |
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
__webpack_nonce__ = btoa(getRequestToken()) |
||||
|
||||
// Correct the root of the app for chunk loading
|
||||
// eslint-disable-next-line
|
||||
__webpack_public_path__ = generateUrl('/apps/weather_status/js/') |
||||
|
||||
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({ |
||||
propsData: { |
||||
inline: true, |
||||
}, |
||||
}).$mount(el) |
||||
}) |
||||
}) |
||||
@ -0,0 +1,26 @@ |
||||
const path = require('path') |
||||
|
||||
module.exports = { |
||||
entry: { |
||||
'weather-status': path.join(__dirname, 'src', 'weather-status') |
||||
}, |
||||
output: { |
||||
path: path.resolve(__dirname, './js'), |
||||
publicPath: '/js/', |
||||
filename: '[name].js?v=[chunkhash]', |
||||
jsonpFunction: 'webpackJsonpWeatherStatus' |
||||
}, |
||||
optimization: { |
||||
splitChunks: { |
||||
automaticNameDelimiter: '-', |
||||
} |
||||
}, |
||||
module: { |
||||
rules: [ |
||||
{ |
||||
test: /\.(png|jpg|gif|svg|woff|woff2|eot|ttf)$/, |
||||
loader: 'url-loader', |
||||
}, |
||||
], |
||||
}, |
||||
} |
||||