Интерфейсы

This commit is contained in:
n1rwana 2023-08-14 14:27:31 +03:00
parent 8f31091e24
commit 0151b3d020
No known key found for this signature in database
GPG key ID: 184A60085ABF17D8
26 changed files with 1395 additions and 33 deletions

View file

@ -14,6 +14,7 @@ class Audio extends Media
{
protected $tableName = "audios";
protected $fileExtension = "mpd";
// protected $fileExtension = "mp3";
# Taken from winamp :D
const genres = [
@ -102,8 +103,8 @@ class Audio extends Media
try {
$args = [
OPENVK_ROOT,
$this->getBaseDir(),
str_replace("enabled", "available", OPENVK_ROOT),
str_replace("enabled", "available", $this->getBaseDir()),
$hash,
$filename,
@ -113,17 +114,23 @@ 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);
@ -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();
}

View file

@ -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;
}
}

View file

@ -403,6 +403,7 @@ class User extends RowModel
"length" => 1,
"mappings" => [
"photos",
"audios",
"videos",
"messages",
"notes",

View file

@ -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;
@ -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);
}
}

View file

@ -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,6 +30,7 @@ final class AdminPresenter extends OpenVKPresenter
$this->gifts = $gifts;
$this->bannedLinks = $bannedLinks;
$this->chandlerGroups = $chandlerGroups;
$this->audios = $audios;
parent::__construct();
}
@ -40,6 +50,15 @@ final class AdminPresenter extends OpenVKPresenter
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
{
parent::onStartup();
@ -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();
}
}
}

View file

@ -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
{
$this->template->_template = "Audio/List";
$audios = [];
$playlists = [];
if ($mode === "list") {
$entity = NULL;
if($owner < 0)
$entity = (new Clubs)->get($owner);
else
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->renderApp("owner=$owner");
$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,8 +94,22 @@ final class AudioPresenter extends OpenVKPresenter
if(!$audio->canBeViewedBy($this->user->identity))
$this->flashFail("err", tr("forbidden"), tr("forbidden_comment"));
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]);
}
}

View file

@ -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",

View file

@ -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">

View file

@ -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>

View 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}

View 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}

View 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}">&laquo;</a>
<a n:if="$isLast" class="aui-button" href="?p={($_GET['p'] ?? 1) + 1}">&raquo;</a>
</div>
{/block}

View 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}

View 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}

View 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}

View 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}

View 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}

View 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}

View 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>

View 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>&nbsp;
<!-- 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>

View 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>

View file

@ -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">

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
Web/static/img/song.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -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,