some change
This commit is contained in:
commit
ce5cdfbc51
10
CHANGELOG.md
10
CHANGELOG.md
@ -1,3 +1,13 @@
|
||||
## 1.0.0 - 2020-11
|
||||
### Added
|
||||
- Complete new rewrite in VueJS
|
||||
Many bugs fixed and some features added.
|
||||
- Dashboard integration
|
||||
- Unified search support
|
||||
- Show recent played stations
|
||||
- Show sidebar with further infos
|
||||
- Improved performance and stability
|
||||
|
||||
## 0.6.6 - 2020-02
|
||||
### Added
|
||||
- French translation
|
||||
|
7
COPYING
7
COPYING
@ -1,7 +0,0 @@
|
||||
Copyright 2018 Jonas Heinrich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Jonas Heinrich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
2
Makefile
2
Makefile
@ -50,3 +50,5 @@ clean:
|
||||
clean-dev:
|
||||
rm -rf node_modules
|
||||
|
||||
appstore:
|
||||
krankerl package
|
||||
|
@ -5,7 +5,7 @@
|
||||
<name>Radio</name>
|
||||
<summary>Radio listening app</summary>
|
||||
<description>Listening to your favorite radio stations in Nextcloud</description>
|
||||
<version>1.0</version>
|
||||
<version>1.0.0</version>
|
||||
<licence>MIT</licence>
|
||||
<author mail="onny@project-insanity.org" >Jonas Heinrich</author>
|
||||
<namespace>Radio</namespace>
|
||||
|
@ -1,25 +1,4 @@
|
||||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
return [
|
||||
'resources' => [
|
||||
|
@ -1,7 +1,7 @@
|
||||
.icon-mail {
|
||||
.icon-radio {
|
||||
background-image: url(./../img/radio-trans.svg);
|
||||
}
|
||||
|
||||
body.theme--dark .icon-mail {
|
||||
body.theme--dark .icon-radio {
|
||||
background-image: url(./../img/radio.svg);
|
||||
}
|
||||
|
@ -1,26 +1,3 @@
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
@include icon-black-white('recent', 'radio', 1);
|
||||
@include icon-black-white('radio', 'radio', 1);
|
||||
@include icon-black-white('radio-trans', 'radio', 1);
|
||||
|
6
krankerl.toml
Normal file
6
krankerl.toml
Normal file
@ -0,0 +1,6 @@
|
||||
[package]
|
||||
before_cmds = [
|
||||
"composer install --no-dev -o",
|
||||
"npm install --deps",
|
||||
"npm run build",
|
||||
]
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace OCA\Radio\AppInfo;
|
||||
|
||||
use OC\Security\CSP\ContentSecurityPolicy;
|
||||
use OCA\Radio\Search\SearchProvider;
|
||||
use OCP\AppFramework\App;
|
||||
use OCP\AppFramework\Bootstrap\IBootContext;
|
||||
@ -30,8 +31,24 @@ class Application extends App implements IBootstrap {
|
||||
return $c->get(IRequest::class);
|
||||
});
|
||||
|
||||
$this->registerCsp();
|
||||
|
||||
}
|
||||
|
||||
public function boot(IBootContext $context): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow radio-browser hosts in the csp
|
||||
*
|
||||
* @throws \OCP\AppFramework\QueryException
|
||||
*/
|
||||
public function registerCsp() {
|
||||
$manager = $this->getContainer()->getServer()->getContentSecurityPolicyManager();
|
||||
$policy = new ContentSecurityPolicy();
|
||||
$policy->addAllowedConnectDomain('https://de1.api.radio-browser.info');
|
||||
$policy->addAllowedImageDomain('*');
|
||||
$policy->addAllowedMediaDomain('*');
|
||||
$manager->addDefaultPolicy($policy);
|
||||
}
|
||||
}
|
||||
|
@ -44,18 +44,27 @@ class FavoriteController extends Controller {
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*/
|
||||
public function create(string $stationuuid, string $name, string $favicon, string $urlresolved): DataResponse {
|
||||
public function create(string $stationuuid, string $name, string $favicon, string $urlresolved,
|
||||
string $bitrate, string $country, string $language, string $homepage,
|
||||
string $codec, string $tags): DataResponse {
|
||||
return new DataResponse($this->service->create($stationuuid, $name,
|
||||
$favicon, $urlresolved, $this->userId));
|
||||
$favicon, $urlresolved, $bitrate, $country, $language, $homepage, $codec,
|
||||
$tags, $this->userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*/
|
||||
public function update(int $id, string $stationuuid,
|
||||
string $name, string $favicon, string $urlresolved): DataResponse {
|
||||
return $this->handleNotFound(function () use ($id, $stationuuid, $name, $favicon, $urlresolved) {
|
||||
return $this->service->update($id, $stationuuid, $name, $favicon, $urlresolved, $this->userId);
|
||||
string $name, string $favicon, string $urlresolved,
|
||||
string $bitrate, string $country, string $language, string $homepage,
|
||||
string $codec, string $tags): DataResponse {
|
||||
return $this->handleNotFound(function () use ($id, $stationuuid, $name,
|
||||
$favicon, $urlresolved, $bitrate, $country, $language, $homepage, $codec,
|
||||
$tags) {
|
||||
return $this->service->update($id, $stationuuid, $name, $favicon,
|
||||
$urlresolved, $bitrate, $country, $language, $homepage, $codec,
|
||||
$tags, $this->userId);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,26 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Radio\Controller;
|
||||
|
||||
|
@ -44,18 +44,26 @@ class RecentController extends Controller {
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*/
|
||||
public function create(string $stationuuid, string $name, string $favicon, string $urlresolved): DataResponse {
|
||||
public function create(string $stationuuid, string $name, string $favicon, string $urlresolved,
|
||||
string $bitrate, string $country, string $language, string $homepage,
|
||||
string $codec, string $tags): DataResponse {
|
||||
return new DataResponse($this->service->create($stationuuid, $name,
|
||||
$favicon, $urlresolved, $this->userId));
|
||||
$favicon, $urlresolved, $bitrate, $country, $language, $homepage, $codec,
|
||||
$tags, $this->userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*/
|
||||
public function update(int $id, string $stationuuid,
|
||||
string $name, string $favicon, string $urlresolved): DataResponse {
|
||||
return $this->handleNotFound(function () use ($id, $stationuuid, $name, $favicon, $urlresolved) {
|
||||
return $this->service->update($id, $stationuuid, $name, $favicon, $urlresolved, $this->userId);
|
||||
public function update(int $id, string $stationuuid, string $name,
|
||||
string $favicon, string $urlresolved, string $bitrate, string $country,
|
||||
string $language, string $homepage, string $codec, string $tags): DataResponse {
|
||||
return $this->handleNotFound(function () use ($id, $stationuuid, $name,
|
||||
$favicon, $urlresolved, $bitrate, $country, $language, $homepage, $codec,
|
||||
$tags) {
|
||||
return $this->service->update($id, $stationuuid, $name, $favicon,
|
||||
$urlresolved, $bitrate, $country, $language, $homepage, $codec,
|
||||
$tags, $this->userId);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -79,7 +79,7 @@ class SettingsController extends ApiController {
|
||||
*/
|
||||
public function setMenuState($menuState = ""): JSONResponse {
|
||||
if ($menuState == 'SEARCH') {
|
||||
return true;
|
||||
return new JSONResponse(['status' => 'success'], Http::STATUS_OK);
|
||||
};
|
||||
$legalArguments = ['TOP', 'RECENT', 'NEW', 'FAVORITES', 'CATEGORIES'];
|
||||
if (!in_array($menuState, $legalArguments)) {
|
||||
|
@ -4,6 +4,8 @@ namespace OCA\Radio\Dashboard;
|
||||
|
||||
use OCP\Dashboard\IWidget;
|
||||
use OCP\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\Util;
|
||||
|
||||
use OCA\Radio\AppInfo\Application;
|
||||
|
||||
@ -12,24 +14,29 @@ class RadioWidget implements IWidget {
|
||||
/** @var IL10N */
|
||||
private $l10n;
|
||||
|
||||
/** @var IURLGenerator */
|
||||
private $urlGenerator;
|
||||
|
||||
public function __construct(
|
||||
IL10N $l10n
|
||||
IL10N $l10n,
|
||||
IURLGenerator $urlGenerator
|
||||
) {
|
||||
$this->l10n = $l10n;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getId(): string {
|
||||
return 'radio';
|
||||
return Application::APP_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getTitle(): string {
|
||||
return $this->l10n->t('Favorite Radio stations');
|
||||
return $this->l10n->t('Radio stations');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -50,14 +57,14 @@ class RadioWidget implements IWidget {
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getUrl(): ?string {
|
||||
return \OC::$server->getURLGenerator()->linkToRoute('settings.PersonalSettings.index', ['section' => 'connected-accounts']);
|
||||
return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute('radio.page.index'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function load(): void {
|
||||
\OC_Util::addScript(Application::APP_ID, Application::APP_ID . '-dashboard');
|
||||
\OC_Util::addStyle(Application::APP_ID, 'dashboard');
|
||||
Util::addScript(Application::APP_ID, 'radio-dashboard');
|
||||
Util::addStyle(Application::APP_ID, 'dashboard');
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,15 @@ class RecentMapper extends QBMapper {
|
||||
->addSelect('name')
|
||||
->addSelect('favicon')
|
||||
->addSelect('urlresolved')
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
->addSelect('bitrate')
|
||||
->addSelect('country')
|
||||
->addSelect('language')
|
||||
->addSelect('homepage')
|
||||
->addSelect('codec')
|
||||
->addSelect('tags')
|
||||
>>>>>>> nc20
|
||||
->from('recent')
|
||||
->orderBy('id', 'DESC')
|
||||
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
|
||||
@ -45,6 +54,15 @@ class RecentMapper extends QBMapper {
|
||||
->addSelect('name')
|
||||
->addSelect('favicon')
|
||||
->addSelect('urlresolved')
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
->addSelect('bitrate')
|
||||
->addSelect('country')
|
||||
->addSelect('language')
|
||||
->addSelect('homepage')
|
||||
->addSelect('codec')
|
||||
->addSelect('tags')
|
||||
>>>>>>> nc20
|
||||
->from('recent')
|
||||
->orderBy('id', 'DESC')
|
||||
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)));
|
||||
|
@ -11,6 +11,12 @@ class Station extends Entity implements JsonSerializable {
|
||||
protected $name;
|
||||
protected $favicon;
|
||||
protected $urlresolved;
|
||||
protected $bitrate;
|
||||
protected $country;
|
||||
protected $language;
|
||||
protected $homepage;
|
||||
protected $codec;
|
||||
protected $tags;
|
||||
protected $userId;
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
@ -19,7 +25,13 @@ class Station extends Entity implements JsonSerializable {
|
||||
'stationuuid' => $this->stationuuid,
|
||||
'name' => $this->name,
|
||||
'favicon' => $this->favicon,
|
||||
'urlresolved' => $this->urlresolved
|
||||
'urlresolved' => $this->urlresolved,
|
||||
'bitrate' => $this->bitrate,
|
||||
'country' => $this->country,
|
||||
'language' => $this->language,
|
||||
'homepage' => $this->homepage,
|
||||
'codec' => $this->codec,
|
||||
'tags' => $this->tags
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,12 @@ class Version000000Date20181013124731 extends SimpleMigrationStep {
|
||||
]);
|
||||
$table->addColumn('favicon', 'text');
|
||||
$table->addColumn('urlresolved', 'text');
|
||||
$table->addColumn('bitrate', 'text');
|
||||
$table->addColumn('country', 'text');
|
||||
$table->addColumn('language', 'text');
|
||||
$table->addColumn('homepage', 'text');
|
||||
$table->addColumn('codec', 'text');
|
||||
$table->addColumn('tags', 'text');
|
||||
|
||||
$table->setPrimaryKey(['id']);
|
||||
$table->addIndex(['user_id'], 'favorites_user_id_index');
|
||||
@ -60,6 +66,12 @@ class Version000000Date20181013124731 extends SimpleMigrationStep {
|
||||
]);
|
||||
$table->addColumn('favicon', 'text');
|
||||
$table->addColumn('urlresolved', 'text');
|
||||
$table->addColumn('bitrate', 'text');
|
||||
$table->addColumn('country', 'text');
|
||||
$table->addColumn('language', 'text');
|
||||
$table->addColumn('homepage', 'text');
|
||||
$table->addColumn('codec', 'text');
|
||||
$table->addColumn('tags', 'text');
|
||||
|
||||
$table->setPrimaryKey(['id']);
|
||||
$table->addIndex(['user_id'], 'recent_user_id_index');
|
||||
|
@ -45,23 +45,37 @@ class FavoriteService {
|
||||
}
|
||||
}
|
||||
|
||||
public function create($stationuuid, $name, $favicon, $urlresolved, $userId) {
|
||||
public function create($stationuuid, $name, $favicon, $urlresolved,
|
||||
$bitrate, $country, $language, $homepage, $codec, $tags, $userId) {
|
||||
$station = new Station();
|
||||
$station->setStationuuid($stationuuid);
|
||||
$station->setName($name);
|
||||
$station->setFavicon($favicon);
|
||||
$station->setUrlresolved($urlresolved);
|
||||
$station->setBitrate($bitrate);
|
||||
$station->setCountry($country);
|
||||
$station->setLanguage($language);
|
||||
$station->setHomepage($homepage);
|
||||
$station->setCodec($codec);
|
||||
$station->setTags($tags);
|
||||
$station->setUserId($userId);
|
||||
return $this->mapper->insert($station);
|
||||
}
|
||||
|
||||
public function update($id, $stationuuid, $name, $favicon, $urlresolved, $userId) {
|
||||
public function update($id, $stationuuid, $name, $favicon, $urlresolved,
|
||||
$bitrate, $country, $language, $homepage, $codec, $tags, $userId) {
|
||||
try {
|
||||
$station = $this->mapper->find($id, $userId);
|
||||
$station->setStationuuid($stationuuid);
|
||||
$station->setName($name);
|
||||
$station->setFavicon($favicon);
|
||||
$station->setUrlresolved($urlresolved);
|
||||
$station->setBitrate($bitrate);
|
||||
$station->setCountry($country);
|
||||
$station->setLanguage($language);
|
||||
$station->setHomepage($homepage);
|
||||
$station->setCodec($codec);
|
||||
$station->setTags($tags);
|
||||
return $this->mapper->update($station);
|
||||
} catch (Exception $e) {
|
||||
$this->handleException($e);
|
||||
|
@ -45,23 +45,37 @@ class RecentService {
|
||||
}
|
||||
}
|
||||
|
||||
public function create($stationuuid, $name, $favicon, $urlresolved, $userId) {
|
||||
public function create($stationuuid, $name, $favicon, $urlresolved,
|
||||
$bitrate, $country, $language, $homepage, $codec, $tags, $userId) {
|
||||
$station = new Station();
|
||||
$station->setStationuuid($stationuuid);
|
||||
$station->setName($name);
|
||||
$station->setFavicon($favicon);
|
||||
$station->setUrlresolved($urlresolved);
|
||||
$station->setBitrate($bitrate);
|
||||
$station->setCountry($country);
|
||||
$station->setLanguage($language);
|
||||
$station->setHomepage($homepage);
|
||||
$station->setCodec($codec);
|
||||
$station->setTags($tags);
|
||||
$station->setUserId($userId);
|
||||
return $this->mapper->insert($station);
|
||||
}
|
||||
|
||||
public function update($id, $stationuuid, $name, $favicon, $urlresolved, $userId) {
|
||||
public function update($id, $stationuuid, $name, $favicon, $urlresolved,
|
||||
$bitrate, $country, $language, $homepage, $codec, $tags, $userId) {
|
||||
try {
|
||||
$station = $this->mapper->find($id, $userId);
|
||||
$station->setStationuuid($stationuuid);
|
||||
$station->setName($name);
|
||||
$station->setFavicon($favicon);
|
||||
$station->setUrlresolved($urlresolved);
|
||||
$station->setBitrate($bitrate);
|
||||
$station->setCountry($country);
|
||||
$station->setLanguage($language);
|
||||
$station->setHomepage($homepage);
|
||||
$station->setCodec($codec);
|
||||
$station->setTags($tags);
|
||||
return $this->mapper->update($station);
|
||||
} catch (Exception $e) {
|
||||
$this->handleException($e);
|
||||
|
2902
package-lock.json
generated
2902
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
40
package.json
40
package.json
@ -30,16 +30,22 @@
|
||||
"stylelint:fix": "stylelint src --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nextcloud/axios": "^1.4.0",
|
||||
"@nextcloud/dialogs": "^3.0.0",
|
||||
"@nextcloud/axios": "^1.5.0",
|
||||
"@nextcloud/dialogs": "^3.1.1",
|
||||
"@nextcloud/l10n": "^1.4.1",
|
||||
"@nextcloud/moment": "^1.1.1",
|
||||
"@nextcloud/router": "^1.2.0",
|
||||
"@nextcloud/vue": "^2.7.0",
|
||||
"@nextcloud/vue": "^2.9.0",
|
||||
"@nextcloud/vue-dashboard": "^1.0.1",
|
||||
"axios": "^0.21.0",
|
||||
"howler": "^2.2.1",
|
||||
"music-metadata": "^7.4.1",
|
||||
"music-metadata": "^7.5.0",
|
||||
"style-loader": "^2.0.0",
|
||||
"vue": "^2.6.12",
|
||||
"vue-blurhash": "^0.1.2",
|
||||
"vue-router": "^3.4.7",
|
||||
"vue-clipboard2": "^0.3.1",
|
||||
"vue-resize-observer": "^1.0.32",
|
||||
"vue-router": "^3.4.9",
|
||||
"vuex": "^3.5.1"
|
||||
},
|
||||
"browserslist": [
|
||||
@ -49,38 +55,38 @@
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.6",
|
||||
"@babel/core": "^7.12.3",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/preset-env": "^7.11.5",
|
||||
"@babel/preset-env": "^7.12.1",
|
||||
"@nextcloud/browserslist-config": "^1.0.0",
|
||||
"@nextcloud/eslint-config": "^2.2.0",
|
||||
"@nextcloud/eslint-plugin": "^1.5.0",
|
||||
"@nextcloud/webpack-vue-config": "^1.1.0",
|
||||
"@vue/test-utils": "^1.1.0",
|
||||
"@nextcloud/webpack-vue-config": "^1.4.1",
|
||||
"@vue/test-utils": "^1.1.1",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^8.1.0",
|
||||
"babel-loader": "^8.2.1",
|
||||
"css-loader": "^3.6.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-standard": "^14.1.1",
|
||||
"eslint-import-resolver-webpack": "^0.13.0",
|
||||
"eslint-loader": "^4.0.2",
|
||||
"eslint-plugin-html": "^6.1.0",
|
||||
"eslint-plugin-html": "^6.1.1",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.0.1",
|
||||
"eslint-plugin-standard": "^4.1.0",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"file-loader": "^6.1.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"sass-loader": "^8.0.2",
|
||||
"stylelint": "^13.7.2",
|
||||
"stylelint": "^13.8.0",
|
||||
"stylelint-config-recommended-scss": "^4.2.0",
|
||||
"stylelint-scss": "^3.18.0",
|
||||
"stylelint-webpack-plugin": "^2.1.0",
|
||||
"vue-loader": "^15.9.3",
|
||||
"stylelint-webpack-plugin": "^2.1.1",
|
||||
"vue-loader": "^15.9.5",
|
||||
"vue-template-compiler": "^2.6.12",
|
||||
"webpack": "^4.44.2",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"webpack-merge": "^5.2.0"
|
||||
"webpack-merge": "^5.4.0"
|
||||
}
|
||||
}
|
||||
|
106
src/components/Dashboard.vue
Normal file
106
src/components/Dashboard.vue
Normal file
@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<DashboardWidget :items="items"
|
||||
:show-more-url="showMoreUrl"
|
||||
:show-more-text="title"
|
||||
:loading="state === 'loading'">
|
||||
<template #empty-content>
|
||||
<EmptyContent
|
||||
v-if="emptyContentMessage"
|
||||
:icon="emptyContentIcon">
|
||||
<template #desc>
|
||||
{{ emptyContentMessage }}
|
||||
</template>
|
||||
</EmptyContent>
|
||||
</template>
|
||||
</DashboardWidget>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { DashboardWidget } from '@nextcloud/vue-dashboard'
|
||||
import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
|
||||
|
||||
export default {
|
||||
name: 'Dashboard',
|
||||
|
||||
components: {
|
||||
DashboardWidget,
|
||||
EmptyContent,
|
||||
},
|
||||
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'radio',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
notifications: [],
|
||||
showMoreUrl: generateUrl('/apps/radio/#/favorites'),
|
||||
state: 'loading',
|
||||
darkThemeColor: OCA.Accessibility.theme === 'dark' ? 'ffffff' : '000000',
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
items() {
|
||||
return this.notifications.map((n) => {
|
||||
return {
|
||||
id: n.id,
|
||||
targetUrl: generateUrl('/apps/radio/#/favorites'),
|
||||
avatarUrl: n.favicon,
|
||||
mainText: n.name,
|
||||
subText: n.tags.replaceAll(',', ', '),
|
||||
}
|
||||
})
|
||||
},
|
||||
emptyContentMessage() {
|
||||
if (this.state === 'error') {
|
||||
return t('radio', 'Error fetching favorite stations')
|
||||
} else if (this.state === 'ok') {
|
||||
return t('radio', 'No favorites added yet!')
|
||||
}
|
||||
return ''
|
||||
},
|
||||
emptyContentIcon() {
|
||||
if (this.state === 'error') {
|
||||
return 'icon-close'
|
||||
} else if (this.state === 'ok') {
|
||||
return 'icon-checkmark'
|
||||
}
|
||||
return 'icon-checkmark'
|
||||
},
|
||||
},
|
||||
|
||||
beforeMount() {
|
||||
this.fetchNotifications()
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchNotifications() {
|
||||
const req = {}
|
||||
axios.get(generateUrl('/apps/radio/api/favorites'), req).then((response) => {
|
||||
this.processNotifications(response.data)
|
||||
this.state = 'ok'
|
||||
}).catch((error) => {
|
||||
clearInterval(this.loop)
|
||||
if (error.response && error.response.status === 401) {
|
||||
showError(t('radio', 'Failed to fetch favorite radio stations'))
|
||||
this.state = 'error'
|
||||
} else {
|
||||
// there was an error in notif processing
|
||||
console.debug(error)
|
||||
}
|
||||
})
|
||||
},
|
||||
processNotifications(newNotifications) {
|
||||
this.notifications = newNotifications
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
@ -4,7 +4,8 @@
|
||||
:station-data="tableData" />
|
||||
<AppContent>
|
||||
<Table
|
||||
v-if="!pageLoading && tableData.length > 0"
|
||||
v-show="!pageLoading && tableData.length > 0"
|
||||
v-resize="onResize"
|
||||
:station-data="tableData"
|
||||
:favorites="favorites"
|
||||
@doPlay="doPlay"
|
||||
@ -13,27 +14,22 @@
|
||||
<EmptyContent
|
||||
v-if="pageLoading"
|
||||
icon="icon-loading" />
|
||||
<template
|
||||
v-if="tableData.length === 0 && !pageLoading">
|
||||
<EmptyContent
|
||||
v-show="$route.name==='FAVORITES'"
|
||||
icon="icon-star">
|
||||
No favorites yet
|
||||
<template #desc>
|
||||
Stations you mark as favorite will show up here
|
||||
</template>
|
||||
</EmptyContent>
|
||||
<EmptyContent
|
||||
v-if="tableData.length === 0 && !pageLoading"
|
||||
v-show="$route.name==='SEARCH'"
|
||||
icon="icon-search">
|
||||
No search results
|
||||
:icon="emptyContentIcon">
|
||||
{{ emptyContentMessage }}
|
||||
<template #desc>
|
||||
No stations were found matching your search term
|
||||
{{ emptyContentDesc }}
|
||||
</template>
|
||||
</EmptyContent>
|
||||
</template>
|
||||
</AppContent>
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
<Sidebar
|
||||
:show-sidebar="showSidebar"
|
||||
:sidebar-station="sidebarStation"
|
||||
@toggleSidebar="toggleSidebar" />
|
||||
>>>>>>> nc20
|
||||
</Content>
|
||||
</template>
|
||||
|
||||
@ -41,15 +37,25 @@
|
||||
import Content from '@nextcloud/vue/dist/Components/Content'
|
||||
import AppContent from '@nextcloud/vue/dist/Components/AppContent'
|
||||
import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
|
||||
<<<<<<< HEAD
|
||||
import Navigation from './Navigation'
|
||||
import Table from './Table'
|
||||
import { Howl } from 'howler'
|
||||
|
||||
=======
|
||||
>>>>>>> nc20
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import axios from '@nextcloud/axios'
|
||||
|
||||
import Navigation from './Navigation'
|
||||
import Table from './Table'
|
||||
import Sidebar from './Sidebar'
|
||||
|
||||
import { Howl, Howler } from 'howler'
|
||||
|
||||
let audioPlayer = null
|
||||
const requesttoken = axios.defaults.headers.requesttoken
|
||||
|
||||
export default {
|
||||
name: 'Main',
|
||||
@ -59,24 +65,53 @@ export default {
|
||||
AppContent,
|
||||
Table,
|
||||
EmptyContent,
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
Sidebar,
|
||||
>>>>>>> nc20
|
||||
},
|
||||
data: () => ({
|
||||
tableData: [],
|
||||
pageLoading: false,
|
||||
blurHashes: require('../assets/blurHashes.json'),
|
||||
favorites: null,
|
||||
favorites: [],
|
||||
showSidebar: false,
|
||||
sidebarStation: [],
|
||||
sidebarStation: {},
|
||||
queryParams: {},
|
||||
}),
|
||||
computed: {
|
||||
player() {
|
||||
return this.$store.state.player
|
||||
},
|
||||
stationTags() {
|
||||
if (this.sidebarStation.tags) {
|
||||
return this.sidebarStation.tags.replaceAll(',', ', ')
|
||||
emptyContentMessage() {
|
||||
if (this.$route.name === 'FAVORITES') {
|
||||
return t('radio', 'No favorites yet')
|
||||
} else if (this.$route.name === 'RECENT') {
|
||||
return t('radio', 'No recent stations yet')
|
||||
} else if (this.$route.name === 'SEARCH') {
|
||||
return t('radio', 'No search results')
|
||||
}
|
||||
return ''
|
||||
return 'No stations here'
|
||||
},
|
||||
emptyContentIcon() {
|
||||
if (this.$route.name === 'FAVORITES') {
|
||||
return 'icon-star'
|
||||
} else if (this.$route.name === 'RECENT') {
|
||||
return 'icon-recent'
|
||||
} else if (this.$route.name === 'SEARCH') {
|
||||
return 'icon-search'
|
||||
}
|
||||
return 'icon-radio'
|
||||
},
|
||||
emptyContentDesc() {
|
||||
if (this.$route.name === 'FAVORITES') {
|
||||
return t('radio', 'Stations you mark as favorite will show up here')
|
||||
} else if (this.$route.name === 'RECENT') {
|
||||
return t('radio', 'Stations you recently played will show up here')
|
||||
} else if (this.$route.name === 'SEARCH') {
|
||||
return t('radio', 'No stations were found matching your search term')
|
||||
}
|
||||
return t('radio', 'No stations here')
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
@ -104,6 +139,19 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
|
||||
onResize({ width, height }) {
|
||||
const contentHeight = document.getElementById('app-content-vue').scrollHeight
|
||||
const tableHeight = height
|
||||
if (tableHeight < contentHeight) {
|
||||
this.preFill()
|
||||
}
|
||||
},
|
||||
|
||||
preFill() {
|
||||
const route = this.$route
|
||||
this.loadStations(route.name)
|
||||
},
|
||||
|
||||
async onRoute() {
|
||||
this.tableData = []
|
||||
this.pageLoading = true
|
||||
@ -124,6 +172,7 @@ export default {
|
||||
stationid = this.favorites[i][0]
|
||||
}
|
||||
}
|
||||
axios.defaults.headers.requesttoken = requesttoken
|
||||
await axios
|
||||
.delete(generateUrl(`/apps/radio/api/favorites/${stationid}`))
|
||||
.then(response => {
|
||||
@ -134,13 +183,26 @@ export default {
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
let stationSrc = ''
|
||||
if (!station.url_resolved) {
|
||||
stationSrc = station.urlresolved
|
||||
} else {
|
||||
stationSrc = station.url_resolved
|
||||
}
|
||||
const stationMap = {
|
||||
id: -1,
|
||||
name: station.name,
|
||||
urlresolved: station.url_resolved,
|
||||
favicon: station.favicon,
|
||||
stationuuid: station.stationuuid,
|
||||
name: station.name.toString(),
|
||||
urlresolved: stationSrc.toString(),
|
||||
favicon: station.favicon.toString(),
|
||||
stationuuid: station.stationuuid.toString(),
|
||||
bitrate: station.bitrate.toString(),
|
||||
country: station.country.toString(),
|
||||
language: station.language.toString(),
|
||||
homepage: station.homepage.toString(),
|
||||
codec: station.codec.toString(),
|
||||
tags: station.tags.toString(),
|
||||
}
|
||||
axios.defaults.headers.requesttoken = requesttoken
|
||||
await axios
|
||||
.post(generateUrl('/apps/radio/api/favorites'), stationMap)
|
||||
.then(response => {
|
||||
@ -157,8 +219,10 @@ export default {
|
||||
* @param {Object} station Station object
|
||||
*/
|
||||
async doPlay(station) {
|
||||
|
||||
const vm = this
|
||||
|
||||
vm.$store.dispatch('isBuffering', true)
|
||||
|
||||
if (audioPlayer !== null) {
|
||||
audioPlayer.fade(vm.player.volume, 0, 500)
|
||||
}
|
||||
@ -170,14 +234,11 @@ export default {
|
||||
} else {
|
||||
stationSrc = station.url_resolved
|
||||
}
|
||||
Howler.unload()
|
||||
audioPlayer = new Howl({
|
||||
src: stationSrc,
|
||||
html5: true,
|
||||
volume: vm.player.volume,
|
||||
/* onfade() { // FIXME
|
||||
if (this.volume() === 0) {
|
||||
this.unload()
|
||||
}
|
||||
}, */
|
||||
onplay() {
|
||||
vm.$store.dispatch('isPlaying', true)
|
||||
vm.$store.dispatch('isBuffering', false)
|
||||
@ -186,13 +247,10 @@ export default {
|
||||
vm.$store.dispatch('isPlaying', false)
|
||||
vm.$store.dispatch('isBuffering', false)
|
||||
},
|
||||
onload() {
|
||||
vm.$store.dispatch('isPlaying', true)
|
||||
vm.$store.dispatch('isBuffering', true)
|
||||
},
|
||||
onstop() {
|
||||
onend() {
|
||||
showError(t('radio', 'Lost connection to radio station, retrying ...'))
|
||||
vm.$store.dispatch('isPlaying', false)
|
||||
vm.$store.dispatch('isBuffering', false)
|
||||
vm.$store.dispatch('isBuffering', true)
|
||||
},
|
||||
})
|
||||
audioPlayer.unload()
|
||||
@ -200,17 +258,35 @@ export default {
|
||||
audioPlayer.fade(0, vm.player.volume, 500)
|
||||
|
||||
/* Count click */
|
||||
try {
|
||||
delete axios.defaults.headers.requesttoken
|
||||
axios.get(this.$apiUrl + '/json/url/' + station.stationuuid)
|
||||
} catch (error) {
|
||||
showError(t('radio', 'Unable to count play on remote API'))
|
||||
}
|
||||
|
||||
/* Put into recent stations */
|
||||
try {
|
||||
let stationSrc = ''
|
||||
if (!station.url_resolved) {
|
||||
stationSrc = station.urlresolved
|
||||
} else {
|
||||
stationSrc = station.url_resolved
|
||||
}
|
||||
const stationMap = {
|
||||
id: -1,
|
||||
name: station.name,
|
||||
urlresolved: station.url_resolved,
|
||||
favicon: station.favicon,
|
||||
stationuuid: station.stationuuid,
|
||||
name: station.name.toString(),
|
||||
urlresolved: stationSrc.toString(),
|
||||
favicon: station.favicon.toString(),
|
||||
stationuuid: station.stationuuid.toString(),
|
||||
bitrate: station.bitrate.toString(),
|
||||
country: station.country.toString(),
|
||||
language: station.language.toString(),
|
||||
homepage: station.homepage.toString(),
|
||||
codec: station.codec.toString(),
|
||||
tags: station.tags.toString(),
|
||||
}
|
||||
axios.defaults.headers.requesttoken = requesttoken
|
||||
await axios
|
||||
.post(generateUrl('/apps/radio/api/recent'), stationMap)
|
||||
} catch (error) {
|
||||
@ -219,10 +295,6 @@ export default {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetching radio stations using Radio-Browser.info API
|
||||
* @param {String} menuState Entries to load
|
||||
*/
|
||||
async loadStations(menuState = 'TOP') {
|
||||
|
||||
const vm = this
|
||||
@ -231,36 +303,63 @@ export default {
|
||||
let sortBy = 'clickcount'
|
||||
|
||||
if (vm.$route.name === 'CATEGORIES') {
|
||||
if (vm.$route.path === '/categories') {
|
||||
vm.tableData = [
|
||||
{
|
||||
name: 'Countries',
|
||||
name: t('radio', 'Countries'),
|
||||
type: 'folder',
|
||||
path: '#/categories/countries',
|
||||
path: '/categories/countries',
|
||||
},
|
||||
{
|
||||
name: 'States',
|
||||
name: t('radio', 'States'),
|
||||
type: 'folder',
|
||||
path: '#/categories/states',
|
||||
path: '/categories/states',
|
||||
},
|
||||
{
|
||||
name: 'Languages',
|
||||
name: t('radio', 'Languages'),
|
||||
type: 'folder',
|
||||
path: '#/categories/languages',
|
||||
path: '/categories/languages',
|
||||
},
|
||||
{
|
||||
name: 'Tags',
|
||||
name: t('radio', 'Tags'),
|
||||
type: 'folder',
|
||||
path: '#/categories/tags',
|
||||
path: '/categories/tags',
|
||||
},
|
||||
]
|
||||
vm.pageLoading = false
|
||||
return true
|
||||
} else if (vm.$route.params.category === 'tags') {
|
||||
if (vm.$route.params.query) {
|
||||
queryURI = this.$apiUrl + '/json/stations/search?tag=' + vm.$route.params.query + '&tagExact=true'
|
||||
} else {
|
||||
queryURI = this.$apiUrl + '/json/tags'
|
||||
}
|
||||
} else if (vm.$route.params.category === 'countries') {
|
||||
if (vm.$route.params.query) {
|
||||
queryURI = this.$apiUrl + '/json/stations/search?country=' + vm.$route.params.query + '&countryExact=true'
|
||||
} else {
|
||||
queryURI = this.$apiUrl + '/json/countries'
|
||||
}
|
||||
} else if (vm.$route.params.category === 'states') {
|
||||
if (vm.$route.params.query) {
|
||||
queryURI = this.$apiUrl + '/json/stations/search?state=' + vm.$route.params.query + '&stateExact=true'
|
||||
} else {
|
||||
queryURI = this.$apiUrl + '/json/states'
|
||||
}
|
||||
} else if (vm.$route.params.category === 'languages') {
|
||||
if (vm.$route.params.query) {
|
||||
queryURI = this.$apiUrl + '/json/stations/search?language=' + vm.$route.params.query + '&languageExact=true'
|
||||
} else {
|
||||
queryURI = this.$apiUrl + '/json/languages'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Skip loading more stations on certain sites
|
||||
// Skip loading more stations on certain sites
|
||||
if (vm.tableData.length > 0
|
||||
&& (vm.$route.name === 'FAVORITES'
|
||||
|| vm.$route.name === 'RECENT')) {
|
||||
|| vm.$route.name === 'RECENT'
|
||||
|| vm.$route.name === 'CATEGORIES')) {
|
||||
return true
|
||||
}
|
||||
|
||||
@ -277,26 +376,46 @@ export default {
|
||||
queryURI = generateUrl('/apps/radio/api/recent')
|
||||
}
|
||||
|
||||
await axios.get(queryURI, {
|
||||
params: {
|
||||
if (menuState !== 'CATEGORIES') {
|
||||
vm.queryParams = {
|
||||
limit: 20,
|
||||
order: sortBy,
|
||||
reverse: true,
|
||||
offset: vm.tableData.length,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
vm.queryParams = {}
|
||||
}
|
||||
|
||||
try {
|
||||
if (menuState === 'FAVORITES' || menuState === 'RECENT') {
|
||||
axios.defaults.headers.requesttoken = requesttoken
|
||||
} else {
|
||||
delete axios.defaults.headers.requesttoken
|
||||
}
|
||||
await axios.get(queryURI, {
|
||||
params: vm.queryParams,
|
||||
})
|
||||
.then(function(response) {
|
||||
for (let i = 0; i < response.data.length; i++) {
|
||||
const obj = response.data[i]
|
||||
if (obj.stationuuid) {
|
||||
let blurHash = vm.blurHashes[obj.stationuuid]
|
||||
if (!blurHash) {
|
||||
blurHash = 'L1TSUA?bj[?b~qfQfQj[ayfQfQfQ'
|
||||
}
|
||||
response.data[i].blurHash = blurHash
|
||||
} else {
|
||||
response.data[i].type = 'folder'
|
||||
response.data[i].path = vm.$route.path + '/' + obj.name
|
||||
}
|
||||
}
|
||||
vm.tableData = vm.tableData.concat(response.data)
|
||||
vm.pageLoading = false
|
||||
})
|
||||
} catch (error) {
|
||||
showError(t('radio', 'Could not fetch stations from remote API'))
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@ -310,18 +429,19 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
loadSettings() {
|
||||
|
||||
axios.defaults.headers.common = {
|
||||
'User-Agent': 'Nextcloud Radio App/1.0', // FIXME: Reference global version number
|
||||
}
|
||||
// axios.defaults.headers.common = {
|
||||
// 'User-Agent': 'Nextcloud Radio App/' + this.$version,
|
||||
// }
|
||||
this.$store.dispatch('getVolumeState')
|
||||
|
||||
},
|
||||
|
||||
async loadFavorites() {
|
||||
const vm = this
|
||||
try {
|
||||
axios.defaults.headers.requesttoken = requesttoken
|
||||
await axios.get(generateUrl('/apps/radio/api/favorites'))
|
||||
.then(function(response) {
|
||||
const favorites = []
|
||||
@ -330,11 +450,18 @@ export default {
|
||||
}
|
||||
vm.favorites = favorites
|
||||
})
|
||||
} catch (error) {
|
||||
showError(t('radio', 'Unable to load favorites'))
|
||||
}
|
||||
},
|
||||
|
||||
toggleSidebar(station) {
|
||||
toggleSidebar(station = null) {
|
||||
if (station) {
|
||||
this.showSidebar = true
|
||||
this.sidebarStation = station
|
||||
} else {
|
||||
this.showSidebar = false
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -342,27 +469,10 @@ export default {
|
||||
|
||||
<style>
|
||||
|
||||
/* Make breadcrumbs sticky and intransparent.
|
||||
Move them to the right and show navigation
|
||||
toggle on smaller screens */
|
||||
|
||||
.breadcrumbs {
|
||||
background-color: var(--color-main-background-translucent);
|
||||
z-index: 60;
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: 50px;
|
||||
padding-bottom: 5px;
|
||||
margin-left: 35px;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 1024px) {
|
||||
.app-navigation-toggle {
|
||||
display: none;
|
||||
}
|
||||
.breadcrumbs {
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
161
src/components/Sidebar.vue
Normal file
161
src/components/Sidebar.vue
Normal file
@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<AppSidebar
|
||||
v-show="showSidebar"
|
||||
:title="sidebarStation.name"
|
||||
:subtitle="stationTags"
|
||||
:background="sidebarStation.favicon"
|
||||
class="has-preview"
|
||||
@close="toggleSidebar">
|
||||
<div class="configBox">
|
||||
<span class="icon icon-link" />
|
||||
<span class="title">
|
||||
{{ t('radio', 'Stream URL') }}
|
||||
</span>
|
||||
<div class="content">
|
||||
<input type="text" :value="urlResolved" disabled="disabled">
|
||||
<Actions>
|
||||
<ActionButton icon="icon-clippy" @click="copyLink">
|
||||
{{ t('radio', 'Copy link to clipboard') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
</div>
|
||||
</div>
|
||||
<div class="configBox">
|
||||
<span class="icon icon-external" />
|
||||
<span class="title">
|
||||
{{ t('radio', 'Homepage') }}
|
||||
</span>
|
||||
<div class="content">
|
||||
<span>
|
||||
<a
|
||||
:href="sidebarStation.homepage"
|
||||
target="new">
|
||||
{{ sidebarStation.homepage }}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="configBox">
|
||||
<span class="icon icon-details" />
|
||||
<span class="title">
|
||||
{{ t('radio', 'Country & Language') }}
|
||||
</span>
|
||||
<div class="content">
|
||||
<span>
|
||||
{{ sidebarStation.country }}, {{ sidebarStation.language }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="configBox">
|
||||
<span class="icon icon-details" />
|
||||
<span class="title">
|
||||
{{ t('radio', 'Codec & Bitrate') }}
|
||||
</span>
|
||||
<div class="content">
|
||||
<span>
|
||||
{{ sidebarStation.codec }}, {{ sidebarStation.bitrate }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</AppSidebar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AppSidebar from '@nextcloud/vue/dist/Components/AppSidebar'
|
||||
import Actions from '@nextcloud/vue/dist/Components/Actions'
|
||||
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
|
||||
export default {
|
||||
name: 'Sidebar',
|
||||
components: {
|
||||
AppSidebar,
|
||||
Actions,
|
||||
ActionButton,
|
||||
},
|
||||
props: {
|
||||
showSidebar: {
|
||||
type: Boolean,
|
||||
default() { return false },
|
||||
},
|
||||
sidebarStation: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
urlResolved() {
|
||||
if (this.sidebarStation.url_resolved) {
|
||||
return this.sidebarStation.url_resolved
|
||||
} else {
|
||||
return this.sidebarStation.urlresolved
|
||||
}
|
||||
},
|
||||
stationTags() {
|
||||
if (this.sidebarStation.tags) {
|
||||
return this.sidebarStation.tags.replaceAll(',', ', ')
|
||||
}
|
||||
return ''
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleSidebar(station) {
|
||||
this.$emit('toggleSidebar')
|
||||
},
|
||||
copyLink() {
|
||||
this.$copyText(this.urlResolved).then(
|
||||
function() {
|
||||
showSuccess(t('radio', 'Link copied to clipboard'))
|
||||
},
|
||||
function() {
|
||||
showError(t('radio', 'Error while copying link to clipboard'))
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.app-sidebar {
|
||||
&.has-preview::v-deep {
|
||||
.app-sidebar-header__figure {
|
||||
background-size: cover;
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.configBox {
|
||||
padding: 0 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.configBox .title {
|
||||
font-size: 1.2em;
|
||||
display: block;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.configBox .icon {
|
||||
float: left;
|
||||
margin: 4px 7px 0px 0px;
|
||||
}
|
||||
|
||||
.configBox .content {
|
||||
display: flex;
|
||||
margin-left: 25px;
|
||||
}
|
||||
|
||||
.configBox .content input {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.configBox .content button {
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
||||
</style>
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<table v-if="stationData" id="table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="iconColumn" />
|
||||
@ -10,6 +10,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-if="!isFolder">
|
||||
<tr
|
||||
v-for="(station, idx) in stationData"
|
||||
:key="idx"
|
||||
@ -53,6 +54,23 @@
|
||||
</Actions>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template v-if="isFolder">
|
||||
<tr
|
||||
v-for="(station, idx) in stationData"
|
||||
:key="idx"
|
||||
@click="changeRoute(station.path)">
|
||||
<td>
|
||||
<span class="icon-folder" />
|
||||
</td>
|
||||
<td class="nameColumn">
|
||||
<span class="innernametext">
|
||||
{{ station.name }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="actionColumn" />
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
@ -80,6 +98,16 @@ export default {
|
||||
data: () => ({
|
||||
activeItem: null,
|
||||
}),
|
||||
computed: {
|
||||
isFolder() {
|
||||
if (this.stationData[0]) {
|
||||
if (this.stationData[0].type === 'folder') {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
doPlay(idx, station) {
|
||||
this.activeItem = idx
|
||||
@ -91,6 +119,9 @@ export default {
|
||||
toggleSidebar(station) {
|
||||
this.$emit('toggleSidebar', station)
|
||||
},
|
||||
changeRoute(path) {
|
||||
this.$router.push({ path })
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -151,6 +182,13 @@ table {
|
||||
background-repeat: no-repeat;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
cursor: pointer;
|
||||
|
||||
span.icon-folder {
|
||||
display: block;
|
||||
background-size: cover;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
@ -178,10 +216,15 @@ table {
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
td.nameColumn {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
td.nameColumn .innernametext {
|
||||
color: var(--color-main-text);
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
vertical-align: top;
|
||||
user-select: none;
|
||||
|
15
src/dashboard.js
Normal file
15
src/dashboard.js
Normal file
@ -0,0 +1,15 @@
|
||||
import Vue from 'vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import Dashboard from './components/Dashboard.vue'
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
OCA.Dashboard.register('radio', (el) => {
|
||||
global.Radio = new Vue({
|
||||
el,
|
||||
store,
|
||||
router,
|
||||
render: h => h(Dashboard),
|
||||
})
|
||||
})
|
||||
})
|
@ -28,15 +28,21 @@ import { translate, translatePlural } from '@nextcloud/l10n'
|
||||
import App from './App'
|
||||
|
||||
import VueBlurHash from 'vue-blurhash'
|
||||
import VueClipboard from 'vue-clipboard2'
|
||||
import VueResizeObserver from 'vue-resize-observer'
|
||||
|
||||
import 'vue-blurhash/dist/vue-blurhash.css'
|
||||
|
||||
Vue.prototype.t = translate
|
||||
Vue.prototype.n = translatePlural
|
||||
|
||||
Vue.prototype.OC = window.OC
|
||||
Vue.prototype.OCA = window.OCA
|
||||
Vue.prototype.$apiUrl = 'https://de1.api.radio-browser.info'
|
||||
Vue.prototype.$version = '1.0'
|
||||
|
||||
Vue.use(VueClipboard)
|
||||
Vue.use(VueBlurHash)
|
||||
Vue.use(VueResizeObserver)
|
||||
|
||||
export default new Vue({
|
||||
el: '#content',
|
||||
|
@ -7,6 +7,7 @@ import Main from './components/Main'
|
||||
import store from './store.js'
|
||||
|
||||
Vue.use(Router)
|
||||
const requesttoken = axios.defaults.headers.requesttoken
|
||||
|
||||
const router = new Router({
|
||||
base: generateUrl('/apps/radio/'),
|
||||
@ -33,7 +34,7 @@ const router = new Router({
|
||||
name: 'FAVORITES',
|
||||
},
|
||||
{
|
||||
path: '/categories',
|
||||
path: '/categories/:category?/:query?',
|
||||
component: Main,
|
||||
name: 'CATEGORIES',
|
||||
},
|
||||
@ -52,6 +53,7 @@ router.beforeEach((to, from, next) => {
|
||||
store.dispatch('setMenuState', to.name)
|
||||
next()
|
||||
} else {
|
||||
axios.defaults.headers.requesttoken = requesttoken
|
||||
axios
|
||||
.get(generateUrl('/apps/radio/settings/menuState'))
|
||||
.then(async response => {
|
||||
|
@ -5,6 +5,7 @@ import axios from '@nextcloud/axios'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
Vue.use(Vuex)
|
||||
const requesttoken = axios.defaults.headers.requesttoken
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
@ -52,11 +53,13 @@ export default new Vuex.Store({
|
||||
state.player.title = title
|
||||
},
|
||||
setMenuState(state, menuState) {
|
||||
axios.defaults.headers.requesttoken = requesttoken
|
||||
axios.post(generateUrl('/apps/radio/settings/menuState'), {
|
||||
menuState,
|
||||
})
|
||||
},
|
||||
getMenuState(state) {
|
||||
axios.defaults.headers.requesttoken = requesttoken
|
||||
axios
|
||||
.get(generateUrl('/apps/radio/settings/menuState'))
|
||||
.then(async response => {
|
||||
@ -67,11 +70,13 @@ export default new Vuex.Store({
|
||||
})
|
||||
},
|
||||
setVolumeState(state, volumeState) {
|
||||
axios.defaults.headers.requesttoken = requesttoken
|
||||
axios.post(generateUrl('/apps/radio/settings/volumeState'), {
|
||||
volumeState,
|
||||
})
|
||||
},
|
||||
getVolumeState(state) {
|
||||
axios.defaults.headers.requesttoken = requesttoken
|
||||
axios
|
||||
.get(generateUrl('/apps/radio/settings/volumeState'))
|
||||
.then(async response => {
|
||||
|
11
webpack.js
11
webpack.js
@ -1,15 +1,16 @@
|
||||
const { merge } = require('webpack-merge')
|
||||
const path = require('path')
|
||||
const webpackConfig = require('@nextcloud/webpack-vue-config')
|
||||
|
||||
const config = {
|
||||
entry: {
|
||||
dashboard: path.join(__dirname, 'src', 'dashboard.js'),
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(png|jpg|gif)$/,
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]?[hash]',
|
||||
},
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user