Interface updade

This commit is contained in:
lalka2018 2023-10-14 10:03:49 +03:00
parent de0155b31c
commit 6966ec0b89
32 changed files with 1170 additions and 781 deletions

View file

@ -15,6 +15,7 @@ use openvk\Web\Models\Entities\Video;
use openvk\Web\Models\Repositories\Videos as VideosRepo;
use openvk\Web\Models\Entities\Note;
use openvk\Web\Models\Repositories\Notes as NotesRepo;
use openvk\Web\Models\Repositories\Audios as AudiosRepo;
final class Wall extends VKAPIRequestHandler
{
@ -450,6 +451,8 @@ final class Wall extends VKAPIRequestHandler
$attachmentType = "video";
elseif(str_contains($attac, "note"))
$attachmentType = "note";
elseif(str_contains($attac, "audio"))
$attachmentType = "audio";
else
$this->fail(205, "Unknown attachment type");
@ -483,6 +486,12 @@ final class Wall extends VKAPIRequestHandler
if(!$attacc->getOwner()->getPrivacyPermission('notes.read', $this->getUser()))
$this->fail(11, "Access to note denied");
$post->attach($attacc);
} elseif($attachmentType == "audio") {
$attacc = (new AudiosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId);
if(!$attacc || $attacc->isDeleted())
$this->fail(100, "Audio does not exist");
$post->attach($attacc);
}
}

View file

@ -101,7 +101,7 @@ class Audio extends Media
$this->stateChanges("segment_size", $ss);
$this->stateChanges("length", $duration);
try {
try {
$args = [
str_replace("enabled", "available", OPENVK_ROOT),
str_replace("enabled", "available", $this->getBaseDir()),
@ -114,23 +114,18 @@ class Audio extends Media
$ss,
];
if(Shell::isPowershell()) {
Shell::powershell("-executionpolicy bypass", "-File", __DIR__ . "/../shell/processAudio.ps1", ...$args)
->start();
->start();
} 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');
exit("Linux uploads are not implemented");
}
# 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);
@ -161,7 +156,7 @@ class Audio extends Media
function getLyrics(): ?string
{
return $this->getRecord()->lyrics ?? NULL;
return !is_null($this->getRecord()->lyrics) ? htmlspecialchars($this->getRecord()->lyrics, ENT_DISALLOWED | ENT_XHTML) : NULL;
}
function getLength(): int

View file

@ -195,12 +195,12 @@ class Audios
function getNew(): EntityStream
{
return new EntityStream("Audio", $this->audios->where("created >= " . (time() - 259200))->order("created DESC")->limit(25));
return new EntityStream("Audio", $this->audios->where("created >= " . (time() - 259200))->where(["withdrawn" => 0, "deleted" => 0, "unlisted" => 0])->order("created DESC")->limit(25));
}
function getPopular(): EntityStream
{
return new EntityStream("Audio", $this->audios->where("listens > 0")->order("listens DESC")->limit(25));
return new EntityStream("Audio", $this->audios->where("listens > 0")->where(["withdrawn" => 0, "deleted" => 0, "unlisted" => 0])->order("listens DESC")->limit(25));
}
function isAdded(int $user_id, int $audio_id): bool
@ -211,12 +211,47 @@ class Audios
])->fetch());
}
function find(string $query, int $page = 1, ?int $perPage = NULL): \Traversable
function find(string $query, array $pars = [], string $sort = "id DESC", int $page = 1, ?int $perPage = NULL): \Traversable
{
$query = "%$query%";
$result = $this->audios->where("name LIKE ? OR performer LIKE ?", $query, $query);
$result = $this->audios->where([
"unlisted" => 0,
"deleted" => 0,
]);
return new Util\EntityStream("Audio", $result);
$notNullParams = [];
foreach($pars as $paramName => $paramValue)
if($paramName != "before" && $paramName != "after" && $paramName != "only_performers")
$paramValue != NULL ? $notNullParams+=["$paramName" => "%$paramValue%"] : NULL;
else
$paramValue != NULL ? $notNullParams+=["$paramName" => "$paramValue"] : NULL;
$nnparamsCount = sizeof($notNullParams);
if($notNullParams["only_performers"] == "1") {
$result->where("performer LIKE ?", $query);
} else {
$result->where("name LIKE ? OR performer LIKE ?", $query, $query);
}
if($nnparamsCount > 0) {
foreach($notNullParams as $paramName => $paramValue) {
switch($paramName) {
case "before":
$result->where("created < ?", $paramValue);
break;
case "after":
$result->where("created > ?", $paramValue);
break;
case "with_lyrics":
$result->where("lyrics IS NOT NULL");
break;
}
}
}
return new Util\EntityStream("Audio", $result->order($sort));
}
function findPlaylists(string $query, int $page = 1, ?int $perPage = NULL): \Traversable

View file

@ -4,18 +4,18 @@ use Chandler\Database\Log;
use Chandler\Database\Logs;
use openvk\Web\Models\Entities\{Voucher, Gift, GiftCategory, User, BannedLink};
use openvk\Web\Models\Repositories\{Audios,
Bans,
ChandlerGroups,
ChandlerUsers,
Photos,
Posts,
Users,
Clubs,
Videos,
Util\EntityStream,
Vouchers,
Gifts,
BannedLinks};
BannedLinks,
Bans,
Photos,
Posts,
Videos};
use Chandler\Database\DatabaseConnection;
final class AdminPresenter extends OpenVKPresenter
@ -39,7 +39,7 @@ final class AdminPresenter extends OpenVKPresenter
$this->chandlerGroups = $chandlerGroups;
$this->audios = $audios;
$this->logs = DatabaseConnection::i()->getContext()->table("ChandlerLogs");
parent::__construct();
}

View file

@ -40,10 +40,9 @@ final class AudioPresenter extends OpenVKPresenter
function renderList(?int $owner = NULL, ?string $mode = "list"): void
{
$this->template->_template = "Audio/List";
$this->template->_template = "Audio/List.xml";
$page = (int)($this->queryParam("p") ?? 1);
$audios = [];
$playlists = [];
if ($mode === "list") {
$entity = NULL;
@ -52,15 +51,15 @@ final class AudioPresenter extends OpenVKPresenter
if (!$entity || $entity->isBanned())
$this->redirect("/audios" . $this->user->id);
$audios = $this->audios->getByClub($entity);
$playlists = $this->audios->getPlaylistsByClub($entity);
$audios = $this->audios->getByClub($entity, $page, 10);
$audiosCount = $this->audios->getClubCollectionSize($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);
$audios = $this->audios->getByUser($entity, $page, 10);
$audiosCount = $this->audios->getUserCollectionSize($entity);
}
if (!$entity)
@ -71,16 +70,20 @@ final class AudioPresenter extends OpenVKPresenter
$this->template->isMyClub = ($owner < 0 && $entity->canBeModifiedBy($this->user->identity));
} else if ($mode === "new") {
$audios = $this->audios->getNew();
$audiosCount = $audios->size();
} else {
$audios = $this->audios->getPopular();
$audiosCount = $audios->size();
}
// $this->renderApp("owner=$owner");
if ($audios !== [])
if ($audios !== []) {
$this->template->audios = iterator_to_array($audios);
$this->template->audiosCount = $audiosCount;
}
if ($playlists !== [])
$this->template->playlists = iterator_to_array($playlists);
$this->template->mode = $mode;
$this->template->page = $page;
}
function renderView(int $owner, int $id): void
@ -169,7 +172,7 @@ final class AudioPresenter extends OpenVKPresenter
$name = $this->postParam("name");
$lyrics = $this->postParam("lyrics");
$genre = empty($this->postParam("genre")) ? "undefined" : $this->postParam("genre");
$nsfw = ($this->postParam("nsfw") ?? "off") === "on";
$nsfw = ($this->postParam("explicit") ?? "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"));
@ -212,12 +215,7 @@ final class AudioPresenter extends OpenVKPresenter
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));
}
$this->redirect("/search?type=audios");
}
function renderNewPlaylist(): void
@ -348,30 +346,46 @@ final class AudioPresenter extends OpenVKPresenter
function renderAction(int $audio_id): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction(true);
$this->assertNoCSRF();
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
header("HTTP/1.1 405 Method Not Allowed");
exit(":)");
}
$audio = $this->audios->get($audio_id);
if(!$audio || $audio->isDeleted())
$this->flashFail("err", "error", tr("invalid_audio"), null, true);
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" => "Аудиозапись уже добавлена"]);
}
if($audio->isWithdrawn())
$this->flashFail("err", "error", tr("invalid_audio"), null, true);
if(!$audio->isInLibraryOf($this->user->identity))
$audio->add($this->user->identity);
else
$this->flashFail("err", "error", tr("do_have_audio"), null, true);
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;
if($audio->isInLibraryOf($this->user->identity))
$audio->remove($this->user->identity);
else
$this->flashFail("err", "error", tr("do_not_have_audio"), null, true);
break;
case "edit":
$audio = $this->audios->get($audio_id);
if (!$audio || $audio->isDeleted() || $audio->isWithdrawn() || $audio->isUnlisted())
$this->returnJson(["success" => false, "error" => "Аудиозапись не найдена"]);
$this->flashFail("err", "error", tr("invalid_audio"), null, true);
if ($audio->getOwner()->getId() !== $this->user->id)
$this->returnJson(["success" => false, "error" => "Ошибка доступа"]);
$this->flashFail("err", "error", tr("access_denied"), null, true);
$performer = $this->postParam("performer");
$name = $this->postParam("name");
@ -379,13 +393,22 @@ final class AudioPresenter extends OpenVKPresenter
$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"));
$this->flashFail("err", tr("error"), tr("error_insufficient_info"), null, true);
$audio->setName($name);
$audio->setPerformer($performer);
$audio->setLyrics(empty($lyrics) ? NULL : $lyrics);
$audio->setGenre($genre);
$audio->save();
$this->returnJson(["success" => true, "new_info" => [
"name" => ovk_proc_strtr($audio->getTitle(), 40),
"performer" => ovk_proc_strtr($audio->getPerformer(), 40),
"lyrics" => nl2br($audio->getLyrics() ?? ""),
"lyrics_unformatted" => $audio->getLyrics() ?? "",
"explicit" => $audio->isExplicit(),
"genre" => $audio->getGenre(),
]]);
break;
default:
@ -394,4 +417,9 @@ final class AudioPresenter extends OpenVKPresenter
$this->returnJson(["success" => true]);
}
function renderPlaylists(int $owner)
{
$this->assertUserLoggedIn();
}
}

