mirror of
https://github.com/openvk/openvk
synced 2025-04-23 00:23:01 +03:00
(смешное название коммита)
- Теперь на странице пользователя/группы показываются три случайные песни, а не первые три как раньше - Теперь пробел на странице аудио не перемещает вас в низ страницы - Оптимизирован мини-плеер, теперь он инициализируется при любом нажатии на него, а не при наведении - Теперь при завершении проигрывания трека в мини-плеере он ищет другой трек рядом, и если находит то воспроизводит. Будет удобно для постов с подборками треков - Поиск теперь показывает 14 результатов - Теперь при возникновении ошибки загрузки аудио она нормально отображается - Вместе с плеером на странице с аудиозаписями теперь двигаются и вкладки - Добавление аудио в группу по идее должно нормально работать
This commit is contained in:
parent
4defb88846
commit
edde634782
15 changed files with 200 additions and 33 deletions
|
@ -120,14 +120,14 @@ class Audio extends Media
|
|||
Shell::powershell("-executionpolicy bypass", "-File", __DIR__ . "/../shell/processAudio.ps1", ...$args)
|
||||
->start();
|
||||
} else {
|
||||
exit("Linux uploads are not implemented");
|
||||
throw new \BadMethodCallException("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
|
||||
throw new \RuntimeException("Timed out waiting FFMPEG");
|
||||
|
||||
} catch(UnknownCommandException $ucex) {
|
||||
exit(OPENVK_ROOT_CONF["openvk"]["debug"] ? "bash/pwsh is not installed" : VIDEOS_FRIENDLY_ERROR);
|
||||
|
|
|
@ -222,4 +222,9 @@ class Playlist extends MediaCollection
|
|||
|
||||
return $cover;
|
||||
}
|
||||
|
||||
function getURL(): string
|
||||
{
|
||||
return "/playlist" . $this->getOwner()->getRealId() . "_" . $this->getId();
|
||||
}
|
||||
}
|
|
@ -109,6 +109,33 @@ class Audios
|
|||
return $this->getByEntityID($user->getId(), ($perPage * ($page - 1)), $perPage, $deleted);
|
||||
}
|
||||
|
||||
function getRandomThreeAudiosByEntityId(int $id): Array
|
||||
{
|
||||
$iter = $this->rels->where("entity", $id);
|
||||
$ids = [];
|
||||
|
||||
foreach($iter as $it)
|
||||
$ids[] = $it->audio;
|
||||
|
||||
$shuffleSeed = openssl_random_pseudo_bytes(6);
|
||||
$shuffleSeed = hexdec(bin2hex($shuffleSeed));
|
||||
|
||||
$ids = knuth_shuffle($ids, $shuffleSeed);
|
||||
$ids = array_slice($ids, 0, 3);
|
||||
$audios = [];
|
||||
|
||||
foreach($ids as $id) {
|
||||
$audio = $this->get((int)$id);
|
||||
|
||||
if(!$audio || $audio->isDeleted())
|
||||
continue;
|
||||
|
||||
$audios[] = $audio;
|
||||
}
|
||||
|
||||
return $audios;
|
||||
}
|
||||
|
||||
function getByClub(Club $club, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable
|
||||
{
|
||||
return $this->getByEntityID($club->getId() * -1, ($perPage * ($page - 1)), $perPage, $deleted);
|
||||
|
|
|
@ -162,7 +162,30 @@ final class AudioPresenter extends OpenVKPresenter
|
|||
} else {
|
||||
$err = !isset($upload) ? 65536 : $upload["error"];
|
||||
$err = str_pad(dechex($err), 9, "0", STR_PAD_LEFT);
|
||||
$this->flashFail("err", tr("error"), tr("error_generic") . "Upload error: 0x$err", null, $isAjax);
|
||||
$readableError = tr("error_generic");
|
||||
|
||||
switch($upload["error"]) {
|
||||
default:
|
||||
case UPLOAD_ERR_INI_SIZE:
|
||||
case UPLOAD_ERR_FORM_SIZE:
|
||||
$readableError = tr("file_too_big");
|
||||
break;
|
||||
case UPLOAD_ERR_PARTIAL:
|
||||
$readableError = tr("file_loaded_partially");
|
||||
break;
|
||||
case UPLOAD_ERR_NO_FILE:
|
||||
$readableError = tr("file_not_uploaded");
|
||||
break;
|
||||
case UPLOAD_ERR_NO_TMP_DIR:
|
||||
$readableError = "Missing a temporary folder.";
|
||||
break;
|
||||
case UPLOAD_ERR_CANT_WRITE:
|
||||
case UPLOAD_ERR_EXTENSION:
|
||||
$readableError = "Failed to write file to disk. ";
|
||||
break;
|
||||
}
|
||||
|
||||
$this->flashFail("err", tr("error"), $readableError . " " . tr("error_code", $err), null, $isAjax);
|
||||
}
|
||||
|
||||
$performer = $this->postParam("performer");
|
||||
|
@ -186,6 +209,12 @@ final class AudioPresenter extends OpenVKPresenter
|
|||
} catch(\DomainException $ex) {
|
||||
$e = $ex->getMessage();
|
||||
$this->flashFail("err", tr("error"), tr("media_file_corrupted_or_too_large") . " $e.", null, $isAjax);
|
||||
} catch(\RuntimeException $ex) {
|
||||
$this->flashFail("err", tr("error"), tr("ffmpeg_timeout"), null, $isAjax);
|
||||
} catch(\BadMethodCallException $ex) {
|
||||
$this->flashFail("err", tr("error"), "Загрузка аудио под Linux на данный момент не реализована. Следите за обновлениями: <a href='https://github.com/openvk/openvk/pull/512/commits'>https://github.com/openvk/openvk/pull/512/commits</a>", null, $isAjax);
|
||||
} catch(\Exception $ex) {
|
||||
$this->flashFail("err", tr("error"), tr("ffmpeg_not_installed"), null, $isAjax);
|
||||
}
|
||||
|
||||
$audio->save();
|
||||
|
|
|
@ -31,7 +31,7 @@ final class GroupPresenter extends OpenVKPresenter
|
|||
$this->template->albumsCount = (new Albums)->getClubAlbumsCount($club);
|
||||
$this->template->topics = (new Topics)->getLastTopics($club, 3);
|
||||
$this->template->topicsCount = (new Topics)->getClubTopicsCount($club);
|
||||
$this->template->audios = (new Audios)->getByClub($club, 1, 3);
|
||||
$this->template->audios = (new Audios)->getRandomThreeAudiosByEntityId($club->getRealId());
|
||||
$this->template->audiosCount = (new Audios)->getClubCollectionSize($club);
|
||||
}
|
||||
|
||||
|
|
|
@ -104,12 +104,13 @@ final class SearchPresenter extends OpenVKPresenter
|
|||
$repo = $repos[$type] or $this->throwError(400, "Bad Request", "Invalid search entity $type.");
|
||||
|
||||
$results = $this->{$repo}->find($query, $parameters, $sort);
|
||||
$iterator = $results->page($page);
|
||||
$iterator = $results->page($page, 14);
|
||||
$count = $results->size();
|
||||
|
||||
$this->template->iterator = iterator_to_array($iterator);
|
||||
$this->template->count = $count;
|
||||
$this->template->type = $type;
|
||||
$this->template->page = $page;
|
||||
$this->template->perPage = 14;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ 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->audios = (new Audios)->getRandomThreeAudiosByEntityId($user->getId());
|
||||
$this->template->audiosCount = (new Audios)->getUserCollectionSize($user);
|
||||
|
||||
$this->template->user = $user;
|
||||
|
|
|
@ -2,6 +2,23 @@
|
|||
|
||||
{block title}{_playlist}{/block}
|
||||
|
||||
{block headIncludes}
|
||||
<meta property="og:type" content="music.album">
|
||||
<meta property="og:title" content="{$playlist->getName()}">
|
||||
<meta property="og:url" content="{$playlist->getURL()}">
|
||||
<meta property="og:description" content="{$playlist->getDescription()}">
|
||||
<meta property="og:image" content="{$playlist->getCoverURL()}">
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "http://schema.org/",
|
||||
"type": "MusicAlbum",
|
||||
"name": {$playlist->getName()},
|
||||
"url": {$playlist->getURL()}
|
||||
}
|
||||
</script>
|
||||
{/block}
|
||||
|
||||
{block header}
|
||||
<a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a>
|
||||
»
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{php $id = $audio->getId() . rand(0, 1000)}
|
||||
{php $isWithdrawn = $audio->isWithdrawn()}
|
||||
{php $editable = isset($thisUser) && $audio->canBeModifiedBy($thisUser)}
|
||||
<div id="audioEmbed-{$id}" data-realid="{$audio->getId()}" {if $hideButtons}data-prettyid="{$audio->getPrettyId()}" data-name="{$audio->getName()}"{/if} data-genre="{$audio->getGenre()}" class="audioEmbed {if !$audio->isAvailable()}processed{/if} {if $isWithdrawn}withdrawn{/if}" onmouseenter="!this.classList.contains('inited') ? initPlayer({$id}, {$audio->getKeys()}, {$audio->getURL()}, {$audio->getLength()}) : void(0)">
|
||||
<div id="audioEmbed-{$id}" data-realid="{$audio->getId()}" {if $hideButtons}data-prettyid="{$audio->getPrettyId()}" data-name="{$audio->getName()}"{/if} data-genre="{$audio->getGenre()}" class="audioEmbed {if !$audio->isAvailable()}processed{/if} {if $isWithdrawn}withdrawn{/if}" data-length="{$audio->getLength()}" data-keys="{json_encode($audio->getKeys())}" data-url="{$audio->getURL()}">
|
||||
<audio class="audio" />
|
||||
|
||||
<div id="miniplayer" class="audioEntry" style="min-height: 39px;">
|
||||
|
|
|
@ -530,7 +530,7 @@
|
|||
{var $musics = explode(", ", $user->getFavoriteMusic())}
|
||||
|
||||
{foreach $musics as $music}
|
||||
<a href="/search?type=users&query=&fav_mus={urlencode($music)}">{$music}</a>{if $music != end($musics)},{/if}
|
||||
<a href="/search?type=audios&query={urlencode($music)}">{$music}</a>{if $music != end($musics)},{/if}
|
||||
{/foreach}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -558,4 +558,11 @@
|
|||
|
||||
.playlistInfo .playlistName {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.searchList.floating {
|
||||
position: fixed;
|
||||
z-index: 199;
|
||||
width: 156px;
|
||||
margin-top: -65px !important;
|
||||
}
|
|
@ -270,7 +270,7 @@ class bigPlayer {
|
|||
})
|
||||
|
||||
u(document).on("keydown", (e) => {
|
||||
if(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
|
||||
if(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", " "].includes(e.key)) {
|
||||
e.preventDefault()
|
||||
|
||||
if(document.querySelector(".ovk-diag-cont") != null)
|
||||
|
@ -290,11 +290,19 @@ class bigPlayer {
|
|||
case "ArrowRight":
|
||||
this.player().currentTime = this.player().currentTime + 3
|
||||
break
|
||||
// буквально
|
||||
case " ":
|
||||
if(this.player().paused)
|
||||
this.play()
|
||||
else
|
||||
this.pause()
|
||||
|
||||
break;
|
||||
}
|
||||
})
|
||||
|
||||
u(document).on("keyup", (e) => {
|
||||
if([32, 87, 65, 83, 68].includes(e.keyCode)) {
|
||||
if([87, 65, 83, 68].includes(e.keyCode)) {
|
||||
e.preventDefault()
|
||||
|
||||
if(document.querySelector(".ovk-diag-cont") != null)
|
||||
|
@ -302,13 +310,6 @@ class bigPlayer {
|
|||
}
|
||||
|
||||
switch(e.keyCode) {
|
||||
case 32:
|
||||
if(this.player().paused)
|
||||
this.play()
|
||||
else
|
||||
this.pause()
|
||||
|
||||
break
|
||||
case 87:
|
||||
case 65:
|
||||
this.showPreviousTrack()
|
||||
|
@ -565,8 +566,10 @@ document.addEventListener("DOMContentLoaded", function() {
|
|||
entries.forEach(x => {
|
||||
if(x.isIntersecting) {
|
||||
document.querySelector('.bigPlayer').classList.remove("floating")
|
||||
document.querySelector('.searchOptions .searchList').classList.remove("floating")
|
||||
document.querySelector('.bigPlayerDetector').style.marginTop = "0px"
|
||||
} else {
|
||||
document.querySelector('.searchOptions .searchList').classList.add("floating")
|
||||
document.querySelector('.bigPlayer').classList.add("floating")
|
||||
document.querySelector('.bigPlayerDetector').style.marginTop = "46px"
|
||||
}
|
||||
|
@ -581,6 +584,20 @@ document.addEventListener("DOMContentLoaded", function() {
|
|||
bigPlayerObserver.observe(bigplayer);
|
||||
}})
|
||||
|
||||
$(document).on("click", ".audioEmbed > *", (e) => {
|
||||
const player = e.currentTarget.closest(".audioEmbed")
|
||||
|
||||
if(player.classList.contains("inited")) return
|
||||
|
||||
initPlayer(player.id.replace("audioEmbed-", ""),
|
||||
JSON.parse(player.dataset.keys),
|
||||
player.dataset.url,
|
||||
player.dataset.length)
|
||||
|
||||
if(e.target.classList.contains("playIcon"))
|
||||
e.target.click()
|
||||
})
|
||||
|
||||
function initPlayer(id, keys, url, length) {
|
||||
document.querySelector(`#audioEmbed-${ id}`).classList.add("inited")
|
||||
const audio = document.querySelector(`#audioEmbed-${ id} .audio`);
|
||||
|
@ -661,8 +678,40 @@ function initPlayer(id, keys, url, length) {
|
|||
}
|
||||
};
|
||||
|
||||
const hideTracks = () => {
|
||||
$(`#audioEmbed-${ id} .track`).toggle()
|
||||
$(`#audioEmbed-${ id}`).removeClass("havePlayed")
|
||||
}
|
||||
|
||||
u(audio).on("play", playButtonImageUpdate);
|
||||
u(audio).on(["pause", "ended", "suspended"], playButtonImageUpdate);
|
||||
u(audio).on(["pause", "suspended"], playButtonImageUpdate);
|
||||
u(audio).on("ended", (e) => {
|
||||
let thisPlayer = e.target.closest(".audioEmbed")
|
||||
let nextPlayer = null
|
||||
if(thisPlayer.closest(".attachment") != null) {
|
||||
try {
|
||||
nextPlayer = thisPlayer.closest(".attachment").nextElementSibling.querySelector(".audioEmbed")
|
||||
} catch(e) {return}
|
||||
} else if(thisPlayer.closest(".audio") != null) {
|
||||
try {
|
||||
nextPlayer = thisPlayer.closest(".audio").nextElementSibling.querySelector(".audioEmbed")
|
||||
} catch(e) {return}
|
||||
} else {
|
||||
nextPlayer = thisPlayer.nextElementSibling
|
||||
}
|
||||
|
||||
playButtonImageUpdate()
|
||||
|
||||
if(!nextPlayer) return
|
||||
|
||||
initPlayer(nextPlayer.id.replace("audioEmbed-", ""),
|
||||
JSON.parse(nextPlayer.dataset.keys),
|
||||
nextPlayer.dataset.url,
|
||||
nextPlayer.dataset.length)
|
||||
|
||||
nextPlayer.querySelector(".playIcon").click()
|
||||
hideTracks()
|
||||
})
|
||||
|
||||
u(`#audioEmbed-${ id} .lengthTrack > div`).on("click", (e) => {
|
||||
let rect = document.querySelector("#audioEmbed-" + id + " .selectableTrack").getBoundingClientRect();
|
||||
|
@ -835,7 +884,7 @@ $(document).on("click", ".musicIcon.remove-icon", (e) => {
|
|||
hooks: {
|
||||
beforeRequest: [
|
||||
(_request) => {
|
||||
e.currentTarget.classList.add("lagged")
|
||||
e.target.classList.add("lagged")
|
||||
}
|
||||
],
|
||||
afterResponse: [
|
||||
|
@ -843,11 +892,11 @@ $(document).on("click", ".musicIcon.remove-icon", (e) => {
|
|||
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")
|
||||
e.target.classList.remove("remove-icon")
|
||||
e.target.classList.add("add-icon")
|
||||
e.target.classList.remove("lagged")
|
||||
|
||||
let withd = e.currentTarget.closest(".audioEmbed.withdrawn")
|
||||
let withd = e.target.closest(".audioEmbed.withdrawn")
|
||||
|
||||
if(withd != null)
|
||||
u(withd).remove()
|
||||
|
@ -920,13 +969,13 @@ $(document).on("click", ".musicIcon.add-icon-group", async (ev) => {
|
|||
$(".ovk-diag-body").on("click", "input[name='addButton']", (e) => {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: `/audio${ev.currentTarget.dataset.id}/action?act=add_to_club`,
|
||||
url: `/audio${ev.target.dataset.id}/action?act=add_to_club`,
|
||||
data: {
|
||||
hash: u("meta[name=csrf]").attr("value"),
|
||||
club: document.querySelector("#addIconsWindow").value
|
||||
},
|
||||
beforeSend: () => {
|
||||
e.currentTarget.classList.add("lagged")
|
||||
e.target.classList.add("lagged")
|
||||
document.querySelector(".errorPlace").innerHTML = ""
|
||||
},
|
||||
success: (response) => {
|
||||
|
@ -949,7 +998,7 @@ $(document).on("click", ".musicIcon.add-icon", (e) => {
|
|||
hooks: {
|
||||
beforeRequest: [
|
||||
(_request) => {
|
||||
e.currentTarget.classList.add("lagged")
|
||||
e.target.classList.add("lagged")
|
||||
}
|
||||
],
|
||||
afterResponse: [
|
||||
|
@ -957,9 +1006,9 @@ $(document).on("click", ".musicIcon.add-icon", (e) => {
|
|||
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")
|
||||
e.target.classList.remove("add-icon")
|
||||
e.target.classList.add("remove-icon")
|
||||
e.target.classList.remove("lagged")
|
||||
} else
|
||||
fastError(json.flash.message)
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ CREATE TABLE IF NOT EXISTS `audio_listens` (
|
|||
`audio` bigint unsigned NOT NULL,
|
||||
`time` bigint unsigned NOT NULL,
|
||||
`index` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'Workaround for Nette DBE bug',
|
||||
`playlist` bigint(20) UNSIGNED DEFAULT NULL,
|
||||
PRIMARY KEY (`index`),
|
||||
KEY `audio` (`audio`),
|
||||
KEY `user` (`entity`) USING BTREE,
|
||||
|
@ -63,6 +64,7 @@ CREATE TABLE IF NOT EXISTS `playlists` (
|
|||
`length` int unsigned NOT NULL DEFAULT '0',
|
||||
`special_type` tinyint unsigned NOT NULL DEFAULT '0',
|
||||
`created` bigint unsigned DEFAULT NULL,
|
||||
`listens` bigint(20) unsigned NOT NULL DEFAULT 0,
|
||||
`edited` bigint unsigned DEFAULT NULL,
|
||||
`deleted` tinyint unsigned DEFAULT '0',
|
||||
PRIMARY KEY (`id`),
|
||||
|
|
|
@ -1402,6 +1402,12 @@
|
|||
"playlist_not_bookmarked" = "This playlist is not in your collection.";
|
||||
"invalid_cover_photo" = "Error when loading cover photo.";
|
||||
"not_a_photo" = "Uploaded file doesn't look like a photo.";
|
||||
"file_too_big" = "File is too big.";
|
||||
"file_loaded_partially" = "The file has been uploaded partially.";
|
||||
"file_not_uploaded" = "Failed to upload the file.";
|
||||
"error_code" = "Error code: $1.";
|
||||
"ffmpeg_timeout" = "Timed out waiting ffmpeg. Try to upload file again.";
|
||||
"ffmpeg_not_installed" = "Failed to proccess the file. It looks like ffmpeg is not installed on this server.";
|
||||
|
||||
/* Admin actions */
|
||||
|
||||
|
@ -1736,8 +1742,17 @@
|
|||
"tour_section_5_text_3" = "In addition to uploading videos directly, the site also supports embedding videos from YouTube";
|
||||
|
||||
|
||||
"tour_section_6_title_1" = "Audios section, which doesn't exist yet xdddd";
|
||||
"tour_section_6_text_1" = "I would love to do a tutorial on this section, but sunshine Vriska didn't make the music :c";
|
||||
"tour_section_6_title_1" = "Listen to audios";
|
||||
"tour_section_6_text_1" = "Вы можете слушать аудиозаписи в разделе \"Мои Аудиозаписи\".";
|
||||
"tour_section_6_text_2" = "Этот раздел также регулируется настройками приватности.";
|
||||
"tour_section_6_text_3" = "Самые прослушиваемые песни находятся во вкладке \"Популярное\", а недавно загруженные — во вкладке \"Новое\"";
|
||||
"tour_section_6_text_4" = "Найти нужную песню можно в поиске.";
|
||||
"tour_section_6_text_5" = "Чтобы добавить песню в свою коллекцию, наведите на неё и нажмите на плюс.";
|
||||
"tour_section_6_text_6" = "Если вы не можете найти нужную песню, вы можете загрузить её самостоятельно.";
|
||||
"tour_section_6_bottom_text_1" = "<b>Важно:</b> песня не должна нарушать авторские права";
|
||||
"tour_section_6_title_1" = "Создавайте плейлисты";
|
||||
"tour_section_6_text_7" = "Вы можете создавать сборники треков во вкладке \"Мои плейлисты\".";
|
||||
"tour_section_6_text_8" = "Можно также добавлять чужие плейлисты в свою коллекцию.";
|
||||
|
||||
|
||||
"tour_section_7_title_1" = "Follow what your friends write";
|
||||
|
|
|
@ -1298,6 +1298,12 @@
|
|||
"playlist_not_bookmarked" = "Плейлиста нет в вашей коллекции.";
|
||||
"invalid_cover_photo" = "Не удалось сохранить обложку плейлиста.";
|
||||
"not_a_photo" = "Загруженный файл не похож на фотографию.";
|
||||
"file_too_big" = "Файл слишком большой.";
|
||||
"file_loaded_partially" = "Файл загрузился частично.";
|
||||
"file_not_uploaded" = "Не удалось загрузить файл.";
|
||||
"error_code" = "Код ошибки: $1.";
|
||||
"ffmpeg_timeout" = "Превышено время ожидания обработки ffmpeg. Попробуйте загрузить файл снова.";
|
||||
"ffmpeg_not_installed" = "Не удалось обработать файл. Похоже, на сервере не установлен ffmpeg.";
|
||||
|
||||
/* Admin actions */
|
||||
|
||||
|
@ -1625,8 +1631,17 @@
|
|||
"tour_section_5_text_3" = "Кроме загрузки видео напрямую, сайт поддерживает и встраивание видео из YouTube";
|
||||
|
||||
|
||||
"tour_section_6_title_1" = "Аудиозаписи, которых пока что нет XD";
|
||||
"tour_section_6_text_1" = "Я был бы очень рад сделать туториал по этому разделу, но солнышко Вриска не сделала музыку";
|
||||
"tour_section_6_title_1" = "Слушайте аудиозаписи";
|
||||
"tour_section_6_text_1" = "Вы можете слушать аудиозаписи в разделе \"Мои Аудиозаписи\".";
|
||||
"tour_section_6_text_2" = "Этот раздел также регулируется настройками приватности.";
|
||||
"tour_section_6_text_3" = "Самые прослушиваемые песни находятся во вкладке \"Популярное\", а недавно загруженные — во вкладке \"Новое\"";
|
||||
"tour_section_6_text_4" = "Найти нужную песню можно в поиске.";
|
||||
"tour_section_6_text_5" = "Чтобы добавить песню в свою коллекцию, наведите на неё и нажмите на плюс.";
|
||||
"tour_section_6_text_6" = "Если вы не можете найти нужную песню, вы можете загрузить её самостоятельно.";
|
||||
"tour_section_6_bottom_text_1" = "<b>Важно:</b> песня не должна нарушать авторские права";
|
||||
"tour_section_6_title_1" = "Создавайте плейлисты";
|
||||
"tour_section_6_text_7" = "Вы можете создавать сборники треков во вкладке \"Мои плейлисты\".";
|
||||
"tour_section_6_text_8" = "Можно также добавлять чужие плейлисты в свою коллекцию.";
|
||||
|
||||
|
||||
"tour_section_7_title_1" = "Следите за тем, что пишут ваши друзья";
|
||||
|
|
Loading…
Reference in a new issue