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
|
## 0.6.6 - 2020-02
|
||||||
### Added
|
### Added
|
||||||
- French translation
|
- 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:
|
clean-dev:
|
||||||
rm -rf node_modules
|
rm -rf node_modules
|
||||||
|
|
||||||
|
appstore:
|
||||||
|
krankerl package
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<name>Radio</name>
|
<name>Radio</name>
|
||||||
<summary>Radio listening app</summary>
|
<summary>Radio listening app</summary>
|
||||||
<description>Listening to your favorite radio stations in Nextcloud</description>
|
<description>Listening to your favorite radio stations in Nextcloud</description>
|
||||||
<version>1.0</version>
|
<version>1.0.0</version>
|
||||||
<licence>MIT</licence>
|
<licence>MIT</licence>
|
||||||
<author mail="onny@project-insanity.org" >Jonas Heinrich</author>
|
<author mail="onny@project-insanity.org" >Jonas Heinrich</author>
|
||||||
<namespace>Radio</namespace>
|
<namespace>Radio</namespace>
|
||||||
|
@ -1,25 +1,4 @@
|
|||||||
<?php
|
<?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 [
|
return [
|
||||||
'resources' => [
|
'resources' => [
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
.icon-mail {
|
.icon-radio {
|
||||||
background-image: url(./../img/radio-trans.svg);
|
background-image: url(./../img/radio-trans.svg);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.theme--dark .icon-mail {
|
body.theme--dark .icon-radio {
|
||||||
background-image: url(./../img/radio.svg);
|
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('recent', 'radio', 1);
|
||||||
@include icon-black-white('radio', 'radio', 1);
|
@include icon-black-white('radio', 'radio', 1);
|
||||||
@include icon-black-white('radio-trans', '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;
|
namespace OCA\Radio\AppInfo;
|
||||||
|
|
||||||
|
use OC\Security\CSP\ContentSecurityPolicy;
|
||||||
use OCA\Radio\Search\SearchProvider;
|
use OCA\Radio\Search\SearchProvider;
|
||||||
use OCP\AppFramework\App;
|
use OCP\AppFramework\App;
|
||||||
use OCP\AppFramework\Bootstrap\IBootContext;
|
use OCP\AppFramework\Bootstrap\IBootContext;
|
||||||
@ -30,8 +31,24 @@ class Application extends App implements IBootstrap {
|
|||||||
return $c->get(IRequest::class);
|
return $c->get(IRequest::class);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$this->registerCsp();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function boot(IBootContext $context): void {
|
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,20 +44,29 @@ class FavoriteController extends Controller {
|
|||||||
/**
|
/**
|
||||||
* @NoAdminRequired
|
* @NoAdminRequired
|
||||||
*/
|
*/
|
||||||
public function create(string $stationuuid, string $name, string $favicon, string $urlresolved): DataResponse {
|
public function create(string $stationuuid, string $name, string $favicon, string $urlresolved,
|
||||||
return new DataResponse($this->service->create($stationuuid, $name,
|
string $bitrate, string $country, string $language, string $homepage,
|
||||||
$favicon, $urlresolved, $this->userId));
|
string $codec, string $tags): DataResponse {
|
||||||
}
|
return new DataResponse($this->service->create($stationuuid, $name,
|
||||||
|
$favicon, $urlresolved, $bitrate, $country, $language, $homepage, $codec,
|
||||||
|
$tags, $this->userId));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @NoAdminRequired
|
* @NoAdminRequired
|
||||||
*/
|
*/
|
||||||
public function update(int $id, string $stationuuid,
|
public function update(int $id, string $stationuuid,
|
||||||
string $name, string $favicon, string $urlresolved): DataResponse {
|
string $name, string $favicon, string $urlresolved,
|
||||||
return $this->handleNotFound(function () use ($id, $stationuuid, $name, $favicon, $urlresolved) {
|
string $bitrate, string $country, string $language, string $homepage,
|
||||||
return $this->service->update($id, $stationuuid, $name, $favicon, $urlresolved, $this->userId);
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @NoAdminRequired
|
* @NoAdminRequired
|
||||||
|
@ -1,26 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
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;
|
namespace OCA\Radio\Controller;
|
||||||
|
|
||||||
|
@ -44,18 +44,26 @@ class RecentController extends Controller {
|
|||||||
/**
|
/**
|
||||||
* @NoAdminRequired
|
* @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,
|
return new DataResponse($this->service->create($stationuuid, $name,
|
||||||
$favicon, $urlresolved, $this->userId));
|
$favicon, $urlresolved, $bitrate, $country, $language, $homepage, $codec,
|
||||||
|
$tags, $this->userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @NoAdminRequired
|
* @NoAdminRequired
|
||||||
*/
|
*/
|
||||||
public function update(int $id, string $stationuuid,
|
public function update(int $id, string $stationuuid, string $name,
|
||||||
string $name, string $favicon, string $urlresolved): DataResponse {
|
string $favicon, string $urlresolved, string $bitrate, string $country,
|
||||||
return $this->handleNotFound(function () use ($id, $stationuuid, $name, $favicon, $urlresolved) {
|
string $language, string $homepage, string $codec, string $tags): DataResponse {
|
||||||
return $this->service->update($id, $stationuuid, $name, $favicon, $urlresolved, $this->userId);
|
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 {
|
public function setMenuState($menuState = ""): JSONResponse {
|
||||||
if ($menuState == 'SEARCH') {
|
if ($menuState == 'SEARCH') {
|
||||||
return true;
|
return new JSONResponse(['status' => 'success'], Http::STATUS_OK);
|
||||||
};
|
};
|
||||||
$legalArguments = ['TOP', 'RECENT', 'NEW', 'FAVORITES', 'CATEGORIES'];
|
$legalArguments = ['TOP', 'RECENT', 'NEW', 'FAVORITES', 'CATEGORIES'];
|
||||||
if (!in_array($menuState, $legalArguments)) {
|
if (!in_array($menuState, $legalArguments)) {
|
||||||
|
@ -4,6 +4,8 @@ namespace OCA\Radio\Dashboard;
|
|||||||
|
|
||||||
use OCP\Dashboard\IWidget;
|
use OCP\Dashboard\IWidget;
|
||||||
use OCP\IL10N;
|
use OCP\IL10N;
|
||||||
|
use OCP\IURLGenerator;
|
||||||
|
use OCP\Util;
|
||||||
|
|
||||||
use OCA\Radio\AppInfo\Application;
|
use OCA\Radio\AppInfo\Application;
|
||||||
|
|
||||||
@ -12,24 +14,29 @@ class RadioWidget implements IWidget {
|
|||||||
/** @var IL10N */
|
/** @var IL10N */
|
||||||
private $l10n;
|
private $l10n;
|
||||||
|
|
||||||
|
/** @var IURLGenerator */
|
||||||
|
private $urlGenerator;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
IL10N $l10n
|
IL10N $l10n,
|
||||||
|
IURLGenerator $urlGenerator
|
||||||
) {
|
) {
|
||||||
$this->l10n = $l10n;
|
$this->l10n = $l10n;
|
||||||
|
$this->urlGenerator = $urlGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritDoc
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
public function getId(): string {
|
public function getId(): string {
|
||||||
return 'radio';
|
return Application::APP_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritDoc
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
public function getTitle(): string {
|
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
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
public function getUrl(): ?string {
|
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
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
public function load(): void {
|
public function load(): void {
|
||||||
\OC_Util::addScript(Application::APP_ID, Application::APP_ID . '-dashboard');
|
Util::addScript(Application::APP_ID, 'radio-dashboard');
|
||||||
\OC_Util::addStyle(Application::APP_ID, 'dashboard');
|
Util::addStyle(Application::APP_ID, 'dashboard');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,15 @@ class RecentMapper extends QBMapper {
|
|||||||
->addSelect('name')
|
->addSelect('name')
|
||||||
->addSelect('favicon')
|
->addSelect('favicon')
|
||||||
->addSelect('urlresolved')
|
->addSelect('urlresolved')
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
->addSelect('bitrate')
|
||||||
|
->addSelect('country')
|
||||||
|
->addSelect('language')
|
||||||
|
->addSelect('homepage')
|
||||||
|
->addSelect('codec')
|
||||||
|
->addSelect('tags')
|
||||||
|
>>>>>>> nc20
|
||||||
->from('recent')
|
->from('recent')
|
||||||
->orderBy('id', 'DESC')
|
->orderBy('id', 'DESC')
|
||||||
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
|
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
|
||||||
@ -45,6 +54,15 @@ class RecentMapper extends QBMapper {
|
|||||||
->addSelect('name')
|
->addSelect('name')
|
||||||
->addSelect('favicon')
|
->addSelect('favicon')
|
||||||
->addSelect('urlresolved')
|
->addSelect('urlresolved')
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
->addSelect('bitrate')
|
||||||
|
->addSelect('country')
|
||||||
|
->addSelect('language')
|
||||||
|
->addSelect('homepage')
|
||||||
|
->addSelect('codec')
|
||||||
|
->addSelect('tags')
|
||||||
|
>>>>>>> nc20
|
||||||
->from('recent')
|
->from('recent')
|
||||||
->orderBy('id', 'DESC')
|
->orderBy('id', 'DESC')
|
||||||
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)));
|
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)));
|
||||||
|
@ -11,6 +11,12 @@ class Station extends Entity implements JsonSerializable {
|
|||||||
protected $name;
|
protected $name;
|
||||||
protected $favicon;
|
protected $favicon;
|
||||||
protected $urlresolved;
|
protected $urlresolved;
|
||||||
|
protected $bitrate;
|
||||||
|
protected $country;
|
||||||
|
protected $language;
|
||||||
|
protected $homepage;
|
||||||
|
protected $codec;
|
||||||
|
protected $tags;
|
||||||
protected $userId;
|
protected $userId;
|
||||||
|
|
||||||
public function jsonSerialize(): array {
|
public function jsonSerialize(): array {
|
||||||
@ -19,7 +25,13 @@ class Station extends Entity implements JsonSerializable {
|
|||||||
'stationuuid' => $this->stationuuid,
|
'stationuuid' => $this->stationuuid,
|
||||||
'name' => $this->name,
|
'name' => $this->name,
|
||||||
'favicon' => $this->favicon,
|
'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('favicon', 'text');
|
||||||
$table->addColumn('urlresolved', '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->setPrimaryKey(['id']);
|
||||||
$table->addIndex(['user_id'], 'favorites_user_id_index');
|
$table->addIndex(['user_id'], 'favorites_user_id_index');
|
||||||
@ -60,6 +66,12 @@ class Version000000Date20181013124731 extends SimpleMigrationStep {
|
|||||||
]);
|
]);
|
||||||
$table->addColumn('favicon', 'text');
|
$table->addColumn('favicon', 'text');
|
||||||
$table->addColumn('urlresolved', '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->setPrimaryKey(['id']);
|
||||||
$table->addIndex(['user_id'], 'recent_user_id_index');
|
$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 = new Station();
|
||||||
$station->setStationuuid($stationuuid);
|
$station->setStationuuid($stationuuid);
|
||||||
$station->setName($name);
|
$station->setName($name);
|
||||||
$station->setFavicon($favicon);
|
$station->setFavicon($favicon);
|
||||||
$station->setUrlresolved($urlresolved);
|
$station->setUrlresolved($urlresolved);
|
||||||
|
$station->setBitrate($bitrate);
|
||||||
|
$station->setCountry($country);
|
||||||
|
$station->setLanguage($language);
|
||||||
|
$station->setHomepage($homepage);
|
||||||
|
$station->setCodec($codec);
|
||||||
|
$station->setTags($tags);
|
||||||
$station->setUserId($userId);
|
$station->setUserId($userId);
|
||||||
return $this->mapper->insert($station);
|
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 {
|
try {
|
||||||
$station = $this->mapper->find($id, $userId);
|
$station = $this->mapper->find($id, $userId);
|
||||||
$station->setStationuuid($stationuuid);
|
$station->setStationuuid($stationuuid);
|
||||||
$station->setName($name);
|
$station->setName($name);
|
||||||
$station->setFavicon($favicon);
|
$station->setFavicon($favicon);
|
||||||
$station->setUrlresolved($urlresolved);
|
$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);
|
return $this->mapper->update($station);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$this->handleException($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 = new Station();
|
||||||
$station->setStationuuid($stationuuid);
|
$station->setStationuuid($stationuuid);
|
||||||
$station->setName($name);
|
$station->setName($name);
|
||||||
$station->setFavicon($favicon);
|
$station->setFavicon($favicon);
|
||||||
$station->setUrlresolved($urlresolved);
|
$station->setUrlresolved($urlresolved);
|
||||||
|
$station->setBitrate($bitrate);
|
||||||
|
$station->setCountry($country);
|
||||||
|
$station->setLanguage($language);
|
||||||
|
$station->setHomepage($homepage);
|
||||||
|
$station->setCodec($codec);
|
||||||
|
$station->setTags($tags);
|
||||||
$station->setUserId($userId);
|
$station->setUserId($userId);
|
||||||
return $this->mapper->insert($station);
|
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 {
|
try {
|
||||||
$station = $this->mapper->find($id, $userId);
|
$station = $this->mapper->find($id, $userId);
|
||||||
$station->setStationuuid($stationuuid);
|
$station->setStationuuid($stationuuid);
|
||||||
$station->setName($name);
|
$station->setName($name);
|
||||||
$station->setFavicon($favicon);
|
$station->setFavicon($favicon);
|
||||||
$station->setUrlresolved($urlresolved);
|
$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);
|
return $this->mapper->update($station);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$this->handleException($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"
|
"stylelint:fix": "stylelint src --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nextcloud/axios": "^1.4.0",
|
"@nextcloud/axios": "^1.5.0",
|
||||||
"@nextcloud/dialogs": "^3.0.0",
|
"@nextcloud/dialogs": "^3.1.1",
|
||||||
"@nextcloud/l10n": "^1.4.1",
|
"@nextcloud/l10n": "^1.4.1",
|
||||||
|
"@nextcloud/moment": "^1.1.1",
|
||||||
"@nextcloud/router": "^1.2.0",
|
"@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",
|
"howler": "^2.2.1",
|
||||||
"music-metadata": "^7.4.1",
|
"music-metadata": "^7.5.0",
|
||||||
|
"style-loader": "^2.0.0",
|
||||||
"vue": "^2.6.12",
|
"vue": "^2.6.12",
|
||||||
"vue-blurhash": "^0.1.2",
|
"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"
|
"vuex": "^3.5.1"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
@ -49,38 +55,38 @@
|
|||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.11.6",
|
"@babel/core": "^7.12.3",
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.8.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/browserslist-config": "^1.0.0",
|
||||||
"@nextcloud/eslint-config": "^2.2.0",
|
"@nextcloud/eslint-config": "^2.2.0",
|
||||||
"@nextcloud/eslint-plugin": "^1.5.0",
|
"@nextcloud/eslint-plugin": "^1.5.0",
|
||||||
"@nextcloud/webpack-vue-config": "^1.1.0",
|
"@nextcloud/webpack-vue-config": "^1.4.1",
|
||||||
"@vue/test-utils": "^1.1.0",
|
"@vue/test-utils": "^1.1.1",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-loader": "^8.1.0",
|
"babel-loader": "^8.2.1",
|
||||||
"css-loader": "^3.6.0",
|
"css-loader": "^3.6.0",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^6.8.0",
|
||||||
"eslint-config-standard": "^14.1.1",
|
"eslint-config-standard": "^14.1.1",
|
||||||
"eslint-import-resolver-webpack": "^0.13.0",
|
"eslint-import-resolver-webpack": "^0.13.0",
|
||||||
"eslint-loader": "^4.0.2",
|
"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-import": "^2.22.1",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-promise": "^4.2.1",
|
"eslint-plugin-promise": "^4.2.1",
|
||||||
"eslint-plugin-standard": "^4.0.1",
|
"eslint-plugin-standard": "^4.1.0",
|
||||||
"eslint-plugin-vue": "^6.2.2",
|
"eslint-plugin-vue": "^6.2.2",
|
||||||
"file-loader": "^6.1.1",
|
"file-loader": "^6.2.0",
|
||||||
"node-sass": "^4.14.1",
|
"node-sass": "^4.14.1",
|
||||||
"sass-loader": "^8.0.2",
|
"sass-loader": "^8.0.2",
|
||||||
"stylelint": "^13.7.2",
|
"stylelint": "^13.8.0",
|
||||||
"stylelint-config-recommended-scss": "^4.2.0",
|
"stylelint-config-recommended-scss": "^4.2.0",
|
||||||
"stylelint-scss": "^3.18.0",
|
"stylelint-scss": "^3.18.0",
|
||||||
"stylelint-webpack-plugin": "^2.1.0",
|
"stylelint-webpack-plugin": "^2.1.1",
|
||||||
"vue-loader": "^15.9.3",
|
"vue-loader": "^15.9.5",
|
||||||
"vue-template-compiler": "^2.6.12",
|
"vue-template-compiler": "^2.6.12",
|
||||||
"webpack": "^4.44.2",
|
"webpack": "^4.44.2",
|
||||||
"webpack-cli": "^3.3.12",
|
"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" />
|
:station-data="tableData" />
|
||||||
<AppContent>
|
<AppContent>
|
||||||
<Table
|
<Table
|
||||||
v-if="!pageLoading && tableData.length > 0"
|
v-show="!pageLoading && tableData.length > 0"
|
||||||
|
v-resize="onResize"
|
||||||
:station-data="tableData"
|
:station-data="tableData"
|
||||||
:favorites="favorites"
|
:favorites="favorites"
|
||||||
@doPlay="doPlay"
|
@doPlay="doPlay"
|
||||||
@ -13,27 +14,22 @@
|
|||||||
<EmptyContent
|
<EmptyContent
|
||||||
v-if="pageLoading"
|
v-if="pageLoading"
|
||||||
icon="icon-loading" />
|
icon="icon-loading" />
|
||||||
<template
|
<EmptyContent
|
||||||
v-if="tableData.length === 0 && !pageLoading">
|
v-if="tableData.length === 0 && !pageLoading"
|
||||||
<EmptyContent
|
:icon="emptyContentIcon">
|
||||||
v-show="$route.name==='FAVORITES'"
|
{{ emptyContentMessage }}
|
||||||
icon="icon-star">
|
<template #desc>
|
||||||
No favorites yet
|
{{ emptyContentDesc }}
|
||||||
<template #desc>
|
</template>
|
||||||
Stations you mark as favorite will show up here
|
</EmptyContent>
|
||||||
</template>
|
|
||||||
</EmptyContent>
|
|
||||||
<EmptyContent
|
|
||||||
v-if="tableData.length === 0 && !pageLoading"
|
|
||||||
v-show="$route.name==='SEARCH'"
|
|
||||||
icon="icon-search">
|
|
||||||
No search results
|
|
||||||
<template #desc>
|
|
||||||
No stations were found matching your search term
|
|
||||||
</template>
|
|
||||||
</EmptyContent>
|
|
||||||
</template>
|
|
||||||
</AppContent>
|
</AppContent>
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
<Sidebar
|
||||||
|
:show-sidebar="showSidebar"
|
||||||
|
:sidebar-station="sidebarStation"
|
||||||
|
@toggleSidebar="toggleSidebar" />
|
||||||
|
>>>>>>> nc20
|
||||||
</Content>
|
</Content>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -41,15 +37,25 @@
|
|||||||
import Content from '@nextcloud/vue/dist/Components/Content'
|
import Content from '@nextcloud/vue/dist/Components/Content'
|
||||||
import AppContent from '@nextcloud/vue/dist/Components/AppContent'
|
import AppContent from '@nextcloud/vue/dist/Components/AppContent'
|
||||||
import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
|
import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
|
||||||
|
<<<<<<< HEAD
|
||||||
import Navigation from './Navigation'
|
import Navigation from './Navigation'
|
||||||
import Table from './Table'
|
import Table from './Table'
|
||||||
import { Howl } from 'howler'
|
import { Howl } from 'howler'
|
||||||
|
|
||||||
|
=======
|
||||||
|
>>>>>>> nc20
|
||||||
import { generateUrl } from '@nextcloud/router'
|
import { generateUrl } from '@nextcloud/router'
|
||||||
import { showError } from '@nextcloud/dialogs'
|
import { showError } from '@nextcloud/dialogs'
|
||||||
import axios from '@nextcloud/axios'
|
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
|
let audioPlayer = null
|
||||||
|
const requesttoken = axios.defaults.headers.requesttoken
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Main',
|
name: 'Main',
|
||||||
@ -59,24 +65,53 @@ export default {
|
|||||||
AppContent,
|
AppContent,
|
||||||
Table,
|
Table,
|
||||||
EmptyContent,
|
EmptyContent,
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
Sidebar,
|
||||||
|
>>>>>>> nc20
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
tableData: [],
|
tableData: [],
|
||||||
pageLoading: false,
|
pageLoading: false,
|
||||||
blurHashes: require('../assets/blurHashes.json'),
|
blurHashes: require('../assets/blurHashes.json'),
|
||||||
favorites: null,
|
favorites: [],
|
||||||
showSidebar: false,
|
showSidebar: false,
|
||||||
sidebarStation: [],
|
sidebarStation: {},
|
||||||
|
queryParams: {},
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
player() {
|
player() {
|
||||||
return this.$store.state.player
|
return this.$store.state.player
|
||||||
},
|
},
|
||||||
stationTags() {
|
emptyContentMessage() {
|
||||||
if (this.sidebarStation.tags) {
|
if (this.$route.name === 'FAVORITES') {
|
||||||
return this.sidebarStation.tags.replaceAll(',', ', ')
|
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: {
|
watch: {
|
||||||
@ -104,6 +139,19 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
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() {
|
async onRoute() {
|
||||||
this.tableData = []
|
this.tableData = []
|
||||||
this.pageLoading = true
|
this.pageLoading = true
|
||||||
@ -124,6 +172,7 @@ export default {
|
|||||||
stationid = this.favorites[i][0]
|
stationid = this.favorites[i][0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
axios.defaults.headers.requesttoken = requesttoken
|
||||||
await axios
|
await axios
|
||||||
.delete(generateUrl(`/apps/radio/api/favorites/${stationid}`))
|
.delete(generateUrl(`/apps/radio/api/favorites/${stationid}`))
|
||||||
.then(response => {
|
.then(response => {
|
||||||
@ -134,13 +183,26 @@ export default {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
|
let stationSrc = ''
|
||||||
|
if (!station.url_resolved) {
|
||||||
|
stationSrc = station.urlresolved
|
||||||
|
} else {
|
||||||
|
stationSrc = station.url_resolved
|
||||||
|
}
|
||||||
const stationMap = {
|
const stationMap = {
|
||||||
id: -1,
|
id: -1,
|
||||||
name: station.name,
|
name: station.name.toString(),
|
||||||
urlresolved: station.url_resolved,
|
urlresolved: stationSrc.toString(),
|
||||||
favicon: station.favicon,
|
favicon: station.favicon.toString(),
|
||||||
stationuuid: station.stationuuid,
|
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
|
await axios
|
||||||
.post(generateUrl('/apps/radio/api/favorites'), stationMap)
|
.post(generateUrl('/apps/radio/api/favorites'), stationMap)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
@ -157,8 +219,10 @@ export default {
|
|||||||
* @param {Object} station Station object
|
* @param {Object} station Station object
|
||||||
*/
|
*/
|
||||||
async doPlay(station) {
|
async doPlay(station) {
|
||||||
|
|
||||||
const vm = this
|
const vm = this
|
||||||
|
|
||||||
|
vm.$store.dispatch('isBuffering', true)
|
||||||
|
|
||||||
if (audioPlayer !== null) {
|
if (audioPlayer !== null) {
|
||||||
audioPlayer.fade(vm.player.volume, 0, 500)
|
audioPlayer.fade(vm.player.volume, 0, 500)
|
||||||
}
|
}
|
||||||
@ -170,14 +234,11 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
stationSrc = station.url_resolved
|
stationSrc = station.url_resolved
|
||||||
}
|
}
|
||||||
|
Howler.unload()
|
||||||
audioPlayer = new Howl({
|
audioPlayer = new Howl({
|
||||||
src: stationSrc,
|
src: stationSrc,
|
||||||
|
html5: true,
|
||||||
volume: vm.player.volume,
|
volume: vm.player.volume,
|
||||||
/* onfade() { // FIXME
|
|
||||||
if (this.volume() === 0) {
|
|
||||||
this.unload()
|
|
||||||
}
|
|
||||||
}, */
|
|
||||||
onplay() {
|
onplay() {
|
||||||
vm.$store.dispatch('isPlaying', true)
|
vm.$store.dispatch('isPlaying', true)
|
||||||
vm.$store.dispatch('isBuffering', false)
|
vm.$store.dispatch('isBuffering', false)
|
||||||
@ -186,13 +247,10 @@ export default {
|
|||||||
vm.$store.dispatch('isPlaying', false)
|
vm.$store.dispatch('isPlaying', false)
|
||||||
vm.$store.dispatch('isBuffering', false)
|
vm.$store.dispatch('isBuffering', false)
|
||||||
},
|
},
|
||||||
onload() {
|
onend() {
|
||||||
vm.$store.dispatch('isPlaying', true)
|
showError(t('radio', 'Lost connection to radio station, retrying ...'))
|
||||||
vm.$store.dispatch('isBuffering', true)
|
|
||||||
},
|
|
||||||
onstop() {
|
|
||||||
vm.$store.dispatch('isPlaying', false)
|
vm.$store.dispatch('isPlaying', false)
|
||||||
vm.$store.dispatch('isBuffering', false)
|
vm.$store.dispatch('isBuffering', true)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
audioPlayer.unload()
|
audioPlayer.unload()
|
||||||
@ -200,17 +258,35 @@ export default {
|
|||||||
audioPlayer.fade(0, vm.player.volume, 500)
|
audioPlayer.fade(0, vm.player.volume, 500)
|
||||||
|
|
||||||
/* Count click */
|
/* Count click */
|
||||||
axios.get(this.$apiUrl + '/json/url/' + station.stationuuid)
|
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 */
|
/* Put into recent stations */
|
||||||
try {
|
try {
|
||||||
|
let stationSrc = ''
|
||||||
|
if (!station.url_resolved) {
|
||||||
|
stationSrc = station.urlresolved
|
||||||
|
} else {
|
||||||
|
stationSrc = station.url_resolved
|
||||||
|
}
|
||||||
const stationMap = {
|
const stationMap = {
|
||||||
id: -1,
|
id: -1,
|
||||||
name: station.name,
|
name: station.name.toString(),
|
||||||
urlresolved: station.url_resolved,
|
urlresolved: stationSrc.toString(),
|
||||||
favicon: station.favicon,
|
favicon: station.favicon.toString(),
|
||||||
stationuuid: station.stationuuid,
|
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
|
await axios
|
||||||
.post(generateUrl('/apps/radio/api/recent'), stationMap)
|
.post(generateUrl('/apps/radio/api/recent'), stationMap)
|
||||||
} catch (error) {
|
} 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') {
|
async loadStations(menuState = 'TOP') {
|
||||||
|
|
||||||
const vm = this
|
const vm = this
|
||||||
@ -231,36 +303,63 @@ export default {
|
|||||||
let sortBy = 'clickcount'
|
let sortBy = 'clickcount'
|
||||||
|
|
||||||
if (vm.$route.name === 'CATEGORIES') {
|
if (vm.$route.name === 'CATEGORIES') {
|
||||||
vm.tableData = [
|
if (vm.$route.path === '/categories') {
|
||||||
{
|
vm.tableData = [
|
||||||
name: 'Countries',
|
{
|
||||||
type: 'folder',
|
name: t('radio', 'Countries'),
|
||||||
path: '#/categories/countries',
|
type: 'folder',
|
||||||
},
|
path: '/categories/countries',
|
||||||
{
|
},
|
||||||
name: 'States',
|
{
|
||||||
type: 'folder',
|
name: t('radio', 'States'),
|
||||||
path: '#/categories/states',
|
type: 'folder',
|
||||||
},
|
path: '/categories/states',
|
||||||
{
|
},
|
||||||
name: 'Languages',
|
{
|
||||||
type: 'folder',
|
name: t('radio', 'Languages'),
|
||||||
path: '#/categories/languages',
|
type: 'folder',
|
||||||
},
|
path: '/categories/languages',
|
||||||
{
|
},
|
||||||
name: 'Tags',
|
{
|
||||||
type: 'folder',
|
name: t('radio', 'Tags'),
|
||||||
path: '#/categories/tags',
|
type: 'folder',
|
||||||
},
|
path: '/categories/tags',
|
||||||
]
|
},
|
||||||
vm.pageLoading = false
|
]
|
||||||
return true
|
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
|
if (vm.tableData.length > 0
|
||||||
&& (vm.$route.name === 'FAVORITES'
|
&& (vm.$route.name === 'FAVORITES'
|
||||||
|| vm.$route.name === 'RECENT')) {
|
|| vm.$route.name === 'RECENT'
|
||||||
|
|| vm.$route.name === 'CATEGORIES')) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,26 +376,46 @@ export default {
|
|||||||
queryURI = generateUrl('/apps/radio/api/recent')
|
queryURI = generateUrl('/apps/radio/api/recent')
|
||||||
}
|
}
|
||||||
|
|
||||||
await axios.get(queryURI, {
|
if (menuState !== 'CATEGORIES') {
|
||||||
params: {
|
vm.queryParams = {
|
||||||
limit: 20,
|
limit: 20,
|
||||||
order: sortBy,
|
order: sortBy,
|
||||||
reverse: true,
|
reverse: true,
|
||||||
offset: vm.tableData.length,
|
offset: vm.tableData.length,
|
||||||
},
|
}
|
||||||
})
|
} else {
|
||||||
.then(function(response) {
|
vm.queryParams = {}
|
||||||
for (let i = 0; i < response.data.length; i++) {
|
}
|
||||||
const obj = response.data[i]
|
|
||||||
let blurHash = vm.blurHashes[obj.stationuuid]
|
try {
|
||||||
if (!blurHash) {
|
if (menuState === 'FAVORITES' || menuState === 'RECENT') {
|
||||||
blurHash = 'L1TSUA?bj[?b~qfQfQj[ayfQfQfQ'
|
axios.defaults.headers.requesttoken = requesttoken
|
||||||
}
|
} else {
|
||||||
response.data[i].blurHash = blurHash
|
delete axios.defaults.headers.requesttoken
|
||||||
}
|
}
|
||||||
vm.tableData = vm.tableData.concat(response.data)
|
await axios.get(queryURI, {
|
||||||
vm.pageLoading = false
|
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,31 +429,39 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
loadSettings() {
|
loadSettings() {
|
||||||
|
|
||||||
axios.defaults.headers.common = {
|
// axios.defaults.headers.common = {
|
||||||
'User-Agent': 'Nextcloud Radio App/1.0', // FIXME: Reference global version number
|
// 'User-Agent': 'Nextcloud Radio App/' + this.$version,
|
||||||
}
|
// }
|
||||||
this.$store.dispatch('getVolumeState')
|
this.$store.dispatch('getVolumeState')
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async loadFavorites() {
|
async loadFavorites() {
|
||||||
const vm = this
|
const vm = this
|
||||||
await axios.get(generateUrl('/apps/radio/api/favorites'))
|
try {
|
||||||
.then(function(response) {
|
axios.defaults.headers.requesttoken = requesttoken
|
||||||
const favorites = []
|
await axios.get(generateUrl('/apps/radio/api/favorites'))
|
||||||
for (let i = 0, len = response.data.length; i < len; i++) {
|
.then(function(response) {
|
||||||
favorites.push([response.data[i].id, response.data[i].stationuuid])
|
const favorites = []
|
||||||
}
|
for (let i = 0, len = response.data.length; i < len; i++) {
|
||||||
vm.favorites = favorites
|
favorites.push([response.data[i].id, response.data[i].stationuuid])
|
||||||
})
|
}
|
||||||
|
vm.favorites = favorites
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
showError(t('radio', 'Unable to load favorites'))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleSidebar(station) {
|
toggleSidebar(station = null) {
|
||||||
this.showSidebar = true
|
if (station) {
|
||||||
this.sidebarStation = station
|
this.showSidebar = true
|
||||||
|
this.sidebarStation = station
|
||||||
|
} else {
|
||||||
|
this.showSidebar = false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -342,27 +469,10 @@ export default {
|
|||||||
|
|
||||||
<style>
|
<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) {
|
@media only screen and (min-width: 1024px) {
|
||||||
.app-navigation-toggle {
|
.app-navigation-toggle {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.breadcrumbs {
|
|
||||||
margin-left: 0px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</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>
|
<template>
|
||||||
<table v-if="stationData" id="table">
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="iconColumn" />
|
<th class="iconColumn" />
|
||||||
@ -10,49 +10,67 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr
|
<template v-if="!isFolder">
|
||||||
v-for="(station, idx) in stationData"
|
<tr
|
||||||
:key="idx"
|
v-for="(station, idx) in stationData"
|
||||||
:class="{ selected: idx === activeItem}">
|
:key="idx"
|
||||||
<td @click="doPlay(idx, station)">
|
:class="{ selected: idx === activeItem}">
|
||||||
<blur-hash-image
|
<td @click="doPlay(idx, station)">
|
||||||
class="stationIcon"
|
<blur-hash-image
|
||||||
width="32"
|
class="stationIcon"
|
||||||
height="32"
|
width="32"
|
||||||
:hash="station.blurHash"
|
height="32"
|
||||||
:src="station.favicon" />
|
:hash="station.blurHash"
|
||||||
<span :class="{ 'icon-starred': favorites.flat().includes(station.stationuuid) }" />
|
:src="station.favicon" />
|
||||||
</td>
|
<span :class="{ 'icon-starred': favorites.flat().includes(station.stationuuid) }" />
|
||||||
<td class="nameColumn" @click="doPlay(idx, station)">
|
</td>
|
||||||
<span class="innernametext">
|
<td class="nameColumn" @click="doPlay(idx, station)">
|
||||||
{{ station.name }}
|
<span class="innernametext">
|
||||||
</span>
|
{{ station.name }}
|
||||||
</td>
|
</span>
|
||||||
<td class="actionColumn">
|
</td>
|
||||||
<Actions>
|
<td class="actionColumn">
|
||||||
<ActionButton
|
<Actions>
|
||||||
v-if="!favorites.flat().includes(station.stationuuid)"
|
<ActionButton
|
||||||
icon="icon-star"
|
v-if="!favorites.flat().includes(station.stationuuid)"
|
||||||
:close-after-click="true"
|
icon="icon-star"
|
||||||
@click="doFavor(idx, station)">
|
:close-after-click="true"
|
||||||
{{ t('radio', 'Add to favorites') }}
|
@click="doFavor(idx, station)">
|
||||||
</ActionButton>
|
{{ t('radio', 'Add to favorites') }}
|
||||||
<ActionButton
|
</ActionButton>
|
||||||
v-if="favorites.flat().includes(station.stationuuid)"
|
<ActionButton
|
||||||
icon="icon-star"
|
v-if="favorites.flat().includes(station.stationuuid)"
|
||||||
:close-after-click="true"
|
icon="icon-star"
|
||||||
@click="doFavor(idx, station)">
|
:close-after-click="true"
|
||||||
{{ t('radio', 'Remove from favorites') }}
|
@click="doFavor(idx, station)">
|
||||||
</ActionButton>
|
{{ t('radio', 'Remove from favorites') }}
|
||||||
<ActionButton
|
</ActionButton>
|
||||||
icon="icon-info"
|
<ActionButton
|
||||||
:close-after-click="true"
|
icon="icon-info"
|
||||||
@click="toggleSidebar(station)">
|
:close-after-click="true"
|
||||||
{{ t('radio', 'Details') }}
|
@click="toggleSidebar(station)">
|
||||||
</ActionButton>
|
{{ t('radio', 'Details') }}
|
||||||
</Actions>
|
</ActionButton>
|
||||||
</td>
|
</Actions>
|
||||||
</tr>
|
</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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</template>
|
</template>
|
||||||
@ -80,6 +98,16 @@ export default {
|
|||||||
data: () => ({
|
data: () => ({
|
||||||
activeItem: null,
|
activeItem: null,
|
||||||
}),
|
}),
|
||||||
|
computed: {
|
||||||
|
isFolder() {
|
||||||
|
if (this.stationData[0]) {
|
||||||
|
if (this.stationData[0].type === 'folder') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
doPlay(idx, station) {
|
doPlay(idx, station) {
|
||||||
this.activeItem = idx
|
this.activeItem = idx
|
||||||
@ -91,6 +119,9 @@ export default {
|
|||||||
toggleSidebar(station) {
|
toggleSidebar(station) {
|
||||||
this.$emit('toggleSidebar', station)
|
this.$emit('toggleSidebar', station)
|
||||||
},
|
},
|
||||||
|
changeRoute(path) {
|
||||||
|
this.$router.push({ path })
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -151,6 +182,13 @@ table {
|
|||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--color-border);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
span.icon-folder {
|
||||||
|
display: block;
|
||||||
|
background-size: cover;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tr {
|
tr {
|
||||||
@ -178,10 +216,15 @@ table {
|
|||||||
padding-right: 0px;
|
padding-right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
td.nameColumn {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding-right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
td.nameColumn .innernametext {
|
td.nameColumn .innernametext {
|
||||||
color: var(--color-main-text);
|
color: var(--color-main-text);
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
user-select: none;
|
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 App from './App'
|
||||||
|
|
||||||
import VueBlurHash from 'vue-blurhash'
|
import VueBlurHash from 'vue-blurhash'
|
||||||
|
import VueClipboard from 'vue-clipboard2'
|
||||||
|
import VueResizeObserver from 'vue-resize-observer'
|
||||||
|
|
||||||
import 'vue-blurhash/dist/vue-blurhash.css'
|
import 'vue-blurhash/dist/vue-blurhash.css'
|
||||||
|
|
||||||
Vue.prototype.t = translate
|
Vue.prototype.t = translate
|
||||||
Vue.prototype.n = translatePlural
|
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.$apiUrl = 'https://de1.api.radio-browser.info'
|
||||||
|
Vue.prototype.$version = '1.0'
|
||||||
|
|
||||||
|
Vue.use(VueClipboard)
|
||||||
Vue.use(VueBlurHash)
|
Vue.use(VueBlurHash)
|
||||||
|
Vue.use(VueResizeObserver)
|
||||||
|
|
||||||
export default new Vue({
|
export default new Vue({
|
||||||
el: '#content',
|
el: '#content',
|
||||||
|
@ -7,6 +7,7 @@ import Main from './components/Main'
|
|||||||
import store from './store.js'
|
import store from './store.js'
|
||||||
|
|
||||||
Vue.use(Router)
|
Vue.use(Router)
|
||||||
|
const requesttoken = axios.defaults.headers.requesttoken
|
||||||
|
|
||||||
const router = new Router({
|
const router = new Router({
|
||||||
base: generateUrl('/apps/radio/'),
|
base: generateUrl('/apps/radio/'),
|
||||||
@ -33,7 +34,7 @@ const router = new Router({
|
|||||||
name: 'FAVORITES',
|
name: 'FAVORITES',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/categories',
|
path: '/categories/:category?/:query?',
|
||||||
component: Main,
|
component: Main,
|
||||||
name: 'CATEGORIES',
|
name: 'CATEGORIES',
|
||||||
},
|
},
|
||||||
@ -52,6 +53,7 @@ router.beforeEach((to, from, next) => {
|
|||||||
store.dispatch('setMenuState', to.name)
|
store.dispatch('setMenuState', to.name)
|
||||||
next()
|
next()
|
||||||
} else {
|
} else {
|
||||||
|
axios.defaults.headers.requesttoken = requesttoken
|
||||||
axios
|
axios
|
||||||
.get(generateUrl('/apps/radio/settings/menuState'))
|
.get(generateUrl('/apps/radio/settings/menuState'))
|
||||||
.then(async response => {
|
.then(async response => {
|
||||||
|
@ -5,6 +5,7 @@ import axios from '@nextcloud/axios'
|
|||||||
import { generateUrl } from '@nextcloud/router'
|
import { generateUrl } from '@nextcloud/router'
|
||||||
|
|
||||||
Vue.use(Vuex)
|
Vue.use(Vuex)
|
||||||
|
const requesttoken = axios.defaults.headers.requesttoken
|
||||||
|
|
||||||
export default new Vuex.Store({
|
export default new Vuex.Store({
|
||||||
state: {
|
state: {
|
||||||
@ -52,11 +53,13 @@ export default new Vuex.Store({
|
|||||||
state.player.title = title
|
state.player.title = title
|
||||||
},
|
},
|
||||||
setMenuState(state, menuState) {
|
setMenuState(state, menuState) {
|
||||||
|
axios.defaults.headers.requesttoken = requesttoken
|
||||||
axios.post(generateUrl('/apps/radio/settings/menuState'), {
|
axios.post(generateUrl('/apps/radio/settings/menuState'), {
|
||||||
menuState,
|
menuState,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getMenuState(state) {
|
getMenuState(state) {
|
||||||
|
axios.defaults.headers.requesttoken = requesttoken
|
||||||
axios
|
axios
|
||||||
.get(generateUrl('/apps/radio/settings/menuState'))
|
.get(generateUrl('/apps/radio/settings/menuState'))
|
||||||
.then(async response => {
|
.then(async response => {
|
||||||
@ -67,11 +70,13 @@ export default new Vuex.Store({
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
setVolumeState(state, volumeState) {
|
setVolumeState(state, volumeState) {
|
||||||
|
axios.defaults.headers.requesttoken = requesttoken
|
||||||
axios.post(generateUrl('/apps/radio/settings/volumeState'), {
|
axios.post(generateUrl('/apps/radio/settings/volumeState'), {
|
||||||
volumeState,
|
volumeState,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getVolumeState(state) {
|
getVolumeState(state) {
|
||||||
|
axios.defaults.headers.requesttoken = requesttoken
|
||||||
axios
|
axios
|
||||||
.get(generateUrl('/apps/radio/settings/volumeState'))
|
.get(generateUrl('/apps/radio/settings/volumeState'))
|
||||||
.then(async response => {
|
.then(async response => {
|
||||||
|
11
webpack.js
11
webpack.js
@ -1,15 +1,16 @@
|
|||||||
const { merge } = require('webpack-merge')
|
const { merge } = require('webpack-merge')
|
||||||
|
const path = require('path')
|
||||||
const webpackConfig = require('@nextcloud/webpack-vue-config')
|
const webpackConfig = require('@nextcloud/webpack-vue-config')
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
|
entry: {
|
||||||
|
dashboard: path.join(__dirname, 'src', 'dashboard.js'),
|
||||||
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.(png|jpg|gif)$/,
|
test: /\.vue$/,
|
||||||
loader: 'file-loader',
|
loader: 'vue-loader',
|
||||||
options: {
|
|
||||||
name: '[name].[ext]?[hash]',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user