View file

@ -1,7 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{User, Club};
use openvk\Web\Models\Repositories\{Users, Clubs, Posts, Comments, Videos, Applications, Notes};
use openvk\Web\Models\Repositories\{Users, Clubs, Posts, Comments, Videos, Applications, Notes, Audios};
use Chandler\Database\DatabaseConnection;
final class SearchPresenter extends OpenVKPresenter
@ -13,6 +13,7 @@ final class SearchPresenter extends OpenVKPresenter
private $videos;
private $apps;
private $notes;
private $audios;
function __construct(Users $users, Clubs $clubs)
{
@ -23,22 +24,21 @@ final class SearchPresenter extends OpenVKPresenter
$this->videos = new Videos;
$this->apps = new Applications;
$this->notes = new Notes;
$this->audios = new Audios;
parent::__construct();
}
function renderIndex(): void
{
$this->assertUserLoggedIn();
$query = $this->queryParam("query") ?? "";
$type = $this->queryParam("type") ?? "users";
$sorter = $this->queryParam("sort") ?? "id";
$invert = $this->queryParam("invert") == 1 ? "ASC" : "DESC";
$page = (int) ($this->queryParam("p") ?? 1);
$this->willExecuteWriteAction();
if($query != "")
$this->assertUserLoggedIn();
# https://youtu.be/pSAWM5YuXx8
$repos = [
@ -47,7 +47,7 @@ final class SearchPresenter extends OpenVKPresenter
"posts" => "posts",
"comments" => "comments",
"videos" => "videos",
"audios" => "posts",
"audios" => "audios",
"apps" => "apps",
"notes" => "notes"
];
@ -62,7 +62,17 @@ final class SearchPresenter extends OpenVKPresenter
break;
case "rating":
$sort = "rating " . $invert;
break;
break;
case "length":
if($type != "audios") break;
$sort = "length " . $invert;
break;
case "listens":
if($type != "audios") break;
$sort = "listens " . $invert;
break;
}
$parameters = [
@ -86,7 +96,9 @@ final class SearchPresenter extends OpenVKPresenter
"hometown" => $this->queryParam("hometown") != "" ? $this->queryParam("hometown") : NULL,
"before" => $this->queryParam("datebefore") != "" ? strtotime($this->queryParam("datebefore")) : NULL,
"after" => $this->queryParam("dateafter") != "" ? strtotime($this->queryParam("dateafter")) : NULL,
"gender" => $this->queryParam("gender") != "" && $this->queryParam("gender") != 2 ? $this->queryParam("gender") : NULL
"gender" => $this->queryParam("gender") != "" && $this->queryParam("gender") != 2 ? $this->queryParam("gender") : NULL,
"only_performers" => $this->queryParam("only_performers") == "on" ? "1" : NULL,
"with_lyrics" => $this->queryParam("with_lyrics") == "on" ? true : NULL,
];
$repo = $repos[$type] or $this->throwError(400, "Bad Request", "Invalid search entity $type.");

View file

@ -5,7 +5,7 @@ use openvk\Web\Util\Sms;
use openvk\Web\Themes\Themepacks;
use openvk\Web\Models\Entities\{Photo, Post, EmailChangeVerification};
use openvk\Web\Models\Entities\Notifications\{CoinsTransferNotification, RatingUpNotification};
use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Videos, Notes, Vouchers, EmailChangeVerifications};
use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Videos, Notes, Vouchers, EmailChangeVerifications, Audios};
use openvk\Web\Models\Exceptions\InvalidUserNameException;
use openvk\Web\Util\Validator;
use Chandler\Security\Authenticator;
@ -45,6 +45,8 @@ final class UserPresenter extends OpenVKPresenter
$this->template->videosCount = (new Videos)->getUserVideosCount($user);
$this->template->notes = (new Notes)->getUserNotes($user, 1, 4);
$this->template->notesCount = (new Notes)->getUserNotesCount($user);
$this->template->audios = (new Audios)->getByUser($user, 1, 3);
$this->template->audiosCount = (new Audios)->getUserCollectionSize($user);
$this->template->user = $user;
}

View file

