mirror of
https://github.com/openvk/openvk
synced 2025-04-23 08:33:02 +03:00
Интерфейсы
This commit is contained in:
parent
8f31091e24
commit
0151b3d020
26 changed files with 1395 additions and 33 deletions
|
@ -14,6 +14,7 @@ class Audio extends Media
|
|||
{
|
||||
protected $tableName = "audios";
|
||||
protected $fileExtension = "mpd";
|
||||
// protected $fileExtension = "mp3";
|
||||
|
||||
# Taken from winamp :D
|
||||
const genres = [
|
||||
|
@ -100,10 +101,10 @@ class Audio extends Media
|
|||
$this->stateChanges("segment_size", $ss);
|
||||
$this->stateChanges("length", $duration);
|
||||
|
||||
try {
|
||||
try {
|
||||
$args = [
|
||||
OPENVK_ROOT,
|
||||
$this->getBaseDir(),
|
||||
str_replace("enabled", "available", OPENVK_ROOT),
|
||||
str_replace("enabled", "available", $this->getBaseDir()),
|
||||
$hash,
|
||||
$filename,
|
||||
|
||||
|
@ -113,21 +114,27 @@ class Audio extends Media
|
|||
$ss,
|
||||
];
|
||||
|
||||
if(Shell::isPowershell())
|
||||
|
||||
if(Shell::isPowershell()) {
|
||||
Shell::powershell("-executionpolicy bypass", "-File", __DIR__ . "/../shell/processAudio.ps1", ...$args)
|
||||
->start();
|
||||
else
|
||||
} else {
|
||||
Shell::bash(__DIR__ . "/../shell/processAudio.sh", ...$args)->start();
|
||||
// Shell::bash(__DIR__ . "/../shell/processAudio.sh", ...$args)->start();
|
||||
// exit("pwsh /opt/chandler/extensions/available/openvk/Web/Models/shell/processAudio.ps1 " . implode(" ", $args) . ' *> /opt/chandler/extensions/available/openvk/storage/log.log');
|
||||
// exit("pwsh /opt/chandler/extensions/available/openvk/Web/Models/shell/processAudio.ps1 " . implode(" ", $args) . ' *> /opt/chandler/extensions/available/openvk/storage/log.log');
|
||||
// Shell::bash("pwsh /opt/chandler/extensions/available/openvk/Web/Models/shell/processAudio.ps1 " . implode(" ", $args) . ' *> /opt/chandler/extensions/available/openvk/storage/log.log');
|
||||
}
|
||||
|
||||
# Wait until processAudio will consume the file
|
||||
$start = time();
|
||||
while(file_exists($filename))
|
||||
if(time() - $start > 5)
|
||||
exit("Timed out waiting for ffmpeg"); // TODO replace with exception
|
||||
// $start = time();
|
||||
// while(file_exists($filename))
|
||||
// if(time() - $start > 5)
|
||||
// exit("Timed out waiting for ffmpeg"); // TODO replace with exception
|
||||
|
||||
} catch(UnknownCommandException $ucex) {
|
||||
exit(OPENVK_ROOT_CONF["openvk"]["debug"] ? "bash/pwsh is not installed" : VIDEOS_FRIENDLY_ERROR);
|
||||
}
|
||||
} catch(UnknownCommandException $ucex) {
|
||||
exit(OPENVK_ROOT_CONF["openvk"]["debug"] ? "bash/pwsh is not installed" : VIDEOS_FRIENDLY_ERROR);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -199,6 +206,13 @@ class Audio extends Media
|
|||
return str_replace(".mpd", "_fragments", $this->getURL()) . "/original_$key.mp3";
|
||||
}
|
||||
|
||||
function getURL(?bool $force = false): string
|
||||
{
|
||||
if ($this->isWithdrawn()) return "";
|
||||
|
||||
return parent::getURL();
|
||||
}
|
||||
|
||||
function getKeys(): array
|
||||
{
|
||||
$keys[bin2hex($this->getRecord()->kid)] = bin2hex($this->getRecord()->key);
|
||||
|
@ -312,7 +326,7 @@ class Audio extends Media
|
|||
]);
|
||||
|
||||
if($entity instanceof User) {
|
||||
$this->stateChanges("listens", $this->getListens() + 1);
|
||||
$this->stateChanges("listens", ($this->getListens() + 1));
|
||||
$this->save();
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ namespace openvk\Web\Models\Entities;
|
|||
use Chandler\Database\DatabaseConnection;
|
||||
use Nette\Database\Table\ActiveRow;
|
||||
use openvk\Web\Models\Repositories\Audios;
|
||||
use openvk\Web\Models\Repositories\Photos;
|
||||
use openvk\Web\Models\RowModel;
|
||||
|
||||
/**
|
||||
|
@ -31,7 +32,8 @@ class Playlist extends MediaCollection
|
|||
|
||||
function getCoverURL(): ?string
|
||||
{
|
||||
return NULL;
|
||||
$photo = (new Photos)->get((int) $this->getRecord()->cover_photo_id);
|
||||
return is_null($photo) ? "/assets/packages/static/openvk/img/song.jpg" : $photo->getURL();
|
||||
}
|
||||
|
||||
function getLength(): int
|
||||
|
@ -158,4 +160,18 @@ class Playlist extends MediaCollection
|
|||
|
||||
parent::delete($softly);
|
||||
}
|
||||
|
||||
function hasAudio(Audio $audio): bool
|
||||
{
|
||||
$ctx = DatabaseConnection::i()->getContext();
|
||||
return !is_null($ctx->table("playlist_relations")->where([
|
||||
"collection" => $this->getId(),
|
||||
"media" => $audio->getId()
|
||||
])->fetch());
|
||||
}
|
||||
|
||||
function getCoverPhotoId(): ?int
|
||||
{
|
||||
return $this->getRecord()->cover_photo_id;
|
||||
}
|
||||
}
|
|
@ -403,6 +403,7 @@ class User extends RowModel
|
|||
"length" => 1,
|
||||
"mappings" => [
|
||||
"photos",
|
||||
"audios",
|
||||
"videos",
|
||||
"messages",
|
||||
"notes",
|
||||
|
|
|
@ -14,6 +14,7 @@ class Audios
|
|||
private $rels;
|
||||
private $playlists;
|
||||
private $playlistImports;
|
||||
private $playlistRels;
|
||||
|
||||
const ORDER_NEW = 0;
|
||||
const ORDER_POPULAR = 1;
|
||||
|
@ -30,6 +31,7 @@ class Audios
|
|||
|
||||
$this->playlists = $this->context->table("playlists");
|
||||
$this->playlistImports = $this->context->table("playlist_imports");
|
||||
$this->playlistRels = $this->context->table("playlist_relations");
|
||||
}
|
||||
|
||||
function get(int $id): ?Audio
|
||||
|
@ -61,6 +63,17 @@ class Audios
|
|||
return new Audio($audio);
|
||||
}
|
||||
|
||||
function getPlaylistByOwnerAndVID(int $owner, int $vId): ?Playlist
|
||||
{
|
||||
$playlist = $this->playlists->where([
|
||||
"owner" => $owner,
|
||||
"id" => $vId,
|
||||
])->fetch();
|
||||
if(!$playlist) return NULL;
|
||||
|
||||
return new Playlist($playlist);
|
||||
}
|
||||
|
||||
function getByEntityID(int $entity, int $offset = 0, ?int $limit = NULL, ?int& $deleted = nullptr): \Traversable
|
||||
{
|
||||
$limit ??= OPENVK_DEFAULT_PER_PAGE;
|
||||
|
@ -158,7 +171,7 @@ class Audios
|
|||
function search(string $query, int $sortMode = 0, bool $performerOnly = false, bool $withLyrics = false): EntityStream
|
||||
{
|
||||
$columns = $performerOnly ? "performer" : "performer, name";
|
||||
$order = (["created", "length", "listens"][$sortMode] ?? "") . "DESC";
|
||||
$order = (["created", "length", "listens"][$sortMode] ?? "") . " DESC";
|
||||
|
||||
$search = $this->audios->where([
|
||||
"unlisted" => 0,
|
||||
|
@ -173,10 +186,44 @@ class Audios
|
|||
|
||||
function searchPlaylists(string $query): EntityStream
|
||||
{
|
||||
$search = $this->audios->where([
|
||||
$search = $this->playlists->where([
|
||||
"deleted" => 0,
|
||||
])->where("MATCH (title, description) AGAINST (? IN BOOLEAN MODE)", $query);
|
||||
])->where("MATCH (`name`, `description`) AGAINST (? IN BOOLEAN MODE)", $query);
|
||||
|
||||
return new EntityStream("Playlist", $search);
|
||||
}
|
||||
}
|
||||
|
||||
function getNew(): EntityStream
|
||||
{
|
||||
return new EntityStream("Audio", $this->audios->where("created >= " . (time() - 259200))->order("created DESC")->limit(25));
|
||||
}
|
||||
|
||||
function getPopular(): EntityStream
|
||||
{
|
||||
return new EntityStream("Audio", $this->audios->where("listens > 0")->order("listens DESC")->limit(25));
|
||||
}
|
||||
|
||||
function isAdded(int $user_id, int $audio_id): bool
|
||||
{
|
||||
return !is_null($this->rels->where([
|
||||
"entity" => $user_id,
|
||||
"audio" => $audio_id
|
||||
])->fetch());
|
||||
}
|
||||
|
||||
function find(string $query, int $page = 1, ?int $perPage = NULL): \Traversable
|
||||
{
|
||||
$query = "%$query%";
|
||||
$result = $this->audios->where("name LIKE ? OR performer LIKE ?", $query, $query);
|
||||
|
||||
return new Util\EntityStream("Audio", $result);
|
||||
}
|
||||
|
||||
function findPlaylists(string $query, int $page = 1, ?int $perPage = NULL): \Traversable
|
||||
{
|
||||
$query = "%$query%";
|
||||
$result = $this->playlists->where("name LIKE ?", $query);
|
||||
|
||||
return new Util\EntityStream("Playlist", $result);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
<?php declare(strict_types=1);
|
||||
namespace openvk\Web\Presenters;
|
||||
use openvk\Web\Models\Entities\{Voucher, Gift, GiftCategory, User, BannedLink};
|
||||
use openvk\Web\Models\Repositories\{ChandlerGroups, ChandlerUsers, Users, Clubs, Vouchers, Gifts, BannedLinks};
|
||||
use openvk\Web\Models\Repositories\{Audios,
|
||||
ChandlerGroups,
|
||||
ChandlerUsers,
|
||||
Users,
|
||||
Clubs,
|
||||
Util\EntityStream,
|
||||
Vouchers,
|
||||
Gifts,
|
||||
BannedLinks};
|
||||
use Chandler\Database\DatabaseConnection;
|
||||
|
||||
final class AdminPresenter extends OpenVKPresenter
|
||||
|
@ -12,8 +20,9 @@ final class AdminPresenter extends OpenVKPresenter
|
|||
private $gifts;
|
||||
private $bannedLinks;
|
||||
private $chandlerGroups;
|
||||
private $audios;
|
||||
|
||||
function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gifts, BannedLinks $bannedLinks, ChandlerGroups $chandlerGroups)
|
||||
function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gifts, BannedLinks $bannedLinks, ChandlerGroups $chandlerGroups, Audios $audios)
|
||||
{
|
||||
$this->users = $users;
|
||||
$this->clubs = $clubs;
|
||||
|
@ -21,7 +30,8 @@ final class AdminPresenter extends OpenVKPresenter
|
|||
$this->gifts = $gifts;
|
||||
$this->bannedLinks = $bannedLinks;
|
||||
$this->chandlerGroups = $chandlerGroups;
|
||||
|
||||
$this->audios = $audios;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
@ -39,6 +49,15 @@ final class AdminPresenter extends OpenVKPresenter
|
|||
$count = $repo->find($query)->size();
|
||||
return $repo->find($query)->page($page, 20);
|
||||
}
|
||||
|
||||
private function searchPlaylists(&$count)
|
||||
{
|
||||
$query = $this->queryParam("q") ?? "";
|
||||
$page = (int) ($this->queryParam("p") ?? 1);
|
||||
|
||||
$count = $this->audios->findPlaylists($query)->size();
|
||||
return $this->audios->findPlaylists($query)->page($page, 20);
|
||||
}
|
||||
|
||||
function onStartup(): void
|
||||
{
|
||||
|
@ -547,4 +566,46 @@ final class AdminPresenter extends OpenVKPresenter
|
|||
|
||||
$this->redirect("/admin/users/id" . $user->getId());
|
||||
}
|
||||
|
||||
function renderMusic(): void
|
||||
{
|
||||
$this->template->mode = in_array($this->queryParam("act"), ["audios", "playlists"]) ? $this->queryParam("act") : "audios";
|
||||
if ($this->template->mode === "audios")
|
||||
$this->template->audios = $this->searchResults($this->audios, $this->template->count);
|
||||
else
|
||||
$this->template->playlists = $this->searchPlaylists($this->template->count);
|
||||
}
|
||||
|
||||
function renderEditMusic(int $audio_id): void
|
||||
{
|
||||
$audio = $this->audios->get($audio_id);
|
||||
$this->template->audio = $audio;
|
||||
|
||||
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||
$audio->setName($this->postParam("name"));
|
||||
$audio->setPerformer($this->postParam("performer"));
|
||||
$audio->setLyrics($this->postParam("text"));
|
||||
$audio->setGenre($this->postParam("genre"));
|
||||
$audio->setOwner((int) $this->postParam("owner"));
|
||||
$audio->setExplicit(!empty($this->postParam("explicit")));
|
||||
$audio->setDeleted(!empty($this->postParam("deleted")));
|
||||
$audio->setWithdrawn(!empty($this->postParam("withdrawn")));
|
||||
$audio->save();
|
||||
}
|
||||
}
|
||||
|
||||
function renderEditPlaylist(int $playlist_id): void
|
||||
{
|
||||
$playlist = $this->audios->getPlaylist($playlist_id);
|
||||
$this->template->playlist = $playlist;
|
||||
|
||||
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||
$playlist->setName($this->postParam("name"));
|
||||
$playlist->setDescription($this->postParam("description"));
|
||||
$playlist->setCover_Photo_Id((int) $this->postParam("photo"));
|
||||
$playlist->setOwner((int) $this->postParam("owner"));
|
||||
$playlist->setDeleted(!empty($this->postParam("deleted")));
|
||||
$playlist->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
<?php declare(strict_types=1);
|
||||
namespace openvk\Web\Presenters;
|
||||
use Chandler\Database\DatabaseConnection;
|
||||
use openvk\Web\Models\Entities\Audio;
|
||||
use openvk\Web\Models\Entities\Club;
|
||||
use openvk\Web\Models\Entities\Photo;
|
||||
use openvk\Web\Models\Entities\Playlist;
|
||||
use openvk\Web\Models\Repositories\Audios;
|
||||
use openvk\Web\Models\Repositories\Clubs;
|
||||
use openvk\Web\Models\Repositories\Users;
|
||||
|
@ -26,26 +30,57 @@ final class AudioPresenter extends OpenVKPresenter
|
|||
|
||||
function renderPopular(): void
|
||||
{
|
||||
$this->renderApp("_popular");
|
||||
$this->renderList(NULL, "popular");
|
||||
}
|
||||
|
||||
function renderNew(): void
|
||||
{
|
||||
$this->renderApp("_new");
|
||||
$this->renderList(NULL, "new");
|
||||
}
|
||||
|
||||
function renderList(int $owner): void
|
||||
function renderList(?int $owner = NULL, ?string $mode = "list"): void
|
||||
{
|
||||
$entity = NULL;
|
||||
if($owner < 0)
|
||||
$entity = (new Clubs)->get($owner);
|
||||
else
|
||||
$entity = (new Users)->get($owner);
|
||||
$this->template->_template = "Audio/List";
|
||||
|
||||
if(!$entity)
|
||||
$this->notFound();
|
||||
$audios = [];
|
||||
$playlists = [];
|
||||
|
||||
$this->renderApp("owner=$owner");
|
||||
if ($mode === "list") {
|
||||
$entity = NULL;
|
||||
if ($owner < 0) {
|
||||
$entity = (new Clubs)->get($owner * -1);
|
||||
if (!$entity || $entity->isBanned())
|
||||
$this->redirect("/audios" . $this->user->id);
|
||||
|
||||
$audios = $this->audios->getByClub($entity);
|
||||
$playlists = $this->audios->getPlaylistsByClub($entity);
|
||||
} else {
|
||||
$entity = (new Users)->get($owner);
|
||||
if (!$entity || $entity->isDeleted() || $entity->isBanned())
|
||||
$this->redirect("/audios" . $this->user->id);
|
||||
|
||||
$audios = $this->audios->getByUser($entity);
|
||||
$playlists = $this->audios->getPlaylistsByUser($entity);
|
||||
}
|
||||
|
||||
if (!$entity)
|
||||
$this->notFound();
|
||||
|
||||
$this->template->owner = $entity;
|
||||
$this->template->isMy = ($owner > 0 && ($entity->getId() === $this->user->id));
|
||||
$this->template->isMyClub = ($owner < 0 && $entity->canBeModifiedBy($this->user->identity));
|
||||
} else if ($mode === "new") {
|
||||
$audios = $this->audios->getNew();
|
||||
} else {
|
||||
$audios = $this->audios->getPopular();
|
||||
}
|
||||
|
||||
// $this->renderApp("owner=$owner");
|
||||
if ($audios !== [])
|
||||
$this->template->audios = iterator_to_array($audios);
|
||||
|
||||
if ($playlists !== [])
|
||||
$this->template->playlists = iterator_to_array($playlists);
|
||||
}
|
||||
|
||||
function renderView(int $owner, int $id): void
|
||||
|
@ -59,7 +94,21 @@ final class AudioPresenter extends OpenVKPresenter
|
|||
if(!$audio->canBeViewedBy($this->user->identity))
|
||||
$this->flashFail("err", tr("forbidden"), tr("forbidden_comment"));
|
||||
|
||||
$this->renderApp("id=" . $audio->getId());
|
||||
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||
switch ($this->queryParam("act")) {
|
||||
case "remove":
|
||||
DatabaseConnection::i()->getContext()->query("DELETE FROM `audio_relations` WHERE `entity` = ? AND `audio` = ?", $this->user->id, $audio->getId());
|
||||
break;
|
||||
|
||||
case "edit":
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->returnJson(["success" => false, "error" => "Action not implemented or not exists"]);
|
||||
}
|
||||
} else {
|
||||
$this->renderApp("id=" . $audio->getId());
|
||||
}
|
||||
}
|
||||
|
||||
function renderEmbed(int $owner, int $id): void
|
||||
|
@ -144,4 +193,205 @@ final class AudioPresenter extends OpenVKPresenter
|
|||
|
||||
$this->redirect(is_null($group) ? "/audios" . $this->user->id : "/audios-" . $group->getId());
|
||||
}
|
||||
|
||||
function renderListen(int $id): void
|
||||
{
|
||||
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||
$this->assertUserLoggedIn();
|
||||
$this->assertNoCSRF();
|
||||
|
||||
$audio = $this->audios->get($id);
|
||||
|
||||
if ($audio && !$audio->isDeleted() && !$audio->isWithdrawn()) {
|
||||
$audio->listen($this->user->identity);
|
||||
}
|
||||
|
||||
$this->returnJson(["response" => true]);
|
||||
}
|
||||
}
|
||||
|
||||
function renderSearch(): void
|
||||
{
|
||||
if ($this->queryParam("q")) {
|
||||
$this->template->q = $this->queryParam("q");
|
||||
$this->template->by_performer = $this->queryParam("by_performer") === "on";
|
||||
$this->template->audios = iterator_to_array($this->audios->search($this->template->q, 1, $this->template->by_performer));
|
||||
$this->template->playlists = iterator_to_array($this->audios->searchPlaylists($this->template->q));
|
||||
}
|
||||
}
|
||||
|
||||
function renderNewPlaylist(): void
|
||||
{
|
||||
$owner = $this->user->id;
|
||||
|
||||
if ($this->requestParam("owner")) {
|
||||
$club = (new Clubs)->get((int) $this->requestParam("owner") * -1);
|
||||
if (!$club || $club->isBanned() || !$club->canBeModifiedBy($this->user->identity))
|
||||
$this->redirect("/audios" . $this->user->id);
|
||||
|
||||
$owner = ($club->getId() * -1);
|
||||
}
|
||||
|
||||
$this->template->owner = $owner;
|
||||
|
||||
// exit(var_dump($owner));
|
||||
|
||||
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||
$title = $this->postParam("title");
|
||||
$description = $this->postParam("description");
|
||||
$audios = !empty($this->postParam("audios")) ? explode(",", $this->postParam("audios")) : [];
|
||||
|
||||
if (!$title)
|
||||
$this->returnJson(["success" => false, "error" => "Название не указано"]);
|
||||
|
||||
$playlist = new Playlist;
|
||||
$playlist->setOwner($owner);
|
||||
$playlist->setName(substr($title, 0, 128));
|
||||
$playlist->setDescription(substr($description, 0, 2048));
|
||||
$playlist->save();
|
||||
|
||||
foreach ($audios as $audio) {
|
||||
DatabaseConnection::i()->getContext()->query("INSERT INTO `playlist_relations` (`collection`, `media`) VALUES (?, ?)", $playlist->getId(), $audio);
|
||||
}
|
||||
|
||||
DatabaseConnection::i()->getContext()->query("INSERT INTO `playlist_imports` (`entity`, `playlist`) VALUES (?, ?)", $owner, $playlist->getId());
|
||||
|
||||
$this->returnJson(["success" => true, "payload" => "/playlist" . $owner . "_" . $playlist->getId()]);
|
||||
} else {
|
||||
$this->template->audios = iterator_to_array($this->audios->getByUser($this->user->identity));
|
||||
}
|
||||
}
|
||||
|
||||
function renderPlaylist(int $owner_id, int $virtual_id): void
|
||||
{
|
||||
$playlist = $this->audios->getPlaylistByOwnerAndVID($owner_id, $virtual_id);
|
||||
|
||||
if (!$playlist || $playlist->isDeleted())
|
||||
$this->notFound();
|
||||
|
||||
$this->template->playlist = $playlist;
|
||||
$this->template->audios = iterator_to_array($playlist->getAudios());
|
||||
$this->template->isMy = $playlist->getOwner()->getId() === $this->user->id;
|
||||
$this->template->canEdit = ($this->template->isMy || ($playlist->getOwner() instanceof Club && $playlist->getOwner()->canBeModifiedBy($this->user->identity)));
|
||||
$this->template->edit = $this->queryParam("act") === "edit";
|
||||
|
||||
if ($this->template->edit) {
|
||||
if (!$this->template->canEdit) {
|
||||
$this->flashFail("err", tr("error"), tr("forbidden"));
|
||||
}
|
||||
|
||||
$_ids = [];
|
||||
$audios = iterator_to_array($playlist->getAudios());
|
||||
foreach ($audios as $audio) {
|
||||
$_ids[] = $audio->getId();
|
||||
}
|
||||
|
||||
foreach ($this->audios->getByUser($this->user->identity) as $audio) {
|
||||
if (!in_array($audio->getId(), $_ids)) {
|
||||
$audios[] = $audio;
|
||||
}
|
||||
}
|
||||
|
||||
$this->template->audios = $audios;
|
||||
} else {
|
||||
$this->template->audios = iterator_to_array($playlist->getAudios());
|
||||
}
|
||||
|
||||
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||
if (!$this->template->canEdit) {
|
||||
$this->flashFail("err", tr("error"), tr("forbidden"));
|
||||
}
|
||||
|
||||
$title = $this->postParam("title");
|
||||
$description = $this->postParam("description");
|
||||
$audios = !empty($this->postParam("audios")) ? explode(",", $this->postParam("audios")) : [];
|
||||
|
||||
$playlist->setName(substr($title, 0, 128));
|
||||
$playlist->setDescription(substr($description, 0, 2048));
|
||||
$playlist->setEdited(time());
|
||||
|
||||
if ($_FILES["cover"]["error"] === UPLOAD_ERR_OK) {
|
||||
$photo = new Photo;
|
||||
$photo->setOwner($this->user->id);
|
||||
$photo->setDescription("Playlist #" . $playlist->getId() . " cover image");
|
||||
$photo->setFile($_FILES["cover"]);
|
||||
$photo->setCreated(time());
|
||||
$photo->save();
|
||||
|
||||
$playlist->setCover_Photo_Id($photo->getId());
|
||||
}
|
||||
|
||||
$playlist->save();
|
||||
|
||||
$_ids = [];
|
||||
|
||||
foreach ($playlist->getAudios() as $audio) {
|
||||
$_ids[] = $audio->getId();
|
||||
}
|
||||
|
||||
foreach ($playlist->getAudios() as $audio) {
|
||||
if (!in_array($audio->getId(), $audios)) {
|
||||
DatabaseConnection::i()->getContext()->query("DELETE FROM `playlist_relations` WHERE `collection` = ? AND `media` = ?", $playlist->getId(), $audio->getId());
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($audios as $audio) {
|
||||
if (!in_array($audio, $_ids)) {
|
||||
DatabaseConnection::i()->getContext()->query("INSERT INTO `playlist_relations` (`collection`, `media`) VALUES (?, ?)", $playlist->getId(), $audio);
|
||||
}
|
||||
}
|
||||
|
||||
$this->flash("succ", tr("changes_saved"));
|
||||
$this->redirect("/playlist" . $playlist->getOwner()->getId() . "_" . $playlist->getId());
|
||||
}
|
||||
}
|
||||
|
||||
function renderAction(int $audio_id): void
|
||||
{
|
||||
switch ($this->queryParam("act")) {
|
||||
case "add":
|
||||
if (!$this->audios->isAdded($this->user->id, $audio_id)) {
|
||||
DatabaseConnection::i()->getContext()->query("INSERT INTO `audio_relations` (`entity`, `audio`) VALUES (?, ?)", $this->user->id, $audio_id);
|
||||
} else {
|
||||
$this->returnJson(["success" => false, "error" => "Аудиозапись уже добавлена"]);
|
||||
}
|
||||
break;
|
||||
|
||||
case "remove":
|
||||
if ($this->audios->isAdded($this->user->id, $audio_id)) {
|
||||
DatabaseConnection::i()->getContext()->query("DELETE FROM `audio_relations` WHERE `entity` = ? AND `audio` = ?", $this->user->id, $audio_id);
|
||||
} else {
|
||||
$this->returnJson(["success" => false, "error" => "Аудиозапись не добавлена"]);
|
||||
}
|
||||
break;
|
||||
|
||||
case "edit":
|
||||
$audio = $this->audios->get($audio_id);
|
||||
if (!$audio || $audio->isDeleted() || $audio->isWithdrawn() || $audio->isUnlisted())
|
||||
$this->returnJson(["success" => false, "error" => "Аудиозапись не найдена"]);
|
||||
|
||||
if ($audio->getOwner()->getId() !== $this->user->id)
|
||||
$this->returnJson(["success" => false, "error" => "Ошибка доступа"]);
|
||||
|
||||
$performer = $this->postParam("performer");
|
||||
$name = $this->postParam("name");
|
||||
$lyrics = $this->postParam("lyrics");
|
||||
$genre = empty($this->postParam("genre")) ? "undefined" : $this->postParam("genre");
|
||||
$nsfw = ($this->postParam("nsfw") ?? "off") === "on";
|
||||
if(empty($performer) || empty($name) || iconv_strlen($performer . $name) > 128) # FQN of audio must not be more than 128 chars
|
||||
$this->flashFail("err", tr("error"), tr("error_insufficient_info"));
|
||||
|
||||
$audio->setName($name);
|
||||
$audio->setPerformer($performer);
|
||||
$audio->setLyrics(empty($lyrics) ? NULL : $lyrics);
|
||||
$audio->setGenre($genre);
|
||||
$audio->save();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
$this->returnJson(["success" => true]);
|
||||
}
|
||||
}
|
|
@ -455,6 +455,7 @@ final class UserPresenter extends OpenVKPresenter
|
|||
} else if($_GET['act'] === "lMenu") {
|
||||
$settings = [
|
||||
"menu_bildoj" => "photos",
|
||||
"menu_muziko" => "audios",
|
||||
"menu_filmetoj" => "videos",
|
||||
"menu_mesagoj" => "messages",
|
||||
"menu_notatoj" => "notes",
|
||||
|
|
|
@ -122,6 +122,7 @@
|
|||
</object>
|
||||
</a>
|
||||
<a n:if="$thisUser->getLeftMenuItemStatus('photos')" href="/albums{$thisUser->getId()}" class="link">{_my_photos}</a>
|
||||
<a n:if="$thisUser->getLeftMenuItemStatus('audios')" href="/audios{$thisUser->getId()}" class="link">Мои Аудиозаписи</a>
|
||||
<a n:if="$thisUser->getLeftMenuItemStatus('videos')" href="/videos{$thisUser->getId()}" class="link">{_my_videos}</a>
|
||||
<a n:if="$thisUser->getLeftMenuItemStatus('messages')" href="/im" class="link">{_my_messages}
|
||||
<object type="internal/link" n:if="$thisUser->getUnreadMessagesCount() > 0">
|
||||
|
|
|
@ -59,6 +59,9 @@
|
|||
<li>
|
||||
<a href="/admin/bannedLinks">{_admin_banned_links}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/admin/music">Музыка</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="aui-nav-heading">
|
||||
<strong>Chandler</strong>
|
||||
|
|
65
Web/Presenters/templates/Admin/EditMusic.xml
Normal file
65
Web/Presenters/templates/Admin/EditMusic.xml
Normal file
|
@ -0,0 +1,65 @@
|
|||
{extends "@layout.xml"}
|
||||
|
||||
{block title}
|
||||
{_edit} {$audio->getName()}
|
||||
{/block}
|
||||
|
||||
{block heading}
|
||||
{$audio->getName()}
|
||||
{/block}
|
||||
|
||||
{block content}
|
||||
<div class="aui-tabs horizontal-tabs">
|
||||
<form class="aui" method="POST">
|
||||
<div class="field-group">
|
||||
<label for="id">ID</label>
|
||||
<input class="text medium-field" type="number" id="id" disabled value="{$audio->getId()}" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label for="name">Название</label>
|
||||
<input class="text medium-field" type="text" id="name" name="name" value="{$audio->getTitle()}" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label for="performer">Исполнитель</label>
|
||||
<input class="text medium-field" type="text" id="performer" name="performer" value="{$audio->getPerformer()}" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label for="ext">Текст</label>
|
||||
<textarea class="text medium-field" type="text" id="text" name="text" style="resize: vertical;">{$audio->getLyrics()}</textarea>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label for="ext">Жанр</label>
|
||||
<select class="select medium-field" name="genre">
|
||||
<option n:foreach='\openvk\Web\Models\Entities\Audio::genres as $genre'
|
||||
n:attr="selected: $genre == $audio->getGenre()" value="{$genre}">
|
||||
{$genre}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="field-group">
|
||||
<label for="owner">Владелец</label>
|
||||
<input class="text medium-field" type="number" id="owner_id" name="owner" value="{$audio->getOwner()->getId()}" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label for="explicit">Explicit</label>
|
||||
<input class="toggle-large" type="checkbox" id="explicit" name="explicit" value="1" {if $audio->isExplicit()} checked {/if} />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label for="deleted">Удалено</label>
|
||||
<input class="toggle-large" type="checkbox" id="deleted" name="deleted" value="1" {if $audio->isDeleted()} checked {/if} />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label for="withdrawn">Изъято</label>
|
||||
<input class="toggle-large" type="checkbox" id="withdrawn" name="withdrawn" value="1" {if $audio->isWithdrawn()} checked {/if} />
|
||||
</div>
|
||||
<hr />
|
||||
<div class="buttons-container">
|
||||
<div class="buttons">
|
||||
<input type="hidden" name="hash" value="{$csrfToken}" />
|
||||
<input class="button submit" type="submit" value="{_save}">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/block}
|
54
Web/Presenters/templates/Admin/EditPlaylist.xml
Normal file
54
Web/Presenters/templates/Admin/EditPlaylist.xml
Normal file
|
@ -0,0 +1,54 @@
|
|||
{extends "@layout.xml"}
|
||||
|
||||
{block title}
|
||||
{_edit} {$playlist->getName()}
|
||||
{/block}
|
||||
|
||||
{block heading}
|
||||
{$playlist->getName()}
|
||||
{/block}
|
||||
|
||||
{block content}
|
||||
<div class="aui-tabs horizontal-tabs">
|
||||
<form class="aui" method="POST">
|
||||
<div class="field-group">
|
||||
<label for="id">ID</label>
|
||||
<input class="text medium-field" type="number" id="id" disabled value="{$playlist->getId()}" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label for="name">Название</label>
|
||||
<input class="text medium-field" type="text" id="name" name="name" value="{$playlist->getName()}" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label for="ext">Описание</label>
|
||||
<textarea class="text medium-field" type="text" id="description" name="description" style="resize: vertical;">{$playlist->getDescription()}</textarea>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label for="ext">Обложка (ID фото)</label>
|
||||
<span id="avatar" class="aui-avatar aui-avatar-project aui-avatar-xlarge">
|
||||
<span class="aui-avatar-inner">
|
||||
<img src="{$playlist->getCoverUrl()}" style="object-fit: cover;"></img>
|
||||
</span>
|
||||
</span>
|
||||
<br />
|
||||
<input class="text medium-field" type="number" id="photo" name="photo" value="{$playlist->getCoverPhotoId()}" />
|
||||
</div>
|
||||
<hr />
|
||||
<div class="field-group">
|
||||
<label for="owner">Владелец</label>
|
||||
<input class="text medium-field" type="number" id="owner_id" name="owner" value="{$playlist->getOwner()->getId()}" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label for="deleted">Удален</label>
|
||||
<input class="toggle-large" type="checkbox" id="deleted" name="deleted" value="1" {if $playlist->isDeleted()} checked {/if} />
|
||||
</div>
|
||||
<hr />
|
||||
<div class="buttons-container">
|
||||
<div class="buttons">
|
||||
<input type="hidden" name="hash" value="{$csrfToken}" />
|
||||
<input class="button submit" type="submit" value="{_save}">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/block}
|
135
Web/Presenters/templates/Admin/Music.xml
Normal file
135
Web/Presenters/templates/Admin/Music.xml
Normal file
|
@ -0,0 +1,135 @@
|
|||
{extends "@layout.xml"}
|
||||
{var $search = $mode === "audios"}
|
||||
|
||||
{block title}
|
||||
Поиск музыки
|
||||
{/block}
|
||||
|
||||
{block heading}
|
||||
Музыка
|
||||
{/block}
|
||||
|
||||
{block searchTitle}
|
||||
{include title}
|
||||
{/block}
|
||||
|
||||
{block content}
|
||||
<nav class="aui-navgroup aui-navgroup-horizontal">
|
||||
<div class="aui-navgroup-inner">
|
||||
<div class="aui-navgroup-primary">
|
||||
<ul class="aui-nav" resolved="">
|
||||
<li n:attr="class => $mode === 'audios' ? 'aui-nav-selected' : ''">
|
||||
<a href="?act=audios">Аудиозаписи</a>
|
||||
</li>
|
||||
<li n:attr="class => $mode === 'playlists' ? 'aui-nav-selected' : ''">
|
||||
<a href="?act=playlists">Плейлисты</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<table class="aui aui-table-list">
|
||||
{if $mode === "audios"}
|
||||
{var $audios = iterator_to_array($audios)}
|
||||
{var $amount = sizeof($audios)}
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>{_admin_author}</th>
|
||||
<th>Исполнитель</th>
|
||||
<th>{_admin_title}</th>
|
||||
<th>Жанр</th>
|
||||
<th>Explicit</th>
|
||||
<th>Изъято</th>
|
||||
<th>Удалено</th>
|
||||
<th>Создан</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr n:foreach="$audios as $audio">
|
||||
<td>{$audio->getId()}</td>
|
||||
<td>
|
||||
{var $owner = $audio->getOwner()}
|
||||
<span class="aui-avatar aui-avatar-xsmall">
|
||||
<span class="aui-avatar-inner">
|
||||
<img src="{$owner->getAvatarUrl('miniscule')}" alt="{$owner->getCanonicalName()}" style="object-fit: cover;" role="presentation" />
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a>
|
||||
|
||||
<span n:if="$owner->isBanned()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed">{_admin_banned}</span>
|
||||
</td>
|
||||
<td>{$audio->getPerformer()}</td>
|
||||
<td>{$audio->getTitle()}</td>
|
||||
<td>{$audio->getGenre()}</td>
|
||||
<td>{$audio->isExplicit() ? tr("yes") : tr("no")}</td>
|
||||
<td n:attr="style => $audio->isWithdrawn() ? 'color: red;' : ''">
|
||||
{$audio->isWithdrawn() ? tr("yes") : tr("no")}
|
||||
</td>
|
||||
<td n:attr="style => $audio->isDeleted() ? 'color: red;' : ''">
|
||||
{$audio->isDeleted() ? tr("yes") : tr("no")}
|
||||
</td>
|
||||
<td>{$audio->getPublicationTime()}</td>
|
||||
<td>
|
||||
<a class="aui-button aui-button-primary" href="/admin/music/{$audio->getId()}/edit">
|
||||
<span class="aui-icon aui-icon-small aui-iconfont-new-edit">Редактировать</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
{else}
|
||||
{var $playlists = iterator_to_array($playlists)}
|
||||
{var $amount = sizeof($playlists)}
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>{_admin_author}</th>
|
||||
<th>Название</th>
|
||||
<th>Создан</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr n:foreach="$playlists as $playlist">
|
||||
<td>{$playlist->getId()}</td>
|
||||
<td>
|
||||
{var $owner = $playlist->getOwner()}
|
||||
<span class="aui-avatar aui-avatar-xsmall">
|
||||
<span class="aui-avatar-inner">
|
||||
<img src="{$owner->getAvatarUrl('miniscule')}" alt="{$owner->getCanonicalName()}" style="object-fit: cover;" role="presentation" />
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a>
|
||||
|
||||
<span n:if="$owner->isBanned()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed">{_admin_banned}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="aui-avatar aui-avatar-xsmall">
|
||||
<span class="aui-avatar-inner">
|
||||
<img src="{$playlist->getCoverURL()}" alt="{$owner->getCanonicalName()}" style="object-fit: cover;" role="presentation" />
|
||||
</span>
|
||||
</span>
|
||||
{$playlist->getName()}
|
||||
</td>
|
||||
<td>{$playlist->getCreationTime()}</td>
|
||||
<td>
|
||||
<a class="aui-button aui-button-primary" href="/admin/playlist/{$playlist->getId()}/edit">
|
||||
<span class="aui-icon aui-icon-small aui-iconfont-new-edit">Редактировать</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
{/if}
|
||||
</table>
|
||||
<br/>
|
||||
<div align="right">
|
||||
{var $isLast = ((10 * (($_GET['p'] ?? 1) - 1)) + $amount) < $count}
|
||||
|
||||
<a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="?p={($_GET['p'] ?? 1) - 1}">«</a>
|
||||
<a n:if="$isLast" class="aui-button" href="?p={($_GET['p'] ?? 1) + 1}">»</a>
|
||||
</div>
|
||||
{/block}
|
86
Web/Presenters/templates/Audio/List.xml
Normal file
86
Web/Presenters/templates/Audio/List.xml
Normal file
|
@ -0,0 +1,86 @@
|
|||
{extends "../@layout.xml"}
|
||||
|
||||
{block title}Аудиозаписи{/block}
|
||||
|
||||
{block header}
|
||||
<div>
|
||||
<div n:if="$isMy">Мои аудиозаписи</div>
|
||||
<div n:if="!$isMy">
|
||||
<a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a>
|
||||
»
|
||||
Аудиозаписи
|
||||
</div>
|
||||
</div>
|
||||
{/block}
|
||||
|
||||
{block content}
|
||||
{include "tabs.xml", mode => "list", listText => ($isMy ? "Моя музыка" : $owner->getCanonicalName())}
|
||||
|
||||
<style>
|
||||
.playlist-name {
|
||||
max-width: 100px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, system-ui, "Helvetica Neue", Roboto, sans-serif;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.playlist-name:hover { text-decoration: underline; }
|
||||
</style>
|
||||
<div n:if="count($playlists) > 0 || count($audios) > 0 || ($isMy || $isMyClub)">
|
||||
<h4 style="padding: 8px; display: flex; justify-content: space-between;">
|
||||
<div>Плейлисты</div>
|
||||
<div n:if="$isMy || $isMyClub">
|
||||
<div class="icon add-icon" onClick="window.location.href = '/audios/newPlaylist'" />
|
||||
</div>
|
||||
</h4>
|
||||
<div style="padding: 8px;">
|
||||
<div n:if="count($playlists) <= 0">
|
||||
{include "../components/nothing.xml"}
|
||||
</div>
|
||||
<div n:if="count($playlists) > 0" style="display: flex; gap: 8px; overflow-x: auto;">
|
||||
<div n:foreach="$playlists as $playlist">
|
||||
<div style="cursor: pointer;" onClick="window.location.href = '/playlist{$playlist->getOwner()->getId()}_{$playlist->getId()}'">
|
||||
<div><img src="{$playlist->getCoverURL()}" width="100" height="100" style="border-radius: 8px;" /></div>
|
||||
<div class="playlist-name">{$playlist->getName()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 style="padding: 8px; display: flex; justify-content: space-between;">
|
||||
<div>Музыка</div>
|
||||
<div n:if="$isMy || $isMyClub">
|
||||
<div class="icon add-icon" onClick="window.location.href = '/player/upload'" />
|
||||
</div>
|
||||
</h4>
|
||||
<div style="padding: 8px;">
|
||||
<div n:if="count($audios) <= 0">
|
||||
{include "../components/nothing.xml"}
|
||||
</div>
|
||||
<div n:if="count($audios) > 0">
|
||||
{foreach $audios as $audio}
|
||||
{include "player.xml",
|
||||
audio => $audio,
|
||||
canAdd => !$isMy && !isMyClub,
|
||||
canRemove => $isMy || !isMyClub,
|
||||
canEdit => $audio->getOwner()->getId() === $thisUser->getId() || !isMyClub,
|
||||
deleteOnClick => "removeAudio({$audio->getId()})",
|
||||
editOnClick => "editAudio({$audio->getId()}, `{$audio->getTitle()}`, `{$audio->getPerformer()}`, `{$audio->getGenre()}`, `{$audio->getLyrics()}`)"
|
||||
}
|
||||
{/foreach}
|
||||
</div>
|
||||
|
||||
<div n:if="count($audios) > 0">
|
||||
{script "js/node_modules/umbrellajs/umbrella.min.js"}
|
||||
{script "js/node_modules/dashjs/dist/dash.all.min.js"}
|
||||
|
||||
{include "player.js.xml", audios => $audios}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div n:if="count($playlists) === 0 && count($audios) === 0 && !($isMy || $isMyClub)" style="padding: 8px;">
|
||||
{include "../components/nothing.xml"}
|
||||
</div>
|
||||
{/block}
|
29
Web/Presenters/templates/Audio/New.xml
Normal file
29
Web/Presenters/templates/Audio/New.xml
Normal file
|
@ -0,0 +1,29 @@
|
|||
{extends "../@layout.xml"}
|
||||
|
||||
{block title}Аудиозаписи{/block}
|
||||
|
||||
{block header}
|
||||
Новое
|
||||
{/block}
|
||||
|
||||
{block content}
|
||||
{include "tabs.xml", mode => "new"}
|
||||
|
||||
<div style="padding: 8px;">
|
||||
<div n:if="count($audios) <= 0">
|
||||
{include "../components/nothing.xml"}
|
||||
</div>
|
||||
<div n:if="count($audios) > 0">
|
||||
{foreach $audios as $audio}
|
||||
{include "player.xml", audio => $audio, addOnClick => "addAudio({$audio->getId()})"}
|
||||
{/foreach}
|
||||
</div>
|
||||
|
||||
<div n:if="count($audios) > 0">
|
||||
{script "js/node_modules/umbrellajs/umbrella.min.js"}
|
||||
{script "js/node_modules/dashjs/dist/dash.all.min.js"}
|
||||
|
||||
{include "player.js.xml", audios => $audios}
|
||||
</div>
|
||||
</div>
|
||||
{/block}
|
81
Web/Presenters/templates/Audio/NewPlaylist.xml
Normal file
81
Web/Presenters/templates/Audio/NewPlaylist.xml
Normal file
|
@ -0,0 +1,81 @@
|
|||
{extends "../@layout.xml"}
|
||||
|
||||
{block title}
|
||||
Создать плейлист
|
||||
{/block}
|
||||
|
||||
{block header}
|
||||
{include title}
|
||||
{/block}
|
||||
|
||||
{block content}
|
||||
{include "tabs.xml", mode => "list"}
|
||||
<br />
|
||||
|
||||
<style>
|
||||
textarea[name='description'] {
|
||||
padding: 4px;
|
||||
resize: vertical;
|
||||
min-height: 150px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<form method="post" id="newPlaylistForm">
|
||||
<input type="text" name="title" placeholder="Название" maxlength="128" />
|
||||
<br /><br />
|
||||
<textarea placeholder="Описание" name="description" maxlength="2048" />
|
||||
<br /><br />
|
||||
|
||||
<div n:if="count($audios) > 0">
|
||||
<div id="newPlaylistAudios" n:foreach="$audios as $audio">
|
||||
{include "player.xml", audio => $audio, canAdd => true, addOnClick => "addToPlaylist({$audio->getId()})"}
|
||||
<br/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="button" style="float: right;">{_create}</button>
|
||||
</form>
|
||||
|
||||
<script n:if="count($audios) > 0">
|
||||
function addToPlaylist(id) {
|
||||
$(`#audioEmbed-${ id} .buttons`).html(`<div class="icon delete-icon" onClick="removeFromPlaylist(${ id})" />`);
|
||||
}
|
||||
|
||||
function removeFromPlaylist(id) {
|
||||
$(`#audioEmbed-${ id} .buttons`).html(`<div class="icon add-icon" onClick="addToPlaylist(${ id})" />`);
|
||||
}
|
||||
|
||||
function create() {
|
||||
let ids = [];
|
||||
$("#newPlaylistAudios .delete-icon").each(function () {
|
||||
ids.push($(this).parents("#miniplayer").first().parent().attr("id").replace("audioEmbed-", ""));
|
||||
});
|
||||
|
||||
$.ajax(`/audios/newPlaylist`, {
|
||||
type: "POST",
|
||||
data: {
|
||||
title: $("input[name='title']").val(),
|
||||
description: $("textarea[name='description']").val(),
|
||||
audios: ids.join(","),
|
||||
owner: {$owner},
|
||||
hash: {$csrfToken}
|
||||
},
|
||||
success: (response) => {
|
||||
if (response.success) {
|
||||
window.location.href = response.payload;
|
||||
} else {
|
||||
NewNotification("Ошибка", (response?.error ?? "Неизвестная ошибка"), "/assets/packages/static/openvk/img/error.png");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
console.log(ids.join(","));
|
||||
}
|
||||
|
||||
$("#newPlaylistForm").submit(function (event) {
|
||||
event.preventDefault();
|
||||
create();
|
||||
});
|
||||
</script>
|
||||
{/block}
|
110
Web/Presenters/templates/Audio/Playlist.xml
Normal file
110
Web/Presenters/templates/Audio/Playlist.xml
Normal file
|
@ -0,0 +1,110 @@
|
|||
{extends "../@layout.xml"}
|
||||
|
||||
{block title}Плейлист{/block}
|
||||
|
||||
{block header}
|
||||
{include title}
|
||||
{/block}
|
||||
|
||||
{block content}
|
||||
{include "tabs.xml"}
|
||||
<style>
|
||||
.playlist-descr > * { padding: 8px 0; }
|
||||
.playlist-dates {
|
||||
margin-top: -8px;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
.playlist-dates > * { color: #818C99; }
|
||||
.dvd { padding: .5px 2px }
|
||||
textarea[name='description'] {
|
||||
padding: 4px;
|
||||
resize: vertical;
|
||||
min-height: 150px;
|
||||
}
|
||||
</style>
|
||||
<br />
|
||||
<div style="display: flex; gap: 16px;">
|
||||
{if $edit}<form id="editPlaylistForm" method="POST" style="display: flex; gap: 16px; width: 100%;" enctype="multipart/form-data">{/if}
|
||||
<div style="width: 120px;">
|
||||
<img src="{$playlist->getCoverURL()}" width="110" height="110" style="border-radius: 8px;" />
|
||||
{if $edit}
|
||||
<br /><br />
|
||||
<label for="file-upload" class="button">
|
||||
Обновить обложку
|
||||
</label>
|
||||
<input style="display: none;" type="file" name="cover" accept="image/*" id="file-upload" />
|
||||
{/if}
|
||||
</div>
|
||||
<div style="width: -webkit-fill-available;">
|
||||
<div class="playlist-descr">
|
||||
{if !$edit}
|
||||
<h4 style="display: flex; justify-content: space-between">
|
||||
<div style="max-width: 90%;">{$playlist->getName()}</div>
|
||||
<a n:if="$canEdit" class="icon edit-icon" href="/playlist{$playlist->getOwner()->getId()}_{$playlist->getId()}?act=edit" />
|
||||
</h4>
|
||||
<div n:if="$playlist->getDescription()">{$playlist->getDescription()}</div>
|
||||
{else}
|
||||
<input type="text" name="title" placeholder="Название" value="{$playlist->getName()}" maxlength="128" />
|
||||
<br /><br />
|
||||
<textarea name="description" placeholder="Описание" maxlength="2048">{$playlist->getDescription()}</textarea>
|
||||
{/if}
|
||||
<div class="playlist-dates" n:attr="style => $playlist->getDescription() ? '' : 'margin-top: 0;'">
|
||||
<span>создан {$playlist->getCreationTime()}</span>
|
||||
{if $playlist->getEditTime()}
|
||||
<span class="dvd">·</span>
|
||||
<span>обновлен {$playlist->getEditTime()}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<h4 />
|
||||
<div style="margin-top: 8px; margin-left: -8px;">
|
||||
<div n:if="count($audios) <= 0">
|
||||
{include "../components/nothing.xml"}
|
||||
</div>
|
||||
<div id="playlistAudios" n:if="count($audios) > 0" n:foreach="$audios as $audio">
|
||||
{include "player.xml",
|
||||
audio => $audio,
|
||||
canAdd => $edit && !$playlist->hasAudio($audio),
|
||||
canRemove => $edit && $playlist->hasAudio($audio),
|
||||
addOnClick => "addToPlaylist({$audio->getId()})",
|
||||
deleteOnClick => "removeFromPlaylist({$audio->getId()})"
|
||||
}
|
||||
</div>
|
||||
|
||||
<div n:if="count($audios) > 0">
|
||||
{script "js/node_modules/umbrellajs/umbrella.min.js"}
|
||||
{script "js/node_modules/dashjs/dist/dash.all.min.js"}
|
||||
|
||||
{include "player.js.xml", audios => $audios}
|
||||
</div>
|
||||
</div>
|
||||
{if $edit}
|
||||
<h4 />
|
||||
<br />
|
||||
<input type="hidden" name="hash" value="{$csrfToken}" />
|
||||
<input type="hidden" name="audios" value="" />
|
||||
<button class="button" style="float: right;">{_save}</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{if $edit}</form>{/if}
|
||||
<script n:if="count($audios) > 0 && $edit">
|
||||
function addToPlaylist(id) {
|
||||
$(`#audioEmbed-${ id} .buttons`).html(`<div class="icon delete-icon" onClick="removeFromPlaylist(${ id})" />`);
|
||||
}
|
||||
|
||||
function removeFromPlaylist(id) {
|
||||
$(`#audioEmbed-${ id} .buttons`).html(`<div class="icon add-icon" onClick="addToPlaylist(${ id})" />`);
|
||||
}
|
||||
|
||||
$("#editPlaylistForm").submit(() => {
|
||||
let ids = [];
|
||||
$("#playlistAudios .delete-icon").each(function () {
|
||||
ids.push($(this).parents("#miniplayer").first().parent().attr("id").replace("audioEmbed-", ""));
|
||||
});
|
||||
$("input[name='audios']").val(ids.join(","));
|
||||
$("#editPlaylistForm").submit();
|
||||
})
|
||||
</script>
|
||||
{/block}
|
29
Web/Presenters/templates/Audio/Popular.xml
Normal file
29
Web/Presenters/templates/Audio/Popular.xml
Normal file
|
@ -0,0 +1,29 @@
|
|||
{extends "../@layout.xml"}
|
||||
|
||||
{block title}Аудиозаписи{/block}
|
||||
|
||||
{block header}
|
||||
Популярное
|
||||
{/block}
|
||||
|
||||
{block content}
|
||||
{include "tabs.xml", mode => "popular"}
|
||||
|
||||
<div style="padding: 8px;">
|
||||
<div n:if="count($audios) <= 0">
|
||||
{include "../components/nothing.xml"}
|
||||
</div>
|
||||
<div n:if="count($audios) > 0">
|
||||
{foreach $audios as $audio}
|
||||
{include "player.xml", audio => $audio, addOnClick => "addAudio({$audio->getId()})"}
|
||||
{/foreach}
|
||||
</div>
|
||||
|
||||
<div n:if="count($audios) > 0">
|
||||
{script "js/node_modules/umbrellajs/umbrella.min.js"}
|
||||
{script "js/node_modules/dashjs/dist/dash.all.min.js"}
|
||||
|
||||
{include "player.js.xml", audios => $audios}
|
||||
</div>
|
||||
</div>
|
||||
{/block}
|
77
Web/Presenters/templates/Audio/Search.xml
Normal file
77
Web/Presenters/templates/Audio/Search.xml
Normal file
|
@ -0,0 +1,77 @@
|
|||
{extends "../@layout.xml"}
|
||||
|
||||
{block title}Аудиозаписи{/block}
|
||||
|
||||
{block header}
|
||||
Поиск
|
||||
{/block}
|
||||
|
||||
{block content}
|
||||
|
||||
{include "tabs.xml", mode => "search"}
|
||||
|
||||
<div style="padding: 8px;">
|
||||
<form>
|
||||
<input n:attr="value => $q" name="q" type="text" placeholder="Поиск..." />
|
||||
<div style="display: flex; justify-content: space-between; padding: 8px 0;">
|
||||
<div><input n:attr="checked => $by_performer" type="checkbox" name="by_performer" /> исполнитель</div>
|
||||
<button class="button">Найти</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div n:if="$q && (count($audios) > 0 || count($playlists) > 0)">
|
||||
<style>
|
||||
.playlist-name {
|
||||
max-width: 100px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, system-ui, "Helvetica Neue", Roboto, sans-serif;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.playlist-name:hover { text-decoration: underline; }
|
||||
</style>
|
||||
<h4 style="padding: 8px;">Плейлисты</h4>
|
||||
<div style="padding: 8px;">
|
||||
<div n:if="count($playlists) <= 0">
|
||||
{include "../components/nothing.xml"}
|
||||
</div>
|
||||
<div n:if="count($playlists) > 0" style="display: flex; gap: 8px; overflow-x: auto;">
|
||||
<div n:foreach="$playlists as $playlist">
|
||||
<div style="cursor: pointer;"
|
||||
onClick="window.location.href = '/playlist{$playlist->getOwner()->getId()}_{$playlist->getId()}'">
|
||||
<div>
|
||||
<img src="{$playlist->getCoverURL()}" width="100" height="100"
|
||||
style="border-radius: 8px;"/>
|
||||
</div>
|
||||
<div class="playlist-name">{$playlist->getName()}</div>
|
||||
<a href="{$playlist->getOwner()->getURL()}" class="playlist-name" style="font-weight: 400;">
|
||||
{$playlist->getOwner()->getCanonicalName()}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 style="padding: 8px;">Треки</h4>
|
||||
<div n:if="count($audios) <= 0">
|
||||
{include "../components/nothing.xml"}
|
||||
</div>
|
||||
<div n:if="count($audios) > 0" n:foreach="$audios as $audio">
|
||||
{include "player.xml", audio => $audio, addOnClick => "addAudio({$audio->getId()})"}
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<div n:if="count($audios) > 0">
|
||||
{script "js/node_modules/umbrellajs/umbrella.min.js"}
|
||||
{script "js/node_modules/dashjs/dist/dash.all.min.js"}
|
||||
|
||||
{include "player.js.xml", audios => $audios}
|
||||
</div>
|
||||
</div>
|
||||
<div n:if="count($playlists) <= 0 && count($audios) <= 0">
|
||||
{include "../components/nothing.xml"}
|
||||
</div>
|
||||
</div>
|
||||
{/block}
|
144
Web/Presenters/templates/Audio/player.js.xml
Normal file
144
Web/Presenters/templates/Audio/player.js.xml
Normal file
|
@ -0,0 +1,144 @@
|
|||
<script>
|
||||
function fmtTime(time) {
|
||||
const mins = String(Math.floor(time / 60)).padStart(2, '0');
|
||||
const secs = String(Math.floor(time % 60)).padStart(2, '0');
|
||||
return `${ mins}:${ secs}`;
|
||||
}
|
||||
|
||||
function initPlayer(id, keys, url, length) {
|
||||
console.log(`INIT PLAYER ${ id}`, keys, url, length);
|
||||
const audio = document.querySelector(`#audioEmbed-${ id} .audio`);
|
||||
const playButton = u(`#audioEmbed-${ id} .playerButton > img`);
|
||||
const trackDiv = u(`#audioEmbed-${ id} .track > div > div`);
|
||||
const volumeSpan = u(`#audioEmbed-${ id} .volume span`);
|
||||
const rect = document.querySelector(`#audioEmbed-${ id} .selectableTrack`).getBoundingClientRect();
|
||||
|
||||
const protData = {
|
||||
"org.w3.clearkey": {
|
||||
"clearkeys": keys
|
||||
}
|
||||
};
|
||||
|
||||
const player = dashjs.MediaPlayer().create();
|
||||
player.initialize(audio, url, false);
|
||||
player.setProtectionData(protData);
|
||||
|
||||
playButton.on("click", () => {
|
||||
if (audio.paused) {
|
||||
document.querySelectorAll('audio').forEach(el => el.pause());
|
||||
audio.play();
|
||||
} else {
|
||||
audio.pause();
|
||||
}
|
||||
});
|
||||
|
||||
u(audio).on("timeupdate", () => {
|
||||
const time = audio.currentTime;
|
||||
const ps = Math.ceil((time * 100) / length);
|
||||
volumeSpan.html(fmtTime(Math.floor(time)));
|
||||
if (ps <= 100)
|
||||
trackDiv.nodes[0].style.width = `${ ps}%`;
|
||||
});
|
||||
|
||||
const playButtonImageUpdate = () => {
|
||||
if ($(`#audioEmbed-${ id} .claimed`).length === 0) {
|
||||
console.log(id);
|
||||
const imgSrc = audio.paused ? "/assets/packages/static/openvk/img/play.jpg" : "/assets/packages/static/openvk/img/pause.jpg";
|
||||
playButton.attr("src", imgSrc);
|
||||
}
|
||||
|
||||
if (!audio.paused) {
|
||||
$.post(`/audio${ id}/listen`, {
|
||||
hash: {$csrfToken}
|
||||
});
|
||||
}
|
||||
|
||||
$(`#audioEmbed-${ id} .performer`).toggle()
|
||||
$(`#audioEmbed-${ id} .track`).toggle()
|
||||
};
|
||||
|
||||
u(audio).on("play", playButtonImageUpdate);
|
||||
u(audio).on(["pause", "ended", "suspended"], playButtonImageUpdate);
|
||||
|
||||
u(`#audioEmbed-${ id} .track > div`).on("click", (e) => {
|
||||
let rect = document.querySelector("#audioEmbed-" + id + " .selectableTrack").getBoundingClientRect();
|
||||
const width = e.clientX - rect.left;
|
||||
const time = Math.ceil((width * length) / (rect.right - rect.left));
|
||||
console.log(width, length, rect.right, rect.left, time);
|
||||
audio.currentTime = time;
|
||||
});
|
||||
}
|
||||
|
||||
function addAudio(id) {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: `/audio${ id}/action?act=add`,
|
||||
success: (response) => {
|
||||
if (response.success) {
|
||||
NewNotification("Успех", "Аудиозапись добавлена", "/assets/packages/static/openvk/img/oxygen-icons/64x64/actions/dialog-ok.png");
|
||||
} else {
|
||||
NewNotification("Ошибка", (response?.error ?? "Неизвестная ошибка"), "/assets/packages/static/openvk/img/error.png");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeAudio(id) {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: `/audio${ id}/action?act=remove`,
|
||||
success: (response) => {
|
||||
if (response.success) {
|
||||
$(`#audioEmbed-${ id}`).remove();
|
||||
NewNotification("Успех", "Аудиозапись удалена", "/assets/packages/static/openvk/img/oxygen-icons/64x64/actions/dialog-ok.png");
|
||||
} else {
|
||||
NewNotification("Ошибка", (response?.error ?? "Неизвестная ошибка"), "/assets/packages/static/openvk/img/error.png");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function editAudio(id, title, performer, genre, lyrics) {
|
||||
$("#editAudioDialogBoxHtml input[name=name]").attr("v", title);
|
||||
$("#editAudioDialogBoxHtml input[name=performer]").attr("v", performer);
|
||||
$("#editAudioDialogBoxHtml select[name=genre]").attr("v", genre);
|
||||
$("#editAudioDialogBoxHtml textarea[name=lyrics]").attr("v", lyrics);
|
||||
|
||||
MessageBox({_edit}, $("#editAudioDialogBoxHtml").html(), [{_ok}, {_cancel}], [
|
||||
function() {
|
||||
let name = $(".ovk-diag-body input[name=name]").val();
|
||||
let perf = $(".ovk-diag-body input[name=performer]").val();
|
||||
let genre = $(".ovk-diag-body select[name=genre]").val();
|
||||
let lyrics = $(".ovk-diag-body textarea[name=lyrics]").val();
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: `/audio${ id}/action?act=edit`,
|
||||
data: {
|
||||
name: name,
|
||||
performer: performer,
|
||||
genre: genre,
|
||||
lyrics: lyrics,
|
||||
hash: {=$csrfToken}
|
||||
},
|
||||
success: (response) => {
|
||||
if (response.success) {
|
||||
NewNotification("Успех", "Аудиозапись отредактирована", "/assets/packages/static/openvk/img/oxygen-icons/64x64/actions/dialog-ok.png");
|
||||
setTimeout(() => { window.location.reload() }, 500)
|
||||
} else {
|
||||
NewNotification("Ошибка", (response?.error ?? "Неизвестная ошибка"), "/assets/packages/static/openvk/img/error.png");
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
Function.noop
|
||||
]);
|
||||
|
||||
$('.ovk-diag-body input, textarea, select').each(function() { $(this).val($(this).attr("v")); });
|
||||
}
|
||||
|
||||
{foreach $audios as $audio}
|
||||
initPlayer({$audio->getId()}, {$audio->getKeys()}, {$audio->getURL()}, {$audio->getLength()});
|
||||
{/foreach}
|
||||
</script>
|
57
Web/Presenters/templates/Audio/player.xml
Normal file
57
Web/Presenters/templates/Audio/player.xml
Normal file
|
@ -0,0 +1,57 @@
|
|||
<div
|
||||
id="audioEmbed-{$audio->getId()}"
|
||||
{if $canAdd || $canRemove || $canEdit}
|
||||
onmouseenter="$('#audioEmbed-{$audio->getId()} .track-additional-info').css('display', 'none'); $('#audioEmbed-{$audio->getId()} .buttons').css('display', 'flex');"
|
||||
onmouseleave="$('#audioEmbed-{$audio->getId()} .track-additional-info').css('display', 'flex'); $('#audioEmbed-{$audio->getId()} .buttons').css('display', 'none')"
|
||||
{/if}
|
||||
>
|
||||
<audio class="audio" />
|
||||
|
||||
<div id="miniplayer" class="audioEntry" style="min-height: 55px;">
|
||||
<div class="playerButton">
|
||||
<img src="/assets/packages/static/openvk/img/play.jpg" />
|
||||
</div>
|
||||
|
||||
<div class="status" style="margin-top: 6px;">
|
||||
<div class="mediaInfo" style="margin-bottom: -8px; cursor: pointer;" onClick="window.location.href = '/audios/search?q=' + {$audio->getTitle()}"">
|
||||
<strong>
|
||||
{$audio->getTitle()}
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
<div class="performer" style="cursor: pointer;" onClick="window.location.href = '/audios/search?q=' + {$audio->getPerformer()} + '&by_performer=on'">
|
||||
<span class="nobold">
|
||||
{$audio->getPerformer()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="track">
|
||||
<center class="claimed" style="width: 100%; padding: 4px; color: #45688E; font-weight: bold;" n:if="$audio->isWithdrawn()">
|
||||
{_audio_embed_withdrawn}
|
||||
</center>
|
||||
<div class="selectableTrack" n:attr="style => $audio->isWithdrawn() ? 'display: none;' : 'border: #707070 1px solid;'">
|
||||
<div>
|
||||
<!-- actual track -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="volume" style="display: flex; flex-direction: column;">
|
||||
<span class="nobold" style="text-align: right;">
|
||||
{$audio->getFormattedLength()}
|
||||
</span>
|
||||
<div class="track-additional-info" style="margin-top: 8px; align-self: flex-end;">
|
||||
<svg n:if="$audio->isExplicit()" xmlns="http://www.w3.org/2000/svg" height="11" viewBox="0 0 11 11" width="11">
|
||||
<path d="m1 2.506v5.988a1.5 1.5 0 0 0 1.491 1.506h6.019c.827 0 1.49-.674 1.49-1.506v-5.988a1.5 1.5 0 0 0 -1.491-1.506h-6.019c-.827 0-1.49.674-1.49 1.506zm4 2.494v-1h2v-1h-3v5h3v-1h-2v-1h2v-1zm-5-2.494a2.496 2.496 0 0 1 2.491-2.506h6.019a2.5 2.5 0 0 1 2.49 2.506v5.988a2.496 2.496 0 0 1 -2.491 2.506h-6.019a2.5 2.5 0 0 1 -2.49-2.506z"
|
||||
fill="#828a99" fill-opacity=".7"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="buttons" style="margin-top: 8px; display: none; justify-content: space-between; gap: 8px; align-self: flex-end;">
|
||||
<div n:if="$canAdd ?? true" class="icon add-icon" n:attr="onClick => $addOnClick ?? ''" />
|
||||
<div n:if="$canRemove" class="icon delete-icon" n:attr="onClick => $deleteOnClick ?? ''" />
|
||||
<div n:if="$canEdit" class="icon edit-icon" n:attr="onClick => $editOnClick ?? ''" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
73
Web/Presenters/templates/Audio/tabs.xml
Normal file
73
Web/Presenters/templates/Audio/tabs.xml
Normal file
|
@ -0,0 +1,73 @@
|
|||
<style>
|
||||
.icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: url("/assets/packages/static/openvk/img/common.png");
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.delete-icon { background-position: 0 -28.5px; }
|
||||
.edit-icon { background-position: 0 -44px; }
|
||||
.plus-icon { background-position: 0 0; }
|
||||
.save-icon { background-position: 0 -15.5px; }
|
||||
.list-icon { background-position: 0 -92.5px; }
|
||||
</style>
|
||||
|
||||
<div class="tabs">
|
||||
<div class="tab" n:attr="id => $mode === 'list' ? 'activetabs' : 'ki'">
|
||||
<a href="/audios{$thisUser->getId()}" n:attr="id => $mode === 'list' ? 'act_tab_a' : 'ki'">{$listText ?? "Моя музыка"}</a>
|
||||
</div>
|
||||
<div class="tab" n:attr="id => $mode === 'new' ? 'activetabs' : 'ki'">
|
||||
<a href="/audios/new" n:attr="id => $mode === 'new' ? 'act_tab_a' : 'ki'">Новое</a>
|
||||
</div>
|
||||
<div class="tab" n:attr="id => $mode === 'popular' ? 'activetabs' : 'ki'">
|
||||
<a href="/audios/popular" n:attr="id => $mode === 'popular' ? 'act_tab_a' : 'ki'">Популярное</a>
|
||||
</div>
|
||||
<div class="tab" n:attr="id => $mode === 'search' ? 'activetabs' : 'ki'">
|
||||
<a href="/audios/search" n:attr="id => $mode === 'search' ? 'act_tab_a' : 'ki'">Поиск</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="editAudioDialogBoxHtml" style="display: none;">
|
||||
<table cellspacing="7" cellpadding="0" border="0" align="center">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td width="120" valign="top">
|
||||
<span class="nobold">Имя:</span>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="name" autocomplete="off"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="120" valign="top">
|
||||
<span class="nobold">Исполнитель:</span>
|
||||
</td>
|
||||
<td>
|
||||
<input name="performer" autocomplete="off"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="120" valign="top">
|
||||
<span class="nobold">Жанр:</span>
|
||||
</td>
|
||||
<td>
|
||||
<select name="genre">
|
||||
<option n:foreach='\openvk\Web\Models\Entities\Audio::genres as $genre'
|
||||
n:attr="selected: $genre == 'Other'" value="{$genre}">
|
||||
{$genre}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="120" valign="top">
|
||||
<span class="nobold">Текст:</span>
|
||||
</td>
|
||||
<td>
|
||||
<textarea name="lyrics"></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
|
@ -597,6 +597,17 @@
|
|||
<td>
|
||||
<span class="nobold">{_my_photos}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="120" valign="top" align="right">
|
||||
<input
|
||||
n:attr="checked => $user->getLeftMenuItemStatus('audios')"
|
||||
type="checkbox"
|
||||
name="menu_muziko" />
|
||||
</td>
|
||||
<td>
|
||||
<span class="nobold">Мои Аудиозаписи</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="120" valign="top" align="right">
|
||||
|
|
|
@ -195,6 +195,16 @@ routes:
|
|||
handler: "Audio->view"
|
||||
- url: "/audio{num}_{num}/embed.xhtml"
|
||||
handler: "Audio->embed"
|
||||
- url: "/audio{num}/listen"
|
||||
handler: "Audio->listen"
|
||||
- url: "/audios/search"
|
||||
handler: "Audio->search"
|
||||
- url: "/audios/newPlaylist"
|
||||
handler: "Audio->newPlaylist"
|
||||
- url: "/playlist{num}_{num}"
|
||||
handler: "Audio->playlist"
|
||||
- url: "/audio{num}/action"
|
||||
handler: "Audio->action"
|
||||
- url: "/{?!club}{num}"
|
||||
handler: "Group->view"
|
||||
placeholders:
|
||||
|
@ -335,6 +345,12 @@ routes:
|
|||
handler: "Admin->bannedLink"
|
||||
- url: "/admin/bannedLink/id{num}/unban"
|
||||
handler: "Admin->unbanLink"
|
||||
- url: "/admin/music"
|
||||
handler: "Admin->music"
|
||||
- url: "/admin/music/{num}/edit"
|
||||
handler: "Admin->editMusic"
|
||||
- url: "/admin/playlist/{num}/edit"
|
||||
handler: "Admin->editPlaylist"
|
||||
- url: "/upload/photo/{text}"
|
||||
handler: "VKAPI->photoUpload"
|
||||
- url: "/method/{text}.{text}"
|
||||
|
|
BIN
Web/static/img/common.png
Normal file
BIN
Web/static/img/common.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
BIN
Web/static/img/song.jpg
Normal file
BIN
Web/static/img/song.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
|
@ -59,6 +59,7 @@ CREATE TABLE IF NOT EXISTS `playlists` (
|
|||
`owner` bigint NOT NULL,
|
||||
`name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci NOT NULL,
|
||||
`description` varchar(2048) DEFAULT NULL,
|
||||
`cover_photo_id` bigint unsigned DEFAULT NULL,
|
||||
`length` int unsigned NOT NULL DEFAULT '0',
|
||||
`special_type` tinyint unsigned NOT NULL DEFAULT '0',
|
||||
`created` bigint unsigned DEFAULT NULL,
|
||||
|
|
Loading…
Reference in a new issue