@ -16,6 +16,8 @@
{script "js/node_modules/umbrellajs/umbrella.min.js"}
{script "js/l10n.js"}
{script "js/openvk.cls.js"}
{script "js/node_modules/dashjs/dist/dash.all.min.js"}
{script "js/al_music.js"}
{css "js/node_modules/tippy.js/dist/backdrop.css"}
{css "js/node_modules/tippy.js/dist/border.css"}
@ -122,6 +124,7 @@
<option value="comments">{_s_by_comments}</option>
<option value="videos">{_s_by_videos}</option>
<option value="apps">{_s_by_apps}</option>
<option value="audios">{_s_by_audios}</option>
</select>
</form>
<div class="searchTips" id="srcht" hidden>
@ -140,13 +143,13 @@
<option value="comments" {if str_contains($_SERVER['REQUEST_URI'], "type=comments")}selected{/if}>{_s_by_comments}</option>
<option value="videos" {if str_contains($_SERVER['REQUEST_URI'], "type=videos")}selected{/if}>{_s_by_videos}</option>
<option value="apps" {if str_contains($_SERVER['REQUEST_URI'], "type=apps")}selected{/if}>{_s_by_apps}</option>
<option value="audios" {if str_contains($_SERVER['REQUEST_URI'], "type=audios")}selected{/if}>{_s_by_audios}</option>
</select>
<button class="searchBtn"><span style="color:white;font-weight: 600;font-size:12px;">{_header_search}</span></button>
</form>
<script>
let els = document.querySelectorAll("div.dec")
for(const element of els)
{
for(const element of els) {
element.style.display = "none"
}
</script>
@ -181,7 +184,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('audios')" href="/audios{$thisUser->getId()}" class="link">{_my_audios}</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">
@ -427,6 +430,12 @@
//]]>
</script>
<script>
window.openvk = {
"audio_genres": {\openvk\Web\Models\Entities\Audio::genres}
}
</script>
{ifset bodyScripts}
{include bodyScripts}
{/ifset}

View file

@ -16,19 +16,19 @@
<input class="text medium-field" type="number" id="id" disabled value="{$audio->getId()}" />
</div>
<div class="field-group">
<label for="name">Название</label>
<label for="name">{_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>
<label for="performer">{_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>
<label for="ext">{_lyrics}</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>
<label for="ext">{_genre}</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}">
@ -38,7 +38,7 @@
</div>
<hr />
<div class="field-group">
<label for="owner">Владелец</label>
<label for="owner">{_owner}</label>
<input class="text medium-field" type="number" id="owner_id" name="owner" value="{$audio->getOwner()->getId()}" />
</div>
<div class="field-group">
@ -46,11 +46,11 @@
<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>
<label for="deleted">{_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>
<label for="withdrawn">{_withdrawn}</label>
<input class="toggle-large" type="checkbox" id="withdrawn" name="withdrawn" value="1" {if $audio->isWithdrawn()} checked {/if} />
</div>
<hr />

View file

@ -2,11 +2,11 @@
{var $search = $mode === "audios"}
{block title}
Поиск музыки
{_audios}
{/block}
{block heading}
Музыка
{_audios}
{/block}
{block searchTitle}
@ -19,10 +19,10 @@
<div class="aui-navgroup-primary">
<ul class="aui-nav" resolved="">
<li n:attr="class => $mode === 'audios' ? 'aui-nav-selected' : ''">
<a href="?act=audios">Аудиозаписи</a>
<a href="?act=audios">{_audios}</a>
</li>
<li n:attr="class => $mode === 'playlists' ? 'aui-nav-selected' : ''">
<a href="?act=playlists">Плейлисты</a>
<a href="?act=playlists">{_playlists}</a>
</li>
</ul>
</div>
@ -39,12 +39,12 @@
<th>{_admin_author}</th>
<th>Исполнитель</th>
<th>{_admin_title}</th>
<th>Жанр</th>
<th>{_genre}</th>
<th>Explicit</th>
<th>Изъято</th>
<th>Удалено</th>
<th>Создан</th>
<th>Действия</th>
<th>{_withdrawn}</th>
<th>{_deleted}</th>
<th>{_created}</th>
<th>{_actions}</th>
</tr>
</thead>
<tbody>
@ -87,7 +87,7 @@
<tr>
<th>ID</th>
<th>{_admin_author}</th>
<th>Название</th>
<th>{_name}</th>
<th>Создан</th>
<th>Действия</th>
</tr>

View file

@ -1,86 +1,97 @@
{extends "../@layout.xml"}
{block title}Аудиозаписи{/block}
{block title}{_audios}{/block}
{block header}
<div>
<div n:if="$isMy">Мои аудиозаписи</div>
<div n:if="!$isMy">
<a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a>
»
Аудиозаписи
</div>
<div n:if="$mode == 'list'">
<div n:if="$isMy">{_my_audios_small}</div>
<div n:if="!$isMy">
<a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a>
»
{_audios}
</div>
</div>
<div n:if="$mode == 'new'">
{_audios}
»
{_audio_new}
</div>
<div n:if="$mode == 'popular'">
{_audios}
»
{_audio_popular}
</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'" />
{* ref: https://archive.li/P32em *}
<div class="bigPlayer">
<div class="paddingLayer">
<div class="playButtons">
<div class="playButton musicIcon"></div>
<div class="arrowsButtons">
<div class="nextButton musicIcon"></div>
<div class="backButton musicIcon"></div>
</div>
</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 class="trackPanel">
<div class="trackInfo">
<b>{_track_unknown}</b>
<span>{_track_noname}</span>
<span class="time">00:00</span>
</div>
<div class="selectableTrack">
<div>&nbsp;
<div class="slider"></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 class="volumePanel">
<div class="selectableTrack">
<div>&nbsp;
<div class="slider"></div>
</div>
</div>
</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 class="additionalButtons">
<div class="repeatButton musicIcon"></div>
<div class="shuffleButton musicIcon"></div>
<div class="deviceButton musicIcon"></div>
</div>
</div>
</div>
<div n:if="count($playlists) === 0 && count($audios) === 0 && !($isMy || $isMyClub)" style="padding: 8px;">
{include "../components/nothing.xml"}
<div style="width: 100%;display: flex;margin-bottom: -10px;">
<div style="width: 74%;">
<div style="padding: 8px;">
<div n:if="$audiosCount <= 0">
{include "../components/nothing.xml"}
</div>
<div n:if="$audiosCount > 0" class="infContainer">
<div class="infObj" n:foreach="$audios as $audio">
{include "player.xml", audio => $audio}
</div>
</div>
<div>
{include "../components/paginator.xml", conf => (object) [
"page" => $page,
"count" => $audiosCount,
"amount" => sizeof($audios),
"perPage" => $perPage ?? OPENVK_DEFAULT_PER_PAGE,
"atBottom" => true,
]}
</div>
</div>
</div>
{include "tabs.xml"}
</div>
{/block}
{/block}

View file

@ -1,29 +0,0 @@
{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,15 @@
{extends "../@layout.xml"}
{block title}{_audios}{/block}
{block header}
{/block}
{block content}
{script "js/node_modules/umbrellajs/umbrella.min.js"}
<div style="width: 100%;display: flex;margin-bottom: -10px;">
{include "tabs.xml"}
</div>
{/block}

View file

@ -1,29 +0,0 @@
{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

@ -1,77 +0,0 @@
{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

@ -20,106 +20,134 @@
{/block}
{block content}
<div class="container_gray" style="background: white; border: 0;">
<div class="container_gray" style="border: 0;margin-top: -10px;">
<div id="upload_container">
<h4>{_select_audio}</h4><br/>
<b><a href="javascript:false">{_limits}</a></b>
<ul>
<li>{tr("audio_requirements", 1, 30, 25)}</li>
</ul>
<div id="audio_upload">
<form enctype="multipart/form-data" method="POST">
<input type="hidden" name="name" />
<input type="hidden" name="performer" />
<input type="hidden" name="lyrics" />
<input type="hidden" name="genre" />
<input type="hidden" name="hash" value="{$csrfToken}" />
<input id="audio_input" type="file" name="blob" accept="audio/*" />
</form>
</div><br/>
<span>{_you_can_also_add_audio_using} <b><a href="/player">{_search_audio_inst}</a></b>.<span>
<div id="firstStep">
<h4>{_select_audio}</h4><br/>
<b><a href="javascript:false">{_limits}</a></b>
<ul>
<li>{tr("audio_requirements", 1, 30, 25)}</li>
</ul>
<div id="audio_upload">
<form enctype="multipart/form-data" method="POST">
<input type="hidden" name="name" />
<input type="hidden" name="performer" />
<input type="hidden" name="lyrics" />
<input type="hidden" name="genre" />
<input type="hidden" name="explicit" />
<input type="hidden" name="hash" value="{$csrfToken}" />
<input id="audio_input" type="file" name="blob" accept="audio/*" style="display:none" />
<input value="{_upload_button}" class="button" type="button" onclick="document.querySelector('#audio_input').click()">
</form>
</div><br/>
<span>{_you_can_also_add_audio_using} <b><a href="/player">{_search_audio_inst}</a></b>.<span>
</div>
<div id="lastStep" style="display:none;">
<table cellspacing="7" cellpadding="0" border="0" align="center">
<tbody>
<tr>
<td width="120" valign="top"><span class="nobold">{_audio_name}:</span></td>
<td><input type="text" name="name" autocomplete="off" /></td>
</tr>
<tr>
<td width="120" valign="top"><span class="nobold">{_performer}:</span></td>
<td><input name="performer" type="text" autocomplete="off" /></td>
</tr>
<tr>
<td width="120" valign="top"><span class="nobold">{_genre}:</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">{_lyrics}:</span></td>
<td><textarea name="lyrics" style="resize: vertical;max-height: 300px;"></textarea></td>
</tr>
<tr>
<td width="120" valign="top"></td>
<td>
<label><input type="checkbox" name="explicit">{_audios_explicit}</label>
</td>
</tr>
<tr>
<td width="120" valign="top"></td>
<td>
<input class="button" type="button" id="uploadMuziko" value="{_upload_button}">
<input class="button" type="button" id="backToUpload" value="{_select_another_file}">
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div id="dialogBoxHtml" 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>
<script type="module">
import * as id3 from "/assets/packages/static/openvk/js/node_modules/id3js/lib/id3.js";
u("#audio_input").on("change", async function(e) {
if(e.currentTarget.files.length <= 0)
let files = e.currentTarget.files
if(files.length <= 0)
return;
var name_ = document.querySelector("#audio_upload input[name=name]");
var perf_ = document.querySelector("#audio_upload input[name=performer]");
var genre_ = document.querySelector("#audio_upload input[name=genre]");
var lyrics_ = document.querySelector("#audio_upload input[name=lyrics]");
document.querySelector("#firstStep").style.display = "none"
document.querySelector("#lastStep").style.display = "block"
MessageBox({_upload_audio}, document.querySelector("#dialogBoxHtml").innerHTML, [{_ok}, {_cancel}], [
function() {
var name = u("input[name=name]", this.$dialog().nodes[0]).nodes[0].value;
var perf = u("input[name=performer]", this.$dialog().nodes[0]).nodes[0].value;
var genre = u("select[name=genre]", this.$dialog().nodes[0]).nodes[0].value;
var lyrics = u("textarea[name=lyrics]", this.$dialog().nodes[0]).nodes[0].value;
name_.value = name;
perf_.value = perf;
genre_.value = genre;
lyrics_.value = lyrics;
document.querySelector("#audio_upload > form").submit();
},
Function.noop
]);
let tags = await id3.fromFile(e.currentTarget.files[0]);
let tags = await id3.fromFile(files[0]);
if(tags != null) {
console.log("ID" + tags.kind + " detected, setting values...");
if(tags.title != null)
document.querySelector(".ovk-diag input[name=name]").value = tags.title;
document.querySelector("#lastStep input[name=name]").value = tags.title;
if(tags.artist != null)
document.querySelector(".ovk-diag input[name=performer]").value = tags.artist;
document.querySelector("#lastStep input[name=performer]").value = tags.artist;
if(tags.genre != null) {
if(document.querySelector(".ovk-diag select[name=genre] > option[value='" + tags.genre + "']") != null) {
document.querySelector(".ovk-diag select[name=genre]").value = tags.genre;
if(document.querySelector("#lastStep select[name=genre] > option[value='" + tags.genre + "']") != null) {
document.querySelector("#lastStep select[name=genre]").value = tags.genre;
} else {
console.warn("Unknown genre: " + tags.genre);
}
}
} else {
document.querySelector("#lastStep input[name=name]").value = files[0].name
document.querySelector("#lastStep select[name=genre]").value = "Other"
}
});
u("#backToUpload").on("click", (e) => {
document.querySelector("#firstStep").style.display = "block"
document.querySelector("#lastStep").style.display = "none"
document.querySelector("#lastStep input[name=name]").value = ""
document.querySelector("#lastStep input[name=performer]").value = ""
document.querySelector("#lastStep select[name=genre]").value = ""
document.querySelector("#lastStep textarea[name=lyrics]").value = ""
document.querySelector("#audio_input").value = ""
})
u("#uploadMuziko").on("click", (e) => {
var name_ = document.querySelector("#audio_upload input[name=name]");
var perf_ = document.querySelector("#audio_upload input[name=performer]");
var genre_ = document.querySelector("#audio_upload input[name=genre]");
var lyrics_ = document.querySelector("#audio_upload input[name=lyrics]");
var explicit_ = document.querySelector("#audio_upload input[name=explicit]");
name_.value = document.querySelector("#lastStep input[name=name]").value
perf_.value = document.querySelector("#lastStep input[name=performer]").value
genre_.value = document.querySelector("#lastStep select[name=genre]").value
lyrics_.value = document.querySelector("#lastStep textarea[name=lyrics]").value
explicit_.value = document.querySelector("#lastStep input[name=explicit]").value
document.querySelector("#audio_upload > form").submit();
})
</script>
{/block}

View file

@ -1,144 +0,0 @@
<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

@ -1,35 +1,36 @@
<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}
>
{* На одной странице может оказаться несколько одинаковых аудиозаписей, и если запустить
одну, то начнут проигрываться все остальные. Поэтому тут мы добавляем рандомное число*}
{php $id = $audio->getId() . rand(0, 1000)}
{php $isWithdrawn = $audio->isWithdrawn()}
<div id="audioEmbed-{$id}" data-realid="{$audio->getId()}" data-genre="{$audio->getGenre()}" class="audioEmbed {if !$audio->isAvailable()}lagged{/if} {if $isWithdrawn}withdrawn{/if}" onmouseenter="!this.classList.contains('inited') ? initPlayer({$id}, {$audio->getKeys()}, {$audio->getURL()}, {$audio->getLength()}) : console.log('Player already inited.')">
<audio class="audio" />
<div id="miniplayer" class="audioEntry" style="min-height: 55px;">
<div id="miniplayer" class="audioEntry" style="min-height: 37px;">
<div class="playerButton">
<img src="/assets/packages/static/openvk/img/play.jpg" />
<div class="playIcon"></div>
</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()}
<div class="status" style="margin-top: 9px;">
<div class="mediaInfo" style="margin-bottom: -8px; cursor: pointer;">
<strong class="performer">
<a style="color: unset !important;" href="/search?query=&type=audios&sort=id&only_performers=on&query={$audio->getPerformer()}">{ovk_proc_strtr($audio->getPerformer(), 50)}</a>
</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 class="title {if !empty($audio->getLyrics())}withLyrics{/if}">
{ovk_proc_strtr($audio->getTitle(), 50)}
</span>
<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="track">
<center class="claimed" style="width: 100%; padding: 4px; color: #45688E; font-weight: bold;" n:if="$audio->isWithdrawn()">
{_audio_embed_withdrawn}
<div class="track" style="margin-top: 3px">
<center class="claimed" style="width: 100%; padding: 4px; color: #45688E; font-weight: bold;" n:if="$isWithdrawn">
{_audio_embed_withdrawn}
</center>
<div class="selectableTrack" n:attr="style => $audio->isWithdrawn() ? 'display: none;' : 'border: #707070 1px solid;'">
<div class="selectableTrack" n:attr="style => $isWithdrawn ? 'display: none;' : ''">
<div>&nbsp;
<!-- actual track -->
</div>
@ -37,21 +38,20 @@
</div>
</div>
<div class="volume" style="display: flex; flex-direction: column;">
<span class="nobold" style="text-align: right;">
<div class="volume" style="display: flex; flex-direction: column;width:14%;">
<span class="nobold" style="text-align: center;margin-top: 12px;">
{$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 class="buttons" style="margin-top: 8px;">
{php $hasAudio = $audio->isInLibraryOf($thisUser)}
<div class="remove-icon musicIcon" data-id="{$audio->getId()}" n:if="$hasAudio" ></div>
<div class="add-icon musicIcon" data-id="{$audio->getId()}" n:if="!$hasAudio" ></div>
<div class="edit-icon musicIcon" data-lyrics="{$audio->getLyrics()}" data-title="{$audio->getTitle()}" data-performer="{$audio->getPerformer()}" n:if="$audio->canBeModifiedBy($thisUser) && !$isWithdrawn" ></div>
</div>
</div>
</div>
</div>
<div class="lyrics" n:if="!empty($audio->getLyrics())">
{nl2br($audio->getLyrics())|noescape}
</div>
</div>

View file

@ -1,73 +1,10 @@
<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>
<div class="searchOptions newer">
<ul class="searchList">
<a n:attr="id => $mode === 'list' ? 'used' : 'ki'" href="/audios{$thisUser->getId()}">{_my_music}</a>
<a href="/player/upload">{_upload_audio}</a>
<a n:attr="id => $mode === 'playlists' ? 'used' : 'ki'" href="/playlists{$thisUser->getId()}">{_my_playlists}</a>
<a n:attr="id => $mode === 'new' ? 'used' : 'ki'" href="/audios/new">{_audio_new}</a>
<a n:attr="id => $mode === 'popular' ? 'used' : 'ki'" href="/audios/popular">{_audio_popular}</a>
<a href="/search?type=audios">{_audio_search}</a>
</ul>
</div>

View file

@ -204,12 +204,14 @@
</div>
{elseif $type == "videos"}
{foreach $data as $dat}
<div class="content">
{include "../components/video.xml", video => $dat}
</div>
<div class="content">
{include "../components/video.xml", video => $dat}
</div>
{/foreach}
{elseif $type == "audios"}
хуй
{foreach $data as $dat}
{include "../Audio/player.xml", audio => $dat}
{/foreach}
{/if}
{else}
{ifset customErrorMessage}
@ -230,12 +232,13 @@
{block searchOptions}
<div class="searchOptions">
<ul class="searchList">
<li {if $type === "users"} id="used"{/if}><a href="/search?type=users{if $_GET['query']}&query={urlencode($_GET['query'])}{/if}&sort=id"> {_s_people} {if $type === "users"} ({$count}){/if}</a></li>
<li {if $type === "groups"} id="used"{/if}><a href="/search?type=groups{if $_GET['query']}&query={urlencode($_GET['query'])}{/if}&sort=id"> {_s_groups} {if $type === "groups"} ({$count}){/if}</a></li>
<li {if $type === "comments"}id="used"{/if}><a href="/search?type=comments{if $_GET['query']}&query={urlencode($_GET['query'])}{/if}&sort=id">{_s_comments} {if $type === "comments"}({$count}){/if}</a></li>
<li {if $type === "posts"} id="used"{/if}><a href="/search?type=posts{if $_GET['query']}&query={urlencode($_GET['query'])}{/if}&sort=id"> {_s_posts} {if $type === "posts"} ({$count}){/if}</a></li>
<li {if $type === "videos"} id="used"{/if}><a href="/search?type=videos{if $_GET['query']}&query={urlencode($_GET['query'])}{/if}&sort=id"> {_s_videos} {if $type === "videos"} ({$count}){/if}</a></li>
<li {if $type === "apps"} id="used"{/if}><a href="/search?type=apps{if $_GET['query']}&query={urlencode($_GET['query'])}{/if}&sort=id"> {_s_apps} {if $type === "apps"} ({$count}){/if}</a></li>
<a {if $type === "users"} id="used"{/if} href="/search?type=users{if $_GET['query']}&query={urlencode($_GET['query'])}{/if}&sort=id"> {_s_people} {if $type === "users"} ({$count}){/if}</a>
<a {if $type === "groups"} id="used"{/if} href="/search?type=groups{if $_GET['query']}&query={urlencode($_GET['query'])}{/if}&sort=id"> {_s_groups} {if $type === "groups"} ({$count}){/if}</a>
<a {if $type === "comments"}id="used"{/if} href="/search?type=comments{if $_GET['query']}&query={urlencode($_GET['query'])}{/if}&sort=id">{_s_comments} {if $type === "comments"}({$count}){/if}</a>
<a {if $type === "posts"} id="used"{/if} href="/search?type=posts{if $_GET['query']}&query={urlencode($_GET['query'])}{/if}&sort=id"> {_s_posts} {if $type === "posts"} ({$count}){/if}</a>
<a {if $type === "videos"} id="used"{/if} href="/search?type=videos{if $_GET['query']}&query={urlencode($_GET['query'])}{/if}&sort=id"> {_s_videos} {if $type === "videos"} ({$count}){/if}</a>
<a {if $type === "apps"} id="used"{/if} href="/search?type=apps{if $_GET['query']}&query={urlencode($_GET['query'])}{/if}&sort=id"> {_s_apps} {if $type === "apps"} ({$count}){/if}</a>
<a {if $type === "audios"} id="used"{/if} href="/search?type=audios{if $_GET['query']}&query={urlencode($_GET['query'])}{/if}&sort=id"> {_s_audios} {if $type === "audios"} ({$count}){/if}</a>
</ul>
<div class="searchOption">
@ -249,6 +252,11 @@
<option value="rating" {if $_GET["sort"] == "rating"}selected{/if}>{_s_order_by_rating}</option>
{/if}
{/if}
{if $type == "audios"}
<option value="length" n:attr="selected => $_GET['sort'] == 'length'">{_s_order_by_length}</option>
<option value="listens" n:attr="selected => $_GET['sort'] == 'listens'">{_s_order_by_listens}</option>
{/if}
</select>
<div id="invertor">
<input type="checkbox" name="invert" value="1" form="searcher" {if !is_null($_GET['invert']) && $_GET['invert'] == "1"}checked{/if}>{_s_order_invert}
@ -351,9 +359,19 @@
<input type="text" value="{if !is_null($_GET['fav_quote'])}{$_GET['fav_quote']}{/if}" form="searcher" placeholder="{_favorite_quotes}" name="fav_quote">
</div>
</div>
<!--<input name="with_photo" type="checkbox" {if !is_null($_GET['with_photo']) && $_GET['with_photo'] == "on"}checked{/if} form="searcher">{_s_with_photo}-->
{/if}
<input class="button" type="button" id="dnt" value="{_reset}" onclick="resetSearch()">
{if $type == "audios"}
<div class="searchOption">
<div class="searchOptionName" id="n_main_audio" onclick="hideParams('main_audio')"><img src="/assets/packages/static/openvk/img/hide.png" class="searchHide">{_s_main}</div>
<div class="searchOptionBlock" id="s_main_audio">
<label><input type="checkbox" name="only_performers" n:attr="checked => !empty($_GET['only_performers'])" form="searcher">{_s_only_performers}</label>
<label><input type="checkbox" name="with_lyrics" n:attr="checked => !empty($_GET['with_lyrics'])" form="searcher">{_s_with_lyrics}</label>
</div>
</div>
{/if}
<input class="button" type="button" id="dnt" value="{_reset}" onclick="resetSearch()">
</div>
{/block}

View file

@ -594,6 +594,25 @@
</div>
</div>
<div n:if="$audiosCount > 0">
<div class="content_title_expanded" onclick="hidePanel(this, {$audiosCount});">
{_audios}
</div>
<div>
<div class="content_subtitle">
{tr("audios_count", $audiosCount)}
<div style="float:right;">
<a href="/audios{$user->getId()}">{_all_title}</a>
</div>
</div>
<div class="content_list long">
<div class="audio" n:foreach="$audios as $audio" style="width: 100%;">
{include "../Audio/player.xml", audio => $audio}
</div>
</div>
</div>
</div>
<div n:if="OPENVK_ROOT_CONF['openvk']['preferences']['commerce'] && ($giftCount = $user->getGiftCount()) > 0">
<div class="content_title_expanded" onclick="hidePanel(this, {$giftCount});">
{_gifts}

View file

@ -10,6 +10,7 @@
{css "css/dialog.css"}
{css "css/notifications.css"}
{css "css/avataredit.css"}
{css "css/audios.css"}
{if $isXmas}
{css "css/xmas.css"}
@ -27,6 +28,7 @@
{css "css/dialog.css"}
{css "css/notifications.css"}
{css "css/avataredit.css"}
{css "css/audios.css"}
{if $isXmas}
{css "css/xmas.css"}

View file

@ -50,6 +50,10 @@
{else}
{include "post.xml", post => $attachment, compact => true}
{/if}
{elseif $attachment instanceof \openvk\Web\Models\Entities\Audio}
<div style="width:100%;">
{include "../Audio/player.xml", audio => $attachment}
</div>
{else}
<span style="color:red;">{_version_incompatibility}</span>
{/if}

View file

@ -22,6 +22,7 @@
</div>
<div class="post-has-videos"></div>
<div class="post-has-audio"></div>
<div n:if="$postOpts ?? true" class="post-opts">
{var $anonEnabled = OPENVK_ROOT_CONF['openvk']['preferences']['wall']['anonymousPosting']['enable']}
@ -89,6 +90,10 @@
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/mimetypes/application-x-srt.png" />
{_note}
</a>
<a id="_audioAttachment">
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/mimetypes/audio-ac3.png" />
{_audio}
</a>
<a n:if="$graffiti ?? false" href="javascript:initGraffiti({$textAreaId});">
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/actions/draw-brush.png" />
{_graffiti}

View file

@ -209,6 +209,8 @@ routes:
handler: "Audio->newPlaylist"
- url: "/playlist{num}_{num}"
handler: "Audio->playlist"
- url: "/playlists{num}"
handler: "Audio->playlists"
- url: "/audio{num}/action"
handler: "Audio->action"
- url: "/{?!club}{num}"

322
Web/static/css/audios.css Normal file
View file

@ -0,0 +1,322 @@
.musicIcon {
background-image: url('/assets/packages/static/openvk/img/audios_controls.png');
background-repeat: no-repeat;
cursor: pointer;
}
.bigPlayer {
background-color: rgb(240, 241, 242);
margin-left: -10px;
margin-top: -10px;
width: 102.8%;
height: 46px;
border-bottom: 1px solid #d8d8d8;
box-shadow: 1px 0px 8px 0px rgba(34, 60, 80, 0.2);
}
.bigPlayer .paddingLayer {
padding: 0px 0px 0px 14px;
}
.bigPlayer .paddingLayer .playButtons {
padding: 12px 0px;
}
.bigPlayer .paddingLayer .playButtons .playButton {
width: 22px;
height: 22px;
float: left;
}
.bigPlayer .paddingLayer .playButtons .nextButton {
width: 16px;
height: 16px;
background-position-y: -47px;
}
.bigPlayer .paddingLayer .playButtons .backButton {
width: 16px;
height: 16px;
background-position-y: -47px;
background-position-x: -16px;
margin-left: 6px;
}
.bigPlayer .paddingLayer .additionalButtons {
float: left;
margin-top: -6px;
width: 12%;
}
.bigPlayer .paddingLayer .additionalButtons .repeatButton {
width: 16px;
height: 16px;
background-position-y: -49px;
background-position-x: -31px;
margin-left: 6px;
float: left;
}
.bigPlayer .paddingLayer .additionalButtons .shuffleButton {
width: 16px;
height: 16px;
background-position: -50px -50px;
margin-left: 6px;
float: left;
}
.bigPlayer .paddingLayer .additionalButtons .deviceButton {
width: 16px;
height: 16px;
background-position: -202px -51px;
margin-left: 6px;
float: left;
}
.bigPlayer .paddingLayer .playButtons .arrowsButtons {
/* скибиди пидорас */
float: left;
display: flex;
padding-left: 4px;
padding-top: 1.2px;
}
.bigPlayer .paddingLayer .trackPanel {
float: left;
margin-top: -13px;
margin-left: 13px;
width: 386px;
}
.bigPlayer .paddingLayer .volumePanel {
width: 73px;
float: left;
}
.bigPlayer .paddingLayer .slider {
width: 18px;
height: 7px;
background: #707070;
position: absolute;
bottom: 0;
top: 0px;
pointer-events: none;
}
.bigPlayer .paddingLayer .trackInfo .time {
float: right;
font-size: 10px;
margin-right: 8px;
}
.music-app {
display: grid;
}
.music-app--player {
display: grid;
grid-template-columns: 32px 32px 32px 1fr;
padding: 8px;
border-bottom: 1px solid #c1c1c1;
border-bottom-style: solid;
border-bottom-style: dashed;
}
.music-app--player .play,
.music-app--player .perv,
.music-app--player .next {
-webkit-appearance: none;
-moz-appearance: none;
background-color: #507597;
color: #fff;
height: 20px;
margin: 5px;
border: none;
border-radius: 5px;
cursor: pointer;
padding: 0;
font-size: 10px;
}
.music-app--player .info {
margin-left: 10px;
width: 550px;
}
.music-app--player .info .song-name * {
color: black;
}
.music-app--player .info .song-name time {
float: right;
}
.music-app--player .info .track {
margin-top: 8px;
height: 5px;
width: 70%;
background-color: #fff;
border-top: 1px solid #507597;
float: left;
}
.music-app--player .info .track .inner-track {
background-color: #507597;
height: inherit;
width: 15px;
opacity: .7;
}
.audioEntry .status .track > .selectableTrack, .bigPlayer .selectableTrack {
margin-top: 3px;
width: calc(100% - 8px);
border-top: #707070 1px solid;
height: 6px;
position: relative;
user-select: none;
}
.audioEntry .status .track > div > div {
height: 100%;
width: 0%;
background-color: #BFBFBF;
}
#audioEmbed {
user-select: none;
background: #eee;
height: 40px;
width: 486px;
position: absolute;
top: 50%;
left: 50%;
margin-right: -50%;
transform: translate(-50%, -50%);
overflow: hidden;
border: 1px solid #8B8B8B;
}
.audioEntry {
display: flex;
height: 100%;
position: relative;
width: 100%;
}
.audioEntry .playerButton {
position: relative;
padding: 9px;
width: 16px;
height: 16px;
}
.audioEntry .playerButton .playIcon {
background-image: url('/assets/packages/static/openvk/img/play_buttons.gif');
cursor: pointer;
width: 16px;
height: 16px;
background-repeat: no-repeat;
}
.audioEntry .playerButton .playIcon.paused {
background-position-y: -16px;
}
.audioEntry .status {
display: grid;
grid-template-columns: 1fr;
width: 85%;
height: 23px;
}
.audioEntry .status strong {
color: #4C4C4C;
}
.audioEntry .status .track {
display: none;
padding: 4px 0;
}
#audioEmbed .audioEntry .status .track, .audioEntry.playing .status .track {
display: unset;
}
.audioEntry:hover {
background: #EEF0F2;
border-radius: 2px;
}
.audioEntry:hover .buttons {
display: block;
}
.audioEntry .buttons {
display: none;
width: 62px;
height: 20px;
position: absolute;
right: 46px;
top: 2px;
}
.audioEntry .buttons .edit-icon {
width: 11px;
height: 11px;
float: right;
margin-right: 4px;
margin-top: 3px;
background-position: -137px -51px;
}
.audioEntry .buttons .add-icon {
width: 11px;
height: 11px;
float: right;
background-position: -80px -52px;
margin-top: 3px;
}
.add-icon-noaction {
background-image: url('/assets/packages/static/openvk/img/audios_controls.png');
width: 11px;
height: 11px;
float: right;
background-position: -94px -52px;
margin-top: 2px;
margin-right: 2px;
}
.audioEntry .buttons .remove-icon {
margin-top: 3px;
width: 11px;
height: 11px;
float: right;
background-position: -108px -52px;
}
.audioEmbed .lyrics {
display: none;
padding: 0px 33px;
}
.audioEmbed .lyrics.showed {
display: block !important;
}
.audioEntry .withLyrics {
user-select: none;
color: #507597;
}
.audioEntry .withLyrics:hover {
text-decoration: underline;
}
.audioEmbed.withdrawn .status > *, .audioEmbed.withdrawn .playerButton > * {
pointer-events: none;
}
.audioEmbed.withdrawn {
filter: opacity(0.8);
}

View file

@ -1242,64 +1242,6 @@ table.User {
border-bottom: 1px solid #c3cad2;
}
.music-app {
display: grid;
}
.music-app--player {
display: grid;
grid-template-columns: 32px 32px 32px 1fr;
padding: 8px;
border-bottom: 1px solid #c1c1c1;
border-bottom-style: solid;
border-bottom-style: dashed;
}
.music-app--player .play,
.music-app--player .perv,
.music-app--player .next {
-webkit-appearance: none;
-moz-appearance: none;
background-color: #507597;
color: #fff;
height: 20px;
margin: 5px;
border: none;
border-radius: 5px;
cursor: pointer;
padding: 0;
font-size: 10px;
}
.music-app--player .info {
margin-left: 10px;
width: 550px;
}
.music-app--player .info .song-name * {
color: black;
}
.music-app--player .info .song-name time {
float: right;
}
.music-app--player .info .track {
margin-top: 8px;
height: 5px;
width: 70%;
background-color: #fff;
border-top: 1px solid #507597;
float: left;
}
.music-app--player .info .track .inner-track {
background-color: #507597;
height: inherit;
width: 15px;
opacity: .7;
}
.settings_delete {
margin: -12px;
padding: 12px;
@ -2161,77 +2103,6 @@ li {
padding-left: 15px;
}
#audioEmbed {
user-select: none;
background: #eee;
height: 40px;
width: 486px;
position: absolute;
top: 50%;
left: 50%;
margin-right: -50%;
transform: translate(-50%, -50%);
overflow: hidden;
border: 1px solid #8B8B8B;
}
.audioEntry {
display: grid;
grid-template-columns: 1fr 10fr 1fr;
height: 100%;
width: 100%;
}
.audioEntry > * {
padding: 5px;
}
.audioEntry .playerButton {
position: relative;
}
.audioEntry .playerButton img {
position: absolute;
max-width: 50%;
top: 50%;
left: 50%;
margin-right: -50%;
transform: translate(-50%, -50%);
cursor: pointer;
}
.audioEntry .status {
display: grid;
grid-template-columns: 1fr;
}
.audioEntry .status strong {
color: #4C4C4C;
}
.audioEntry .status .track {
display: none;
padding: 4px 0;
}
#audioEmbed .audioEntry .status .track, .audioEntry.playing .status .track {
display: unset;
}
.audioEntry .status .track > .selectableTrack {
margin-top: 3px;
width: calc(100% - 8px);
border-top: #707070 1px solid;
height: 6px;
cursor: text;
}
.audioEntry .status .track > div > div {
height: 100%;
width: 0%;
background-color: #BFBFBF;
}
#votesBalance {
margin-top: 10px;
padding: 7px;
@ -2582,8 +2453,7 @@ a.poll-retract-vote {
display: none;
}
.searchOptions
{
.searchOptions {
overflow: hidden;
width:25.5%;
border-top:1px solid #E5E7E6;
@ -2594,8 +2464,7 @@ a.poll-retract-vote {
margin-right: -7px;
}
.searchBtn
{
.searchBtn {
border: solid 1px #575757;
background-color: #696969;
color:white;
@ -2607,25 +2476,22 @@ a.poll-retract-vote {
margin-top: 1px;
}
.searchBtn:active
{
.searchBtn:active {
border: solid 1px #666666;
background-color: #696969;
color:white;
box-shadow: 0px -2px 0px 0px rgba(255, 255, 255, 0.18) inset;
}
.searchList
{
.searchList {
list-style: none;
user-select: none;
padding-left:0px;
}
.searchList #used
{
.searchList #used {
margin-left:0px;
color: white;
color: white !important;
padding:2px;
padding-top:5px;
padding-bottom:5px;
@ -2636,23 +2502,21 @@ a.poll-retract-vote {
width:87%;
}
.searchList #used a
{
.searchList #used a {
color: white;
}
.sr:focus
{
.sr:focus {
outline:none;
}
.searchHide
{
.searchHide {
padding-right: 5px;
}
.searchList li
.searchList li, .searchList a
{
display: block;
margin-left:0px;
color: #2B587A !important;
cursor:pointer;
@ -2663,13 +2527,15 @@ a.poll-retract-vote {
padding-left:9px;
}
.searchList li a
{
.searchList li a {
min-width:100%;
}
.searchList li:hover
{
.searchList a {
min-width: 88%;
}
.searchList li:hover, .searchList a:hover {
margin-left:0px;
color: #2B587A !important;
background:#ebebeb;
@ -2681,8 +2547,7 @@ a.poll-retract-vote {
width:91%;
}
.whatFind
{
.whatFind {
color:rgb(128, 128, 128);
background:none;
border:none;
@ -3084,3 +2949,10 @@ body.article .floating_sidebar, body.article .page_content {
background: #E9F0F1 !important;
}
.searchOptions.newer {
padding-left: 6px;
border-top: unset !important;
height: unset !important;
border-left: 1px solid #d8d8d8;
width: 26% !important;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 B

250
Web/static/js/al_music.js Normal file
View file

@ -0,0 +1,250 @@
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) {
document.querySelector(`#audioEmbed-${ id}`).classList.add("inited")
const audio = document.querySelector(`#audioEmbed-${ id} .audio`);
const playButton = u(`#audioEmbed-${ id} .playerButton > .playIcon`);
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}%`;
if(audio.paused) {
playButtonImageUpdate()
}
});
const playButtonImageUpdate = () => {
if ($(`#audioEmbed-${ id} .claimed`).length === 0) {
console.log(id);
}
if (!audio.paused) {
playButton.addClass("paused")
$.post(`/audio${ id}/listen`, {
hash: u("meta[name=csrf]").attr("value")
});
} else {
playButton.removeClass("paused")
}
if(!$(`#audioEmbed-${ id}`).hasClass("havePlayed")) {
$(`#audioEmbed-${ id}`).addClass("havePlayed")
$(`#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;
});
}
$(document).on("click", ".musicIcon.edit-icon", (e) => {
let player = e.currentTarget.closest(".audioEmbed")
let id = Number(player.dataset.realid)
let performer = e.currentTarget.dataset.performer
let name = e.currentTarget.dataset.title
let genre = player.dataset.genre
let lyrics = e.currentTarget.dataset.lyrics
MessageBox(tr("edit"), `
<div>
${tr("audio_name")}
<input name="name" maxlength="40" type="text" value="${name}">
</div>
<div style="margin-top: 11px">
${tr("performer")}
<input name="performer" maxlength="40" type="text" value="${performer}">
</div>
<div style="margin-top: 11px">
${tr("genre")}
<select name="genre"></select>
</div>
<div style="margin-top: 11px">
${tr("lyrics")}
<textarea name="lyrics" maxlength="500">${lyrics ?? ""}</textarea>
</div>
`, [tr("ok"), tr("cancel")], [
function() {
let t_name = $(".ovk-diag-body input[name=name]").val();
let t_perf = $(".ovk-diag-body input[name=performer]").val();
let t_genre = $(".ovk-diag-body select[name=genre]").val();
let t_lyrics = $(".ovk-diag-body textarea[name=lyrics]").val();
$.ajax({
type: "POST",
url: `/audio${id}/action?act=edit`,
data: {
name: t_name,
performer: t_perf,
genre: t_genre,
lyrics: t_lyrics,
hash: u("meta[name=csrf]").attr("value")
},
success: (response) => {
if(response.success) {
let perf = player.querySelector(".performer a")
perf.innerHTML = response.new_info.performer
perf.setAttribute("href", "/search?query=&type=audios&sort=id&only_performers=on&query="+response.new_info.performer)
e.currentTarget.setAttribute("data-performer", response.new_info.performer)
let name = player.querySelector(".title")
name.innerHTML = escapeHtml(response.new_info.name)
e.currentTarget.setAttribute("data-title", response.new_info.name)
if(player.querySelector(".lyrics") != null) {
player.querySelector(".lyrics").innerHTML = response.new_info.lyrics
player.querySelector(".title").classList.ad
} else {
player.insertAdjacentHTML("beforeend", `
<div class="lyrics" n:if="!empty($audio->getLyrics())">
${response.new_info.lyrics}
</div>
`)
}
e.currentTarget.setAttribute("data-lyrics", response.new_info.lyrics_unformatted)
player.setAttribute("data-genre", response.new_info.genre)
console.log(response)
} else {
MessageBox(tr("error"), response.flash.message, [tr("ok")], [Function.noop])
}
}
});
},
Function.noop
]);
window.openvk.audio_genres.forEach(elGenre => {
document.querySelector(".ovk-diag-body select[name=genre]").insertAdjacentHTML("beforeend", `
<option value="${elGenre}" ${elGenre == genre ? "selected" : ""}>${elGenre}</option>
`)
})
})
$(document).on("click", ".title.withLyrics", (e) => {
let parent = e.currentTarget.closest(".audioEmbed")
parent.querySelector(".lyrics").classList.toggle("showed")
})
$(document).on("click", ".musicIcon.remove-icon", (e) => {
let id = e.currentTarget.dataset.id
let formdata = new FormData()
formdata.append("hash", u("meta[name=csrf]").attr("value"))
ky.post(`/audio${id}/action?act=remove`, {
hooks: {
beforeRequest: [
(_request) => {
e.currentTarget.classList.add("lagged")
}
],
afterResponse: [
async (_request, _options, response) => {
let json = await response.json()
if(json.success) {
e.currentTarget.classList.remove("remove-icon")
e.currentTarget.classList.add("add-icon")
e.currentTarget.classList.remove("lagged")
let withd = e.currentTarget.closest(".audioEmbed.withdrawn")
if(withd != null) {
u(withd).remove()
}
} else {
MessageBox(tr("error"), json.flash.message, [tr("ok")], [Function.noop])
}
}
]
}, body: formdata
})
})
$(document).on("click", ".musicIcon.add-icon", (e) => {
let id = e.currentTarget.dataset.id
let formdata = new FormData()
formdata.append("hash", u("meta[name=csrf]").attr("value"))
ky.post(`/audio${id}/action?act=add`, {
hooks: {
beforeRequest: [
(_request) => {
e.currentTarget.classList.add("lagged")
}
],
afterResponse: [
async (_request, _options, response) => {
let json = await response.json()
if(json.success) {
e.currentTarget.classList.remove("add-icon")
e.currentTarget.classList.add("remove-icon")
e.currentTarget.classList.remove("lagged")
} else {
MessageBox(tr("error"), json.flash.message, [tr("ok")], [Function.noop])
}
}
]
}, body: formdata
})
})
$(document).on("click", "#_audioAttachment", (e) => {
let body = `
я ещё не сделал
`
MessageBox(tr("select_audio"), body, [tr("ok")], [Function.noop])
})
$(document).on("click", ".audioEmbed.lagged", (e) => {
MessageBox(tr("error"), tr("audio_embed_processing"), [tr("ok")], [Function.noop])
})
$(document).on("click", ".audioEmbed.withdrawn", (e) => {
MessageBox(tr("error"), tr("audio_embed_withdrawn"), [tr("ok")], [Function.noop])
})

View file

@ -479,6 +479,7 @@
"my_videos" = "My Videos";
"my_messages" = "My Messages";
"my_notes" = "My Notes";
"my_audios" = "My Audios";
"my_groups" = "My Groups";
"my_feed" = "My Feed";
"my_feedback" = "My Feedback";
@ -719,6 +720,56 @@
"change_video" = "Change video";
"unknown_video" = "This video is not supported in your version of OpenVK.";
/* Audios */
"audios" = "Audios";
"audio" = "Audio";
"upload_audio" = "Upload audio";
"performer" = "Performer";
"audio_name" = "Name";
"genre" = "Genre";
"lyrics" = "Lyrics";
"select_another_file" = "Select another file";
"limits" = "Limits";
"select_audio" = "Select audio from your computer";
"audio_requirements" = "Audio must be between $1s to $2 minutes, weights to $3MB, contain audio stream must not infringe on the copyright";
"you_can_also_add_audio_using" = "You can also add audio from among the files you have already downloaded using";
"search_audio_inst" = "audios search";
"audio_embed_not_found" = "Audio not found";
"audio_embed_deleted" = "Audio was deleted";
"audio_embed_withdrawn" = "The audio was withdrawn at the request of the copyright holder";
"audio_embed_forbidden" = "The user's privacy settings do not allow this embed this audio";
"audio_embed_processing" = "Audio is still being processed, or has not been processed correctly.";
"audios_count_zero" = "No audios";
"audios_count_one" = "One audio";
"audios_count_few" = "$1 audios";
"audios_count_many" = "$1 audios";
"audios_count_other" = "$1 audios";
"track_unknown" = "Unknown";
"track_noname" = "Without name";
"my_music" = "My music";
"audio_new" = "New";
"audio_popular" = "Popular";
"audio_search" = "Search";
"my_audios_small" = "My audios";
"my_playlists" = "My playlists";
"playlists" = "Playlists";
"audios_explicit" = "Contains obscene language";
"withdrawn" = "Withdrawn";
"deleted" = "Deleted";
"owner" = "Owner";
"select_audio" = "Select audios";
/* Notifications */
"feedback" = "Feedback";
@ -1287,6 +1338,10 @@
"description_too_long" = "Description is too long.";
"invalid_audio" = "Invalid audio.";
"do_not_have_audio" = "You don't have this audio.";
"do_have_audio" = "You already have this audio.";
/* Admin actions */
"login_as" = "Login as $1";
@ -1732,6 +1787,8 @@
"s_order_by_name" = "By name";
"s_order_by_random" = "By random";
"s_order_by_rating" = "By rating";
"s_order_by_length" = "By length";
"s_order_by_listens" = "By listens count";
"s_order_invert" = "Invert";
"s_by_date" = "By date";
@ -1753,6 +1810,8 @@
"deleted_target_comment" = "This comment belongs to deleted post";
"no_results" = "No results";
"s_only_performers" = "Performers only";
"s_with_lyrics" = "With lyrics";
/* BadBrowser */

View file

@ -461,6 +461,7 @@
"my_videos" = "Мои Видеозаписи";
"my_messages" = "Мои Сообщения";
"my_notes" = "Мои Заметки";
"my_audios" = "Мои Аудиозаписи";
"my_groups" = "Мои Группы";
"my_feed" = "Мои Новости";
"my_feedback" = "Мои Ответы";
@ -682,10 +683,12 @@
"upload_audio" = "Загрузить аудио";
"performer" = "Исполнитель";
"audio_name" = "Имя композиции";
"audio_name" = "Название";
"genre" = "Жанр";
"lyrics" = "Текст";
"select_another_file" = "Выбрать другой файл";
"limits" = "Ограничения";
"select_audio" = "Выберите аудиозапись на Вашем компьютере";
"audio_requirements" = "Аудиозапись должна быть длинной от $1c до $2 минут, весить до $3мб, содержать аудиопоток и не нарушать авторские права.";
@ -696,7 +699,31 @@
"audio_embed_deleted" = "Аудиозапись была удалена";
"audio_embed_withdrawn" = "Аудиозапись была изъята по обращению правообладателя";
"audio_embed_forbidden" = "Настройки приватности пользователя не позволяют встраивать эту композицию";
"audio_embed_processing" = "Композиция ещё обрабатывается";
"audio_embed_processing" = "Аудио ещё обрабатывается, либо обработалось неправильно.";
"audios_count_zero" = "Нет аудиозаписей";
"audios_count_one" = "Одна аудиозапись";
"audios_count_few" = "$1 аудиозаписи";
"audios_count_many" = "$1 аудиозаписей";
"audios_count_other" = "$1 аудиозаписей";
"track_unknown" = "Неизвестен";
"track_noname" = "Без названия";
"my_music" = "Моя музыка";
"audio_new" = "Новое";
"audio_popular" = "Популярное";
"audio_search" = "Поиск";
"my_audios_small" = "Мои аудиозаписи";
"my_playlists" = "Мои плейлисты";
"playlists" = "Плейлисты";
"audios_explicit" = "Содержит нецензурную лексику";
"withdrawn" = "Изъято";
"deleted" = "Удалено";
"owner" = "Владелец";
"select_audio" = "Выбрать аудиозаписи";
/* Notifications */
@ -1207,6 +1234,9 @@
"group_owner_is_banned" = "Создатель сообщества успешно забанен.";
"group_is_banned" = "Сообщество успешно забанено";
"description_too_long" = "Описание слишком длинное.";
"invalid_audio" = "Такой аудиозаписи не существует.";
"do_not_have_audio" = "У вас нет этой аудиозаписи.";
"do_have_audio" = "У вас уже есть эта аудиозапись.";
/* Admin actions */
@ -1646,6 +1676,8 @@
"s_order_by_name" = "По имени";
"s_order_by_random" = "По случайности";
"s_order_by_rating" = "По рейтингу";
"s_order_by_length" = "По длине";
"s_order_by_listens" = "По числу прослушиваний";
"s_order_invert" = "Инвертировать";
"s_by_date" = "По дате";
@ -1667,6 +1699,8 @@
"deleted_target_comment" = "Этот комментарий принадлежит к удалённой записи";
"no_results" = "Результатов нет";
"s_only_performers" = "Только исполнители";
"s_with_lyrics" = "С текстом";
/* BadBrowser */