diff --git a/README.md b/README.md index 152e05bf..934ff0b7 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ If you want, you can add your instance to the list above so that people can regi 1. Install PHP 7.4, web-server, Composer, Node.js, Yarn and [Chandler](https://github.com/openvk/chandler) -* PHP 8.1 is supported too, however it was not tested carefully, so be aware. +* PHP 8 is still being tested; the functionality of the engine on this version of PHP is not yet guaranteed. 2. Install MySQL-compatible database. diff --git a/README_RU.md b/README_RU.md index cc4f672f..7de91c39 100644 --- a/README_RU.md +++ b/README_RU.md @@ -30,7 +30,7 @@ _[English](README.md)_ 1. Установите PHP 7.4, веб-сервер, Composer, Node.js, Yarn и [Chandler](https://github.com/openvk/chandler) -* PHP 8 еще **не** тестировался, поэтому не стоит ожидать, что он будет работать (UPD: он не работает). +* PHP 8 пока ещё тестируется, работоспособность движка на этой версии PHP пока не гарантируется. 2. Установите MySQL-совместимую базу данных. diff --git a/VKAPI/Handlers/Audio.php b/VKAPI/Handlers/Audio.php index 3fa68e72..413a2a3a 100644 --- a/VKAPI/Handlers/Audio.php +++ b/VKAPI/Handlers/Audio.php @@ -1,22 +1,788 @@ fail(0404, "Audio not found"); + else if(!$audio->canBeViewedBy($this->getUser())) + $this->fail(201, "Access denied to audio(" . $audio->getPrettyId() . ")"); + + # рофлан ебало + $privApi = $hash && $GLOBALS["csrfCheck"]; + $audioObj = $audio->toVkApiStruct($this->getUser()); + if(!$privApi) { + $audioObj->manifest = false; + $audioObj->keys = false; + } + + if($need_user) { + $user = (new \openvk\Web\Models\Repositories\Users)->get($audio->getOwner()->getId()); + $audioObj->user = (object) [ + "id" => $user->getId(), + "photo" => $user->getAvatarUrl(), + "name" => $user->getCanonicalName(), + "name_gen" => $user->getCanonicalName(), + ]; + } + + return $audioObj; + } + + private function streamToResponse(EntityStream $es, int $offset, int $count, ?string $hash = NULL): object + { + $items = []; + foreach($es->offsetLimit($offset, $count) as $audio) { + $items[] = $this->toSafeAudioStruct($audio, $hash); + } + + return (object) [ + "count" => sizeof($items), + "items" => $items, + ]; + } + + private function validateGenre(?string& $genre_str, ?int $genre_id): void + { + if(!is_null($genre_str)) { + if(!in_array($genre_str, AEntity::genres)) + $this->fail(8, "Invalid genre_str"); + } else if(!is_null($genre_id)) { + $genre_str = array_flip(AEntity::vkGenres)[$genre_id] ?? NULL; + if(!$genre_str) + $this->fail(8, "Invalid genre ID $genre_id"); + } + } + + private function audioFromAnyId(string $id): ?AEntity + { + $descriptor = explode("_", $id); + if(sizeof($descriptor) === 1) { + if(ctype_digit($descriptor[0])) { + $audio = (new Audios)->get((int) $descriptor[0]); + } else { + $aid = base64_decode($descriptor[0], true); + if(!$aid) + $this->fail(8, "Invalid audio $id"); + + $audio = (new Audios)->get((int) $aid); + } + } else if(sizeof($descriptor) === 2) { + $audio = (new Audios)->getByOwnerAndVID((int) $descriptor[0], (int) $descriptor[1]); + } else { + $this->fail(8, "Invalid audio $id"); + } + + return $audio; + } + + function getById(string $audios, ?string $hash = NULL, int $need_user = 0): object + { + $this->requireUser(); + + $audioIds = array_unique(explode(",", $audios)); + if(sizeof($audioIds) === 1) { + $audio = $this->audioFromAnyId($audioIds[0]); + + return (object) [ + "count" => 1, + "items" => [ + $this->toSafeAudioStruct($audio, $hash, (bool) $need_user), + ], + ]; + } else if(sizeof($audioIds) > 6000) { + $this->fail(1980, "Can't get more than 6000 audios at once"); + } + + $audios = []; + foreach($audioIds as $id) + $audios[] = $this->getById($id, $hash)->items[0]; + + return (object) [ + "count" => sizeof($audios), + "items" => $audios, + ]; + } + + function isLagtrain(string $audio_id): int + { + $this->requireUser(); + + $audio = $this->audioFromAnyId($audio_id); + if(!$audio) + $this->fail(0404, "Audio not found"); + + # Possible information disclosure risks are acceptable :D + return (int) (strpos($audio->getName(), "Lagtrain") !== false); + } + + // TODO stub + function getRecommendations(): object + { + return (object) [ + "count" => 0, + "items" => [], + ]; + } + + function getPopular(?int $genre_id = NULL, ?string $genre_str = NULL, int $offset = 0, int $count = 100, ?string $hash = NULL): object + { + $this->requireUser(); + $this->validateGenre($genre_str, $genre_id); + + $results = (new Audios)->getGlobal(Audios::ORDER_POPULAR, $genre_str); + + return $this->streamToResponse($results, $offset, $count, $hash); + } + + function getFeed(?int $genre_id = NULL, ?string $genre_str = NULL, int $offset = 0, int $count = 100, ?string $hash = NULL): object + { + $this->requireUser(); + $this->validateGenre($genre_str, $genre_id); + + $results = (new Audios)->getGlobal(Audios::ORDER_NEW, $genre_str); + + return $this->streamToResponse($results, $offset, $count, $hash); + } + + function search(string $q, int $auto_complete = 0, int $lyrics = 0, int $performer_only = 0, int $sort = 2, int $search_own = 0, int $offset = 0, int $count = 30, ?string $hash = NULL): object + { + $this->requireUser(); + + if(($auto_complete + $search_own) != 0) + $this->fail(10, "auto_complete and search_own are not supported"); + else if($count > 300 || $count < 1) + $this->fail(8, "count is invalid: $count"); + + $results = (new Audios)->search($q, $sort, (bool) $performer_only, (bool) $lyrics); + + return $this->streamToResponse($results, $offset, $count, $hash); + } + + function getCount(int $owner_id, int $uploaded_only = 0): int + { + $this->requireUser(); + + if($owner_id < 0) { + $owner_id *= -1; + $group = (new Clubs)->get($owner_id); + if(!$group) + $this->fail(0404, "Group not found"); + + return (new Audios)->getClubCollectionSize($group); + } + + $user = (new \openvk\Web\Models\Repositories\Users)->get($owner_id); + if(!$user) + $this->fail(0404, "User not found"); + + if(!$user->getPrivacyPermission("audios.read", $this->getUser())) + $this->fail(15, "Access denied"); + + if($uploaded_only) { + return DatabaseConnection::i()->getContext()->table("audios") + ->where([ + "deleted" => false, + "owner" => $owner_id, + ])->count(); + } + + return (new Audios)->getUserCollectionSize($user); + } + + function get(int $owner_id = 0, int $album_id = 0, string $audio_ids = '', int $need_user = 1, int $offset = 0, int $count = 100, int $uploaded_only = 0, int $need_seed = 0, ?string $shuffle_seed = NULL, int $shuffle = 0, ?string $hash = NULL): object { - $serverUrl = ovk_scheme(true) . $_SERVER["SERVER_NAME"]; - - return (object) [ - "count" => 1, - "items" => [(object) [ - "id" => 1, - "owner_id" => 1, - "artist" => "В ОВК ПОКА НЕТ МУЗЫКИ", - "title" => "ЖДИТЕ :)))", - "duration" => 22, - "url" => $serverUrl . "/assets/packages/static/openvk/audio/nomusic.mp3" - ]] - ]; + $this->requireUser(); + + $shuffleSeed = NULL; + $shuffleSeedStr = NULL; + if($shuffle == 1) { + if(!$shuffle_seed) { + if($need_seed == 1) { + $shuffleSeed = openssl_random_pseudo_bytes(6); + $shuffleSeedStr = base64_encode($shuffleSeed); + $shuffleSeed = hexdec(bin2hex($shuffleSeed)); + } else { + $hOffset = ((int) date("i") * 60) + (int) date("s"); + $thisHour = time() - $hOffset; + $shuffleSeed = $thisHour + $this->getUser()->getId(); + $shuffleSeedStr = base64_encode(hex2bin(dechex($shuffleSeed))); + } + } else { + $shuffleSeed = hexdec(bin2hex(base64_decode($shuffle_seed))); + $shuffleSeedStr = $shuffle_seed; + } + } + + if($album_id != 0) { + $album = (new Audios)->getPlaylist($album_id); + if(!$album) + $this->fail(0404, "album_id invalid"); + else if(!$album->canBeViewedBy($this->getUser())) + $this->fail(600, "Can't open this album for reading"); + + $songs = []; + $list = $album->getAudios($offset, $count, $shuffleSeed); + + foreach($list as $song) + $songs[] = $this->toSafeAudioStruct($song, $hash, $need_user == 1); + + $response = (object) [ + "count" => sizeof($songs), + "items" => $songs, + ]; + if(!is_null($shuffleSeed)) + $response->shuffle_seed = $shuffleSeedStr; + + return $response; + } + + if(!empty($audio_ids)) { + $audio_ids = explode(",", $audio_ids); + if(!$audio_ids) + $this->fail(10, "Audio::get@L0d186:explode(string): Unknown error"); + else if(sizeof($audio_ids) < 1) + $this->fail(8, "Invalid audio_ids syntax"); + + if(!is_null($shuffleSeed)) + $audio_ids = knuth_shuffle($audio_ids, $shuffleSeed); + + $obj = $this->getById(implode(",", $audio_ids), $hash, $need_user); + if(!is_null($shuffleSeed)) + $obj->shuffle_seed = $shuffleSeedStr; + + return $obj; + } + + $dbCtx = DatabaseConnection::i()->getContext(); + if($uploaded_only == 1) { + if($owner_id <= 0) + $this->fail(8, "uploaded_only can only be used with owner_id > 0"); + + $user = (new \openvk\Web\Models\Repositories\Users)->get($owner_id); + + if(!$user) + $this->fail(0602, "Invalid user"); + + if(!$user->getPrivacyPermission("audios.read", $this->getUser())) + $this->fail(15, "Access denied: this user chose to hide his audios"); + + if(!is_null($shuffleSeed)) { + $audio_ids = []; + $query = $dbCtx->table("audios")->select("virtual_id")->where([ + "owner" => $owner_id, + "deleted" => 0, + ]); + + foreach($query as $res) + $audio_ids[] = $res->virtual_id; + + $audio_ids = knuth_shuffle($audio_ids, $shuffleSeed); + $audio_ids = array_slice($audio_ids, $offset, $count); + $audio_q = ""; # audio.getById query + foreach($audio_ids as $aid) + $audio_q .= ",$owner_id" . "_$aid"; + + $obj = $this->getById(substr($audio_q, 1), $hash, $need_user); + $obj->shuffle_seed = $shuffleSeedStr; + + return $obj; + } + + $res = (new Audios)->getByUploader((new \openvk\Web\Models\Repositories\Users)->get($owner_id)); + + return $this->streamToResponse($res, $offset, $count, $hash, $need_user); + } + + $query = $dbCtx->table("audio_relations")->select("audio")->where("entity", $owner_id); + if(!is_null($shuffleSeed)) { + $audio_ids = []; + foreach($query as $aid) + $audio_ids[] = $aid->audio; + + $audio_ids = knuth_shuffle($audio_ids, $shuffleSeed); + $audio_ids = array_slice($audio_ids, $offset, $count); + $audio_q = ""; + foreach($audio_ids as $aid) + $audio_q .= ",$aid"; + + $obj = $this->getById(substr($audio_q, 1), $hash, $need_user); + $obj->shuffle_seed = $shuffleSeedStr; + + return $obj; + } + + $items = []; + + if($owner_id > 0) { + $user = (new \openvk\Web\Models\Repositories\Users)->get($owner_id); + + if(!$user) + $this->fail(50, "Invalid user"); + + if(!$user->getPrivacyPermission("audios.read", $this->getUser())) + $this->fail(15, "Access denied: this user chose to hide his audios"); + } + + $audios = (new Audios)->getByEntityID($owner_id, $offset, $count); + foreach($audios as $audio) + $items[] = $this->toSafeAudioStruct($audio, $hash, $need_user == 1); + + return (object) [ + "count" => sizeof($items), + "items" => $items, + ]; } + + function getLyrics(int $lyrics_id): object + { + $this->requireUser(); + + $audio = (new Audios)->get($lyrics_id); + if(!$audio || !$audio->getLyrics()) + $this->fail(0404, "Not found"); + + if(!$audio->canBeViewedBy($this->getUser())) + $this->fail(201, "Access denied to lyrics"); + + return (object) [ + "lyrics_id" => $lyrics_id, + "text" => preg_replace("%\r\n?%", "\n", $audio->getLyrics()), + ]; + } + + function beacon(int $aid, ?int $gid = NULL): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $audio = (new Audios)->get($aid); + if(!$audio) + $this->fail(0404, "Not Found"); + else if(!$audio->canBeViewedBy($this->getUser())) + $this->fail(201, "Insufficient permissions to listen this audio"); + + $group = NULL; + if(!is_null($gid)) { + $group = (new Clubs)->get($gid); + if(!$group) + $this->fail(0404, "Not Found"); + else if(!$group->canBeModifiedBy($this->getUser())) + $this->fail(203, "Insufficient rights to this group"); + } + + return (int) $audio->listen($group ?? $this->getUser()); + } + + function setBroadcast(string $audio, string $target_ids): array + { + $this->requireUser(); + + [$owner, $aid] = explode("_", $audio); + $song = (new Audios)->getByOwnerAndVID((int) $owner, (int) $aid); + $ids = []; + foreach(explode(",", $target_ids) as $id) { + $id = (int) $id; + if($id > 0) { + if ($id != $this->getUser()->getId()) { + $this->fail(600, "Can't listen on behalf of $id"); + } else { + $ids[] = $id; + $this->beacon($song->getId()); + continue; + } + } + + $group = (new Clubs)->get($id * -1); + if(!$group) + $this->fail(0404, "Not Found"); + else if(!$group->canBeModifiedBy($this->getUser())) + $this->fail(203,"Insufficient rights to this group"); + + $ids[] = $id; + $this->beacon($song ? $song->getId() : 0, $id * -1); + } + + return $ids; + } + + function getBroadcastList(string $filter = "all", int $active = 0, ?string $hash = NULL): object + { + $this->requireUser(); + + if(!in_array($filter, ["all", "friends", "groups"])) + $this->fail(8, "Invalid filter $filter"); + + $broadcastList = $this->getUser()->getBroadcastList($filter); + $items = []; + foreach($broadcastList as $res) { + $struct = $res->toVkApiStruct(); + $status = $res->getCurrentAudioStatus(); + + $struct->status_audio = $status ? $this->toSafeAudioStruct($status) : NULL; + $items[] = $struct; + } + + return (object) [ + "count" => sizeof($items), + "items" => $items, + ]; + } + + function edit(int $owner_id, int $audio_id, ?string $artist = NULL, ?string $title = NULL, ?string $text = NULL, ?int $genre_id = NULL, ?string $genre_str = NULL, int $no_search = 0): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $audio = (new Audios)->getByOwnerAndVID($owner_id, $audio_id); + if(!$audio) + $this->fail(0404, "Not Found"); + else if(!$audio->canBeModifiedBy($this->getUser())) + $this->fail(201, "Insufficient permissions to edit this audio"); + + if(!is_null($genre_id)) { + $genre = array_flip(AEntity::vkGenres)[$genre_id] ?? NULL; + if(!$genre) + $this->fail(8, "Invalid genre ID $genre_id"); + + $audio->setGenre($genre); + } else if(!is_null($genre_str)) { + if(!in_array($genre_str, AEntity::genres)) + $this->fail(8, "Invalid genre ID $genre_str"); + + $audio->setGenre($genre_str); + } + + $lyrics = 0; + if(!is_null($text)) { + $audio->setLyrics($text); + $lyrics = $audio->getId(); + } + + if(!is_null($artist)) + $audio->setPerformer($artist); + + if(!is_null($title)) + $audio->setName($title); + + $audio->setSearchability(!((bool) $no_search)); + $audio->setEdited(time()); + $audio->save(); + + return $lyrics; + } + + function add(int $audio_id, int $owner_id, ?int $group_id = NULL, ?int $album_id = NULL): string + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + if(!is_null($album_id)) + $this->fail(10, "album_id not implemented"); + + // TODO get rid of dups + $to = $this->getUser(); + if(!is_null($group_id)) { + $group = (new Clubs)->get($group_id); + if(!$group) + $this->fail(0404, "Invalid group_id"); + else if(!$group->canBeModifiedBy($this->getUser())) + $this->fail(203, "Insufficient rights to this group"); + + $to = $group; + } + + $audio = (new Audios)->getByOwnerAndVID($owner_id, $audio_id); + if(!$audio) + $this->fail(0404, "Not found"); + else if(!$audio->canBeViewedBy($this->getUser())) + $this->fail(201, "Access denied to audio(owner=$owner_id, vid=$audio_id)"); + + try { + $audio->add($to); + } catch(\OverflowException $ex) { + $this->fail(300, "Album is full"); + } + + return $audio->getPrettyId(); + } + + function delete(int $audio_id, int $owner_id, ?int $group_id = NULL): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $from = $this->getUser(); + if(!is_null($group_id)) { + $group = (new Clubs)->get($group_id); + if(!$group) + $this->fail(0404, "Invalid group_id"); + else if(!$group->canBeModifiedBy($this->getUser())) + $this->fail(203, "Insufficient rights to this group"); + + $from = $group; + } + + $audio = (new Audios)->getByOwnerAndVID($owner_id, $audio_id); + if(!$audio) + $this->fail(0404, "Not found"); + + $audio->remove($from); + + return 1; + } + + function restore(int $audio_id, int $owner_id, ?int $group_id = NULL, ?string $hash = NULL): object + { + $this->requireUser(); + + $vid = $this->add($audio_id, $owner_id, $group_id); + + return $this->getById($vid, $hash)->items[0]; + } + + function getAlbums(int $owner_id = 0, int $offset = 0, int $count = 50, int $drop_private = 1): object + { + $this->requireUser(); + + $owner_id = $owner_id == 0 ? $this->getUser()->getId() : $owner_id; + $playlists = []; + + if($owner_id > 0 && $owner_id != $this->getUser()->getId()) { + $user = (new \openvk\Web\Models\Repositories\Users)->get($owner_id); + + if(!$user->getPrivacyPermission("audios.read", $this->getUser())) + $this->fail(50, "Access to playlists denied"); + } + + foreach((new Audios)->getPlaylistsByEntityId($owner_id, $offset, $count) as $playlist) { + if(!$playlist->canBeViewedBy($this->getUser())) { + if($drop_private == 1) + continue; + + $playlists[] = NULL; + continue; + } + + $playlists[] = $playlist->toVkApiStruct($this->getUser()); + } + + return (object) [ + "count" => sizeof($playlists), + "items" => $playlists, + ]; + } + + function searchAlbums(string $query, int $offset = 0, int $limit = 25, int $drop_private = 0): object + { + $this->requireUser(); + + $playlists = []; + $search = (new Audios)->searchPlaylists($query)->offsetLimit($offset, $limit); + foreach($search as $playlist) { + if(!$playlist->canBeViewedBy($this->getUser())) { + if($drop_private == 0) + $playlists[] = NULL; + + continue; + } + + $playlists[] = $playlist->toVkApiStruct($this->getUser()); + } + + return (object) [ + "count" => sizeof($playlists), + "items" => $playlists, + ]; + } + + function addAlbum(string $title, ?string $description = NULL, int $group_id = 0): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $group = NULL; + if($group_id != 0) { + $group = (new Clubs)->get($group_id); + if(!$group) + $this->fail(0404, "Invalid group_id"); + else if(!$group->canBeModifiedBy($this->getUser())) + $this->fail(600, "Insufficient rights to this group"); + } + + $album = new Playlist; + $album->setName($title); + if(!is_null($group)) + $album->setOwner($group_id * -1); + else + $album->setOwner($this->getUser()->getId()); + + if(!is_null($description)) + $album->setDescription($description); + + $album->save(); + if(!is_null($group)) + $album->bookmark($group); + else + $album->bookmark($this->getUser()); + + return $album->getId(); + } + + function editAlbum(int $album_id, ?string $title = NULL, ?string $description = NULL): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios)->getPlaylist($album_id); + if(!$album) + $this->fail(0404, "Album not found"); + else if(!$album->canBeModifiedBy($this->getUser())) + $this->fail(600, "Insufficient rights to this album"); + + if(!is_null($title)) + $album->setName($title); + + if(!is_null($description)) + $album->setDescription($description); + + $album->setEdited(time()); + $album->save(); + + return (int) !(!$title && !$description); + } + + function deleteAlbum(int $album_id): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios)->getPlaylist($album_id); + if(!$album) + $this->fail(0404, "Album not found"); + else if(!$album->canBeModifiedBy($this->getUser())) + $this->fail(600, "Insufficient rights to this album"); + + $album->delete(); + + return 1; + } + + function moveToAlbum(int $album_id, string $audio_ids): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios)->getPlaylist($album_id); + if(!$album) + $this->fail(0404, "Album not found"); + else if(!$album->canBeModifiedBy($this->getUser())) + $this->fail(600, "Insufficient rights to this album"); + + $audios = []; + $audio_ids = array_unique(explode(",", $audio_ids)); + if(sizeof($audio_ids) < 1 || sizeof($audio_ids) > 1000) + $this->fail(8, "audio_ids must contain at least 1 audio and at most 1000"); + + foreach($audio_ids as $audio_id) { + $audio = $this->audioFromAnyId($audio_id); + if(!$audio) + continue; + else if(!$audio->canBeViewedBy($this->getUser())) + continue; + + $audios[] = $audio; + } + + if(sizeof($audios) < 1) + return 0; + + $res = 1; + try { + foreach ($audios as $audio) + $res = min($res, (int) $album->add($audio)); + } catch(\OutOfBoundsException $ex) { + return 0; + } + + return $res; + } + + function removeFromAlbum(int $album_id, string $audio_ids): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios)->getPlaylist($album_id); + if(!$album) + $this->fail(0404, "Album not found"); + else if(!$album->canBeModifiedBy($this->getUser())) + $this->fail(600, "Insufficient rights to this album"); + + $audios = []; + $audio_ids = array_unique(explode(",", $audio_ids)); + if(sizeof($audio_ids) < 1 || sizeof($audio_ids) > 1000) + $this->fail(8, "audio_ids must contain at least 1 audio and at most 1000"); + + foreach($audio_ids as $audio_id) { + $audio = $this->audioFromAnyId($audio_id); + if(!$audio) + continue; + else if($audio->canBeViewedBy($this->getUser())) + continue; + + $audios[] = $audio; + } + + if(sizeof($audios) < 1) + return 0; + + foreach($audios as $audio) + $album->remove($audio); + + return 1; + } + + function copyToAlbum(int $album_id, string $audio_ids): int + { + return $this->moveToAlbum($album_id, $audio_ids); + } + + function bookmarkAlbum(int $id): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios)->getPlaylist($id); + if(!$album) + $this->fail(0404, "Not found"); + + if(!$album->canBeViewedBy($this->getUser())) + $this->fail(600, "Access error"); + + return (int) $album->bookmark($this->getUser()); + } + + function unBookmarkAlbum(int $id): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios)->getPlaylist($id); + if(!$album) + $this->fail(0404, "Not found"); + + if(!$album->canBeViewedBy($this->getUser())) + $this->fail(600, "Access error"); + + return (int) $album->unbookmark($this->getUser()); + } } diff --git a/VKAPI/Handlers/Friends.php b/VKAPI/Handlers/Friends.php index f7873520..59cf5679 100644 --- a/VKAPI/Handlers/Friends.php +++ b/VKAPI/Handlers/Friends.php @@ -4,7 +4,7 @@ use openvk\Web\Models\Repositories\Users as UsersRepo; final class Friends extends VKAPIRequestHandler { - function get(int $user_id, string $fields = "", int $offset = 0, int $count = 100): object + function get(int $user_id = 0, string $fields = "", int $offset = 0, int $count = 100): object { $i = 0; $offset++; @@ -13,6 +13,14 @@ final class Friends extends VKAPIRequestHandler $users = new UsersRepo; $this->requireUser(); + + if ($user_id == 0) { + $user_id = $this->getUser()->getId(); + } + + if (is_null($users->get($user_id))) { + $this->fail(100, "One of the parameters specified was missing or invalid"); + } foreach($users->get($user_id)->getFriends($offset, $count) as $friend) { $friends[$i] = $friend->getId(); diff --git a/VKAPI/Handlers/Groups.php b/VKAPI/Handlers/Groups.php index 9cc6a08c..007b68ea 100644 --- a/VKAPI/Handlers/Groups.php +++ b/VKAPI/Handlers/Groups.php @@ -292,7 +292,8 @@ final class Groups extends VKAPIRequestHandler int $topics = NULL, int $adminlist = NULL, int $topicsAboveWall = NULL, - int $hideFromGlobalFeed = NULL) + int $hideFromGlobalFeed = NULL, + int $audio = NULL) { $this->requireUser(); $this->willExecuteWriteAction(); @@ -303,17 +304,22 @@ final class Groups extends VKAPIRequestHandler if(!$club || !$club->canBeModifiedBy($this->getUser())) $this->fail(15, "You can't modify this group."); if(!empty($screen_name) && !$club->setShortcode($screen_name)) $this->fail(103, "Invalid shortcode."); - !is_null($title) ? $club->setName($title) : NULL; - !is_null($description) ? $club->setAbout($description) : NULL; - !is_null($screen_name) ? $club->setShortcode($screen_name) : NULL; - !is_null($website) ? $club->setWebsite((!parse_url($website, PHP_URL_SCHEME) ? "https://" : "") . $website) : NULL; - !is_null($wall) ? $club->setWall($wall) : NULL; - !is_null($topics) ? $club->setEveryone_Can_Create_Topics($topics) : NULL; - !is_null($adminlist) ? $club->setAdministrators_List_Display($adminlist) : NULL; - !is_null($topicsAboveWall) ? $club->setDisplay_Topics_Above_Wall($topicsAboveWall) : NULL; - !is_null($hideFromGlobalFeed) ? $club->setHide_From_Global_Feed($hideFromGlobalFeed) : NULL; + !empty($title) ? $club->setName($title) : NULL; + !empty($description) ? $club->setAbout($description) : NULL; + !empty($screen_name) ? $club->setShortcode($screen_name) : NULL; + !empty($website) ? $club->setWebsite((!parse_url($website, PHP_URL_SCHEME) ? "https://" : "") . $website) : NULL; + !empty($wall) ? $club->setWall($wall) : NULL; + !empty($topics) ? $club->setEveryone_Can_Create_Topics($topics) : NULL; + !empty($adminlist) ? $club->setAdministrators_List_Display($adminlist) : NULL; + !empty($topicsAboveWall) ? $club->setDisplay_Topics_Above_Wall($topicsAboveWall) : NULL; + !empty($hideFromGlobalFeed) ? $club->setHide_From_Global_Feed($hideFromGlobalFeed) : NULL; + in_array($audio, [0, 1]) ? $club->setEveryone_can_upload_audios($audio) : NULL; - $club->save(); + try { + $club->save(); + } catch(\TypeError $e) { + $this->fail(8, "Nothing changed"); + } return 1; } @@ -370,7 +376,7 @@ final class Groups extends VKAPIRequestHandler $arr->items[$i]->can_see_all_posts = 1; break; case "can_see_audio": - $arr->items[$i]->can_see_audio = 0; + $arr->items[$i]->can_see_audio = 1; break; case "can_write_private_message": $arr->items[$i]->can_write_private_message = 0; @@ -469,7 +475,7 @@ final class Groups extends VKAPIRequestHandler "wall" => $club->canPost() == true ? 1 : 0, "photos" => 1, "video" => 0, - "audio" => 0, + "audio" => $club->isEveryoneCanUploadAudios() ? 1 : 0, "docs" => 0, "topics" => $club->isEveryoneCanCreateTopics() == true ? 1 : 0, "wiki" => 0, diff --git a/VKAPI/Handlers/Photos.php b/VKAPI/Handlers/Photos.php index e3e9abac..bb1a22f0 100644 --- a/VKAPI/Handlers/Photos.php +++ b/VKAPI/Handlers/Photos.php @@ -432,13 +432,11 @@ final class Photos extends VKAPIRequestHandler if(empty($photo_ids)) { $album = (new Albums)->getAlbumByOwnerAndId($owner_id, $album_id); - if(!$album->getOwner()->getPrivacyPermission('photos.read', $this->getUser())) { - $this->fail(21, "This user chose to hide his albums."); - } - - if(!$album || $album->isDeleted()) { + if(!$album || $album->isDeleted()) $this->fail(21, "Invalid album"); - } + + if(!$album->getOwner()->getPrivacyPermission('photos.read', $this->getUser())) + $this->fail(21, "This user chose to hide his albums."); $photos = array_slice(iterator_to_array($album->getPhotos(1, $count + $offset)), $offset); $res["count"] = sizeof($photos); @@ -456,8 +454,7 @@ final class Photos extends VKAPIRequestHandler "items" => [] ]; - foreach($photos as $photo) - { + foreach($photos as $photo) { $id = explode("_", $photo); $phot = (new PhotosRepo)->getByOwnerAndVID((int)$id[0], (int)$id[1]); diff --git a/VKAPI/Handlers/Status.php b/VKAPI/Handlers/Status.php index 843f42bd..a1b104a2 100644 --- a/VKAPI/Handlers/Status.php +++ b/VKAPI/Handlers/Status.php @@ -8,13 +8,23 @@ final class Status extends VKAPIRequestHandler function get(int $user_id = 0, int $group_id = 0) { $this->requireUser(); - if($user_id == 0 && $group_id == 0) { - return $this->getUser()->getStatus(); - } else { - if($group_id > 0) - $this->fail(501, "Group statuses are not implemented"); - else - return (new UsersRepo)->get($user_id)->getStatus(); + + if($user_id == 0 && $group_id == 0) + $user_id = $this->getUser()->getId(); + + if($group_id > 0) + $this->fail(501, "Group statuses are not implemented"); + else { + $user = (new UsersRepo)->get($user_id); + $audioStatus = $user->getCurrentAudioStatus(); + if($audioStatus) { + return [ + "status" => $user->getStatus(), + "audio" => $audioStatus->toVkApiStruct(), + ]; + } + + return $user->getStatus(); } } diff --git a/VKAPI/Handlers/Users.php b/VKAPI/Handlers/Users.php index 6d67ed39..d1c29705 100644 --- a/VKAPI/Handlers/Users.php +++ b/VKAPI/Handlers/Users.php @@ -37,8 +37,8 @@ final class Users extends VKAPIRequestHandler } else if($usr->isBanned()) { $response[$i] = (object)[ "id" => $usr->getId(), - "first_name" => $usr->getFirstName(), - "last_name" => $usr->getLastName(), + "first_name" => $usr->getFirstName(true), + "last_name" => $usr->getLastName(true), "deactivated" => "banned", "ban_reason" => $usr->getBanReason() ]; @@ -47,8 +47,8 @@ final class Users extends VKAPIRequestHandler } else { $response[$i] = (object)[ "id" => $usr->getId(), - "first_name" => $usr->getFirstName(), - "last_name" => $usr->getLastName(), + "first_name" => $usr->getFirstName(true), + "last_name" => $usr->getLastName(true), "is_closed" => false, "can_access_closed" => true, ]; @@ -96,6 +96,12 @@ final class Users extends VKAPIRequestHandler case "status": if($usr->getStatus() != NULL) $response[$i]->status = $usr->getStatus(); + + $audioStatus = $usr->getCurrentAudioStatus(); + + if($audioStatus) + $response[$i]->status_audio = $audioStatus->toVkApiStruct(); + break; case "screen_name": if($usr->getShortCode() != NULL) @@ -160,6 +166,18 @@ final class Users extends VKAPIRequestHandler case "interests": $response[$i]->interests = $usr->getInterests(); break; + case "quotes": + $response[$i]->interests = $usr->getFavoriteQuote(); + break; + case "email": + $response[$i]->interests = $usr->getEmail(); + break; + case "telegram": + $response[$i]->interests = $usr->getTelegram(); + break; + case "about": + $response[$i]->interests = $usr->getDescription(); + break; case "rating": $response[$i]->rating = $usr->getRating(); break; diff --git a/VKAPI/Handlers/Wall.php b/VKAPI/Handlers/Wall.php index 61c75703..907f06fa 100644 --- a/VKAPI/Handlers/Wall.php +++ b/VKAPI/Handlers/Wall.php @@ -16,6 +16,7 @@ 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\Polls as PollsRepo; +use openvk\Web\Models\Repositories\Audios as AudiosRepo; final class Wall extends VKAPIRequestHandler { @@ -59,6 +60,11 @@ final class Wall extends VKAPIRequestHandler $attachments[] = $attachment->getApiStructure(); } else if ($attachment instanceof \openvk\Web\Models\Entities\Note) { $attachments[] = $attachment->toVkApiStruct(); + } else if ($attachment instanceof \openvk\Web\Models\Entities\Audio) { + $attachments[] = [ + "type" => "audio", + "audio" => $attachment->toVkApiStruct($this->getUser()), + ]; } else if ($attachment instanceof \openvk\Web\Models\Entities\Post) { $repostAttachments = []; @@ -234,6 +240,11 @@ final class Wall extends VKAPIRequestHandler $attachments[] = $attachment->getApiStructure(); } else if ($attachment instanceof \openvk\Web\Models\Entities\Note) { $attachments[] = $attachment->toVkApiStruct(); + } else if ($attachment instanceof \openvk\Web\Models\Entities\Audio) { + $attachments[] = [ + "type" => "audio", + "audio" => $attachment->toVkApiStruct($this->getUser()) + ]; } else if ($attachment instanceof \openvk\Web\Models\Entities\Post) { $repostAttachments = []; @@ -458,6 +469,9 @@ final class Wall extends VKAPIRequestHandler $attachmentType = "note"; elseif(str_contains($attac, "poll")) $attachmentType = "poll"; + elseif(str_contains($attac, "audio")) + $attachmentType = "audio"; + else $this->fail(205, "Unknown attachment type"); @@ -499,6 +513,10 @@ final class Wall extends VKAPIRequestHandler $this->fail(100, "Poll does not exist"); if($attacc->getOwner()->getId() != $this->getUser()->getId()) $this->fail(43, "You do not have access to this poll"); + } elseif($attachmentType == "audio") { + $attacc = (new AudiosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); + if(!$attacc || $attacc->isDeleted()) + $this->fail(100, "Audio does not exist"); $post->attach($attacc); } @@ -579,6 +597,11 @@ final class Wall extends VKAPIRequestHandler $attachments[] = $this->getApiPhoto($attachment); } elseif($attachment instanceof \openvk\Web\Models\Entities\Note) { $attachments[] = $attachment->toVkApiStruct(); + } elseif($attachment instanceof \openvk\Web\Models\Entities\Audio) { + $attachments[] = [ + "type" => "audio", + "audio" => $attachment->toVkApiStruct($this->getUser()), + ]; } } @@ -638,6 +661,9 @@ final class Wall extends VKAPIRequestHandler $comment = (new CommentsRepo)->get($comment_id); # один хуй айди всех комментов общий + if(!$comment || $comment->isDeleted()) + $this->fail(100, "Invalid comment"); + $profiles = []; $attachments = []; @@ -645,6 +671,11 @@ final class Wall extends VKAPIRequestHandler foreach($comment->getChildren() as $attachment) { if($attachment instanceof \openvk\Web\Models\Entities\Photo) { $attachments[] = $this->getApiPhoto($attachment); + } elseif($attachment instanceof \openvk\Web\Models\Entities\Audio) { + $attachments[] = [ + "type" => "audio", + "audio" => $attachment->toVkApiStruct($this->getUser()), + ]; } } @@ -736,6 +767,8 @@ final class Wall extends VKAPIRequestHandler $attachmentType = "photo"; elseif(str_contains($attac, "video")) $attachmentType = "video"; + elseif(str_contains($attac, "audio")) + $attachmentType = "audio"; else $this->fail(205, "Unknown attachment type"); @@ -761,6 +794,12 @@ final class Wall extends VKAPIRequestHandler if(!$attacc->getOwner()->getPrivacyPermission('videos.read', $this->getUser())) $this->fail(11, "Access to video denied"); + $comment->attach($attacc); + } elseif($attachmentType == "audio") { + $attacc = (new AudiosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); + if(!$attacc || $attacc->isDeleted()) + $this->fail(100, "Audio does not exist"); + $comment->attach($attacc); } } @@ -883,7 +922,7 @@ final class Wall extends VKAPIRequestHandler return [ "type" => "photo", "photo" => [ - "album_id" => $attachment->getAlbum() ? $attachment->getAlbum()->getId() : NULL, + "album_id" => $attachment->getAlbum() ? $attachment->getAlbum()->getId() : 0, "date" => $attachment->getPublicationTime()->timestamp(), "id" => $attachment->getVirtualId(), "owner_id" => $attachment->getOwner()->getId(), diff --git a/Web/Models/Entities/APIToken.php b/Web/Models/Entities/APIToken.php index f5744ec3..0cc53148 100644 --- a/Web/Models/Entities/APIToken.php +++ b/Web/Models/Entities/APIToken.php @@ -48,7 +48,7 @@ class APIToken extends RowModel $this->delete(); } - function save(): void + function save(?bool $log = false): void { if(is_null($this->getRecord())) $this->stateChanges("secret", bin2hex(openssl_random_pseudo_bytes(36))); diff --git a/Web/Models/Entities/Audio.php b/Web/Models/Entities/Audio.php new file mode 100644 index 00000000..11d8502b --- /dev/null +++ b/Web/Models/Entities/Audio.php @@ -0,0 +1,469 @@ + 1, + "Pop" => 2, + "Rap" => 3, + "Hip-Hop" => 3, # VK API lists №3 as Rap & Hip-Hop, but these genres are distinct in OpenVK + "Easy Listening" => 4, + "House" => 5, + "Dance" => 5, + "Instrumental" => 6, + "Metal" => 7, + "Alternative" => 21, + "Dubstep" => 8, + "Jazz" => 1001, + "Blues" => 1001, + "Drum & Bass" => 10, + "Trance" => 11, + "Chanson" => 12, + "Ethnic" => 13, + "Acoustic" => 14, + "Vocal" => 14, + "Reggae" => 15, + "Classical" => 16, + "Indie Pop" => 17, + "Speech" => 19, + "Disco" => 22, + "Other" => 18, + ]; + + private function fileLength(string $filename): int + { + if(!Shell::commandAvailable("ffmpeg") || !Shell::commandAvailable("ffprobe")) + throw new \Exception(); + + $error = NULL; + $streams = Shell::ffprobe("-i", $filename, "-show_streams", "-select_streams a", "-loglevel error")->execute($error); + if($error !== 0) + throw new \DomainException("$filename is not recognized as media container"); + else if(empty($streams) || ctype_space($streams)) + throw new \DomainException("$filename does not contain any audio streams"); + + $vstreams = Shell::ffprobe("-i", $filename, "-show_streams", "-select_streams v", "-loglevel error")->execute($error); + + # check if audio has cover (attached_pic) + preg_match("%attached_pic=([0-1])%", $vstreams, $hasCover); + if(!empty($vstreams) && !ctype_space($vstreams) && ((int)($hasCover[1]) !== 1)) + throw new \DomainException("$filename is a video"); + + $durations = []; + preg_match_all('%duration=([0-9\.]++)%', $streams, $durations); + if(sizeof($durations[1]) === 0) + throw new \DomainException("$filename does not contain any meaningful audio streams"); + + $length = 0; + foreach($durations[1] as $duration) { + $duration = floatval($duration); + if($duration < 1.0 || $duration > 65536.0) + throw new \DomainException("$filename does not contain any meaningful audio streams"); + else + $length = max($length, $duration); + } + + return (int) round($length, 0, PHP_ROUND_HALF_EVEN); + } + + /** + * @throws \Exception + */ + protected function saveFile(string $filename, string $hash): bool + { + $duration = $this->fileLength($filename); + + $kid = openssl_random_pseudo_bytes(16); + $key = openssl_random_pseudo_bytes(16); + $tok = openssl_random_pseudo_bytes(28); + $ss = ceil($duration / 15); + + $this->stateChanges("kid", $kid); + $this->stateChanges("key", $key); + $this->stateChanges("token", $tok); + $this->stateChanges("segment_size", $ss); + $this->stateChanges("length", $duration); + + try { + $args = [ + str_replace("enabled", "available", OPENVK_ROOT), + str_replace("enabled", "available", $this->getBaseDir()), + $hash, + $filename, + + bin2hex($kid), + bin2hex($key), + bin2hex($tok), + $ss, + ]; + + if(Shell::isPowershell()) { + Shell::powershell("-executionpolicy bypass", "-File", __DIR__ . "/../shell/processAudio.ps1", ...$args) + ->start(); + } else { + Shell::bash(__DIR__ . "/../shell/processAudio.sh", ...$args) // Pls workkkkk + ->start(); // idk, not tested :") + } + + # Wait until processAudio will consume the file + $start = time(); + while(file_exists($filename)) + if(time() - $start > 5) + throw new \RuntimeException("Timed out waiting FFMPEG"); + + } catch(UnknownCommandException $ucex) { + exit(OPENVK_ROOT_CONF["openvk"]["debug"] ? "bash/pwsh is not installed" : VIDEOS_FRIENDLY_ERROR); + } + + return true; + } + + function getTitle(): string + { + return $this->getRecord()->name; + } + + function getPerformer(): string + { + return $this->getRecord()->performer; + } + + function getName(): string + { + return $this->getPerformer() . " — " . $this->getTitle(); + } + + function getGenre(): ?string + { + return $this->getRecord()->genre; + } + + function getLyrics(): ?string + { + return !is_null($this->getRecord()->lyrics) ? htmlspecialchars($this->getRecord()->lyrics, ENT_DISALLOWED | ENT_XHTML) : NULL; + } + + function getLength(): int + { + return $this->getRecord()->length; + } + + function getFormattedLength(): string + { + $len = $this->getLength(); + $mins = floor($len / 60); + $secs = $len - ($mins * 60); + + return ( + str_pad((string) $mins, 2, "0", STR_PAD_LEFT) + . ":" . + str_pad((string) $secs, 2, "0", STR_PAD_LEFT) + ); + } + + function getSegmentSize(): float + { + return $this->getRecord()->segment_size; + } + + function getListens(): int + { + return $this->getRecord()->listens; + } + + function getOriginalURL(bool $force = false): string + { + $disallowed = !OPENVK_ROOT_CONF["openvk"]["preferences"]["music"]["exposeOriginalURLs"] && !$force; + if(!$this->isAvailable() || $disallowed) + return ovk_scheme(true) + . $_SERVER["HTTP_HOST"] . ":" + . $_SERVER["HTTP_PORT"] + . "/assets/packages/static/openvk/audio/nomusic.mp3"; + + $key = bin2hex($this->getRecord()->token); + + return str_replace(".mpd", "_fragments", $this->getURL()) . "/original_$key.mp3"; + } + + function getURL(?bool $force = false): string + { + if ($this->isWithdrawn()) return ""; + + return parent::getURL(); + } + + function getKeys(): array + { + $keys[bin2hex($this->getRecord()->kid)] = bin2hex($this->getRecord()->key); + + return $keys; + } + + function isAnonymous(): bool + { + return false; + } + + function isExplicit(): bool + { + return (bool) $this->getRecord()->explicit; + } + + function isWithdrawn(): bool + { + return (bool) $this->getRecord()->withdrawn; + } + + function isUnlisted(): bool + { + return (bool) $this->getRecord()->unlisted; + } + + # NOTICE may flush model to DB if it was just processed + function isAvailable(): bool + { + if($this->getRecord()->processed) + return true; + + # throttle requests to isAvailable to prevent DoS attack if filesystem is actually an S3 storage + if(time() - $this->getRecord()->checked < 5) + return false; + + try { + $fragments = str_replace(".mpd", "_fragments", $this->getFileName()); + $original = "original_" . bin2hex($this->getRecord()->token) . ".mp3"; + if(file_exists("$fragments/$original")) { + # Original gets uploaded after fragments + $this->stateChanges("processed", 0x01); + + return true; + } + } finally { + $this->stateChanges("checked", time()); + $this->save(); + } + + return false; + } + + function isInLibraryOf($entity): bool + { + return sizeof(DatabaseConnection::i()->getContext()->table("audio_relations")->where([ + "entity" => $entity->getId() * ($entity instanceof Club ? -1 : 1), + "audio" => $this->getId(), + ])) != 0; + } + + function add($entity): bool + { + if($this->isInLibraryOf($entity)) + return false; + + $entityId = $entity->getId() * ($entity instanceof Club ? -1 : 1); + $audioRels = DatabaseConnection::i()->getContext()->table("audio_relations"); + if(sizeof($audioRels->where("entity", $entityId)) > 65536) + throw new \OverflowException("Can't have more than 65536 audios in a playlist"); + + $audioRels->insert([ + "entity" => $entityId, + "audio" => $this->getId(), + ]); + + return true; + } + + function remove($entity): bool + { + if(!$this->isInLibraryOf($entity)) + return false; + + DatabaseConnection::i()->getContext()->table("audio_relations")->where([ + "entity" => $entity->getId() * ($entity instanceof Club ? -1 : 1), + "audio" => $this->getId(), + ])->delete(); + + return true; + } + + function listen($entity, Playlist $playlist = NULL): bool + { + $listensTable = DatabaseConnection::i()->getContext()->table("audio_listens"); + $lastListen = $listensTable->where([ + "entity" => $entity->getRealId(), + "audio" => $this->getId(), + ])->order("index DESC")->fetch(); + + if(!$lastListen || (time() - $lastListen->time >= $this->getLength())) { + $listensTable->insert([ + "entity" => $entity->getRealId(), + "audio" => $this->getId(), + "time" => time(), + "playlist" => $playlist ? $playlist->getId() : NULL, + ]); + + if($entity instanceof User) { + $this->stateChanges("listens", ($this->getListens() + 1)); + $this->save(); + + if($playlist) { + $playlist->incrementListens(); + $playlist->save(); + } + } + + $entity->setLast_played_track($this->getId()); + $entity->save(); + + return true; + } + + $lastListen->update([ + "time" => time(), + ]); + + return false; + } + + /** + * Returns compatible with VK API 4.x, 5.x structure. + * + * Always sets album(_id) to NULL at this time. + * If genre is not present in VK genre list, fallbacks to "Other". + * The url and manifest properties will be set to false if the audio can't be played (processing, removed). + * + * Aside from standard VK properties, this method will also return some OVK extended props: + * 1. added - Is in the library of $user? + * 2. editable - Can be edited by $user? + * 3. withdrawn - Removed due to copyright request? + * 4. ready - Can be played at this time? + * 5. genre_str - Full name of genre, NULL if it's undefined + * 6. manifest - URL to MPEG-DASH manifest + * 7. keys - ClearKey DRM keys + * 8. explicit - Marked as NSFW? + * 9. searchable - Can be found via search? + * 10. unique_id - Unique ID of audio + * + * @notice that in case if exposeOriginalURLs is set to false in config, "url" will always contain link to nomusic.mp3, + * unless $forceURLExposure is set to true. + * + * @notice may trigger db flush if the audio is not processed yet, use with caution on unsaved models. + * + * @param ?User $user user, relative to whom "added", "editable" will be set + * @param bool $forceURLExposure force set "url" regardless of config + */ + function toVkApiStruct(?User $user = NULL, bool $forceURLExposure = false): object + { + $obj = (object) []; + $obj->unique_id = base64_encode((string) $this->getId()); + $obj->id = $obj->aid = $this->getVirtualId(); + $obj->artist = $this->getPerformer(); + $obj->title = $this->getTitle(); + $obj->duration = $this->getLength(); + $obj->album_id = $obj->album = NULL; # i forgor to implement + $obj->url = false; + $obj->manifest = false; + $obj->keys = false; + $obj->genre_id = $obj->genre = self::vkGenres[$this->getGenre() ?? ""] ?? 18; # return Other if no match + $obj->genre_str = $this->getGenre(); + $obj->owner_id = $this->getOwner()->getId(); + if($this->getOwner() instanceof Club) + $obj->owner_id *= -1; + + $obj->lyrics = NULL; + if(!is_null($this->getLyrics())) + $obj->lyrics = $this->getId(); + + $obj->added = $user && $this->isInLibraryOf($user); + $obj->editable = $user && $this->canBeModifiedBy($user); + $obj->searchable = !$this->isUnlisted(); + $obj->explicit = $this->isExplicit(); + $obj->withdrawn = $this->isWithdrawn(); + $obj->ready = $this->isAvailable() && !$obj->withdrawn; + if($obj->ready) { + $obj->url = $this->getOriginalURL($forceURLExposure); + $obj->manifest = $this->getURL(); + $obj->keys = $this->getKeys(); + } + + return $obj; + } + + function setOwner(int $oid): void + { + # WARNING: API implementation won't be able to handle groups like that, don't remove + if($oid <= 0) + throw new \OutOfRangeException("Only users can be owners of audio!"); + + $this->stateChanges("owner", $oid); + } + + function setGenre(string $genre): void + { + if(!in_array($genre, Audio::genres)) { + $this->stateChanges("genre", NULL); + return; + } + + $this->stateChanges("genre", $genre); + } + + function setCopyrightStatus(bool $withdrawn = true): void { + $this->stateChanges("withdrawn", $withdrawn); + } + + function setSearchability(bool $searchable = true): void { + $this->stateChanges("unlisted", !$searchable); + } + + function setToken(string $tok): void { + throw new \LogicException("Changing keys is not supported."); + } + + function setKid(string $kid): void { + throw new \LogicException("Changing keys is not supported."); + } + + function setKey(string $key): void { + throw new \LogicException("Changing keys is not supported."); + } + + function setLength(int $len): void { + throw new \LogicException("Changing length is not supported."); + } + + function setSegment_Size(int $len): void { + throw new \LogicException("Changing length is not supported."); + } + + function delete(bool $softly = true): void + { + $ctx = DatabaseConnection::i()->getContext(); + $ctx->table("audio_relations")->where("audio", $this->getId()) + ->delete(); + $ctx->table("audio_listens")->where("audio", $this->getId()) + ->delete(); + $ctx->table("playlist_relations")->where("media", $this->getId()) + ->delete(); + + parent::delete($softly); + } +} diff --git a/Web/Models/Entities/Club.php b/Web/Models/Entities/Club.php index 46e8e747..319678f3 100644 --- a/Web/Models/Entities/Club.php +++ b/Web/Models/Entities/Club.php @@ -371,6 +371,29 @@ class Club extends RowModel { return $this->getRecord()->alert; } + + function getRealId(): int + { + return $this->getId() * -1; + } + + function isEveryoneCanUploadAudios(): bool + { + return (bool) $this->getRecord()->everyone_can_upload_audios; + } + + function canUploadAudio(?User $user): bool + { + if(!$user) + return NULL; + + return $this->isEveryoneCanUploadAudios() || $this->canBeModifiedBy($user); + } + + function getAudiosCollectionSize() + { + return (new \openvk\Web\Models\Repositories\Audios)->getClubCollectionSize($this); + } function toVkApiStruct(?User $user = NULL): object { @@ -381,26 +404,27 @@ class Club extends RowModel $res->screen_name = $this->getShortCode(); $res->is_closed = 0; $res->deactivated = NULL; - $res->is_admin = $this->canBeModifiedBy($user); + $res->is_admin = $user && $this->canBeModifiedBy($user); - if($this->canBeModifiedBy($user)) { + if($user && $this->canBeModifiedBy($user)) { $res->admin_level = 3; } - $res->is_member = $this->getSubscriptionStatus($user) ? 1 : 0; + $res->is_member = $user && $this->getSubscriptionStatus($user) ? 1 : 0; $res->type = "group"; $res->photo_50 = $this->getAvatarUrl("miniscule"); $res->photo_100 = $this->getAvatarUrl("tiny"); $res->photo_200 = $this->getAvatarUrl("normal"); - $res->can_create_topic = $this->canBeModifiedBy($user) ? 1 : ($this->isEveryoneCanCreateTopics() ? 1 : 0); + $res->can_create_topic = $user && $this->canBeModifiedBy($user) ? 1 : ($this->isEveryoneCanCreateTopics() ? 1 : 0); - $res->can_post = $this->canBeModifiedBy($user) ? 1 : ($this->canPost() ? 1 : 0); + $res->can_post = $user && $this->canBeModifiedBy($user) ? 1 : ($this->canPost() ? 1 : 0); return $res; } use Traits\TBackDrops; use Traits\TSubscribable; + use Traits\TAudioStatuses; } diff --git a/Web/Models/Entities/Correspondence.php b/Web/Models/Entities/Correspondence.php index e4b7d96e..a972425e 100644 --- a/Web/Models/Entities/Correspondence.php +++ b/Web/Models/Entities/Correspondence.php @@ -131,7 +131,7 @@ class Correspondence */ function getPreviewMessage(): ?Message { - $messages = $this->getMessages(1, NULL, 1); + $messages = $this->getMessages(1, NULL, 1, 0); return $messages[0] ?? NULL; } diff --git a/Web/Models/Entities/MediaCollection.php b/Web/Models/Entities/MediaCollection.php index 05f3835c..1f061988 100644 --- a/Web/Models/Entities/MediaCollection.php +++ b/Web/Models/Entities/MediaCollection.php @@ -17,7 +17,17 @@ abstract class MediaCollection extends RowModel protected $specialNames = []; - private $relations; + protected $relations; + + /** + * Maximum amount of items Collection can have + */ + const MAX_ITEMS = INF; + + /** + * Maximum amount of Collections with same "owner" allowed + */ + const MAX_COUNT = INF; function __construct(?ActiveRow $ar = NULL) { @@ -70,18 +80,29 @@ abstract class MediaCollection extends RowModel } abstract function getCoverURL(): ?string; - - function fetch(int $page = 1, ?int $perPage = NULL): \Traversable + + function fetchClassic(int $offset = 0, ?int $limit = NULL): \Traversable { - $related = $this->getRecord()->related("$this->relTableName.collection")->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE)->order("media ASC"); + $related = $this->getRecord()->related("$this->relTableName.collection") + ->limit($limit ?? OPENVK_DEFAULT_PER_PAGE, $offset) + ->order("media ASC"); + foreach($related as $rel) { $media = $rel->ref($this->entityTableName, "media"); if(!$media) continue; - + yield new $this->entityClassName($media); } } + + function fetch(int $page = 1, ?int $perPage = NULL): \Traversable + { + $page = max(1, $page); + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + + return $this->fetchClassic($perPage * ($page - 1), $perPage); + } function size(): int { @@ -110,7 +131,7 @@ abstract class MediaCollection extends RowModel { return $this->getRecord()->special_type !== 0; } - + function add(RowModel $entity): bool { $this->entitySuitable($entity); @@ -118,6 +139,10 @@ abstract class MediaCollection extends RowModel if(!$this->allowDuplicates) if($this->has($entity)) return false; + + if(self::MAX_ITEMS != INF) + if(sizeof($this->relations->where("collection", $this->getId())) > self::MAX_ITEMS) + throw new \OutOfBoundsException("Collection is full"); $this->relations->insert([ "collection" => $this->getId(), @@ -127,14 +152,14 @@ abstract class MediaCollection extends RowModel return true; } - function remove(RowModel $entity): void + function remove(RowModel $entity): bool { $this->entitySuitable($entity); - $this->relations->where([ + return $this->relations->where([ "collection" => $this->getId(), "media" => $entity->getId(), - ])->delete(); + ])->delete() > 0; } function has(RowModel $entity): bool @@ -148,6 +173,33 @@ abstract class MediaCollection extends RowModel return !is_null($rel); } - + + function save(?bool $log = false): void + { + $thisTable = DatabaseConnection::i()->getContext()->table($this->tableName); + if(self::MAX_COUNT != INF) + if(isset($this->changes["owner"])) + if(sizeof($thisTable->where("owner", $this->changes["owner"])) > self::MAX_COUNT) + throw new \OutOfBoundsException("Maximum amount of collections"); + + if(is_null($this->getRecord())) + if(!isset($this->changes["created"])) + $this->stateChanges("created", time()); + else + $this->stateChanges("edited", time()); + + parent::save($log); + } + + function delete(bool $softly = true): void + { + if(!$softly) { + $this->relations->where("collection", $this->getId()) + ->delete(); + } + + parent::delete($softly); + } + use Traits\TOwnable; } diff --git a/Web/Models/Entities/PasswordReset.php b/Web/Models/Entities/PasswordReset.php index cf0e73ea..372c63f8 100644 --- a/Web/Models/Entities/PasswordReset.php +++ b/Web/Models/Entities/PasswordReset.php @@ -54,11 +54,11 @@ class PasswordReset extends RowModel } } - function save(): void + function save(?bool $log = false): void { $this->stateChanges("key", base64_encode(openssl_random_pseudo_bytes(46))); $this->stateChanges("timestamp", time()); - parent::save(); + parent::save($log); } } diff --git a/Web/Models/Entities/Playlist.php b/Web/Models/Entities/Playlist.php new file mode 100644 index 00000000..c027a038 --- /dev/null +++ b/Web/Models/Entities/Playlist.php @@ -0,0 +1,256 @@ +importTable = DatabaseConnection::i()->getContext()->table("playlist_imports"); + } + + function getCoverURL(string $size = "normal"): ?string + { + $photo = (new Photos)->get((int) $this->getRecord()->cover_photo_id); + return is_null($photo) ? "/assets/packages/static/openvk/img/song.jpg" : $photo->getURLBySizeId($size); + } + + function getLength(): int + { + return $this->getRecord()->length; + } + + function getAudios(int $offset = 0, ?int $limit = NULL, ?int $shuffleSeed = NULL): \Traversable + { + if(!$shuffleSeed) { + foreach ($this->fetchClassic($offset, $limit) as $e) + yield $e; # No, I can't return, it will break with [] + + return; + } + + $ids = []; + foreach($this->relations->select("media AS i")->where("collection", $this->getId()) as $rel) + $ids[] = $rel->i; + + $ids = knuth_shuffle($ids, $shuffleSeed); + $ids = array_slice($ids, $offset, $limit ?? OPENVK_DEFAULT_PER_PAGE); + foreach($ids as $id) + yield (new Audios)->get($id); + } + + function add(RowModel $audio): bool + { + if($res = parent::add($audio)) { + $this->stateChanges("length", $this->getRecord()->length + $audio->getLength()); + $this->save(); + } + + return $res; + } + + function remove(RowModel $audio): bool + { + if($res = parent::remove($audio)) { + $this->stateChanges("length", $this->getRecord()->length - $audio->getLength()); + $this->save(); + } + + return $res; + } + + function isBookmarkedBy(RowModel $entity): bool + { + $id = $entity->getId(); + if($entity instanceof Club) + $id *= -1; + + return !is_null($this->importTable->where([ + "entity" => $id, + "playlist" => $this->getId(), + ])->fetch()); + } + + function bookmark(RowModel $entity): bool + { + if($this->isBookmarkedBy($entity)) + return false; + + $id = $entity->getId(); + if($entity instanceof Club) + $id *= -1; + + if($this->importTable->where("entity", $id)->count() > self::MAX_COUNT) + throw new \OutOfBoundsException("Maximum amount of playlists"); + + $this->importTable->insert([ + "entity" => $id, + "playlist" => $this->getId(), + ]); + + return true; + } + + function unbookmark(RowModel $entity): bool + { + $id = $entity->getId(); + if($entity instanceof Club) + $id *= -1; + + $count = $this->importTable->where([ + "entity" => $id, + "playlist" => $this->getId(), + ])->delete(); + + return $count > 0; + } + + function getDescription(): ?string + { + return $this->getRecord()->description; + } + + function getDescriptionHTML(): ?string + { + return htmlspecialchars($this->getRecord()->description, ENT_DISALLOWED | ENT_XHTML); + } + + function getListens() + { + return $this->getRecord()->listens; + } + + function toVkApiStruct(?User $user = NULL): object + { + $oid = $this->getOwner()->getId(); + if($this->getOwner() instanceof Club) + $oid *= -1; + + return (object) [ + "id" => $this->getId(), + "owner_id" => $oid, + "title" => $this->getName(), + "description" => $this->getDescription(), + "size" => $this->size(), + "length" => $this->getLength(), + "created" => $this->getCreationTime()->timestamp(), + "modified" => $this->getEditTime() ? $this->getEditTime()->timestamp() : NULL, + "accessible" => $this->canBeViewedBy($user), + "editable" => $this->canBeModifiedBy($user), + "bookmarked" => $this->isBookmarkedBy($user), + "listens" => $this->getListens(), + "cover_url" => $this->getCoverURL(), + ]; + } + + function setLength(): void + { + throw new \LogicException("Can't set length of playlist manually"); + } + + function resetLength(): bool + { + $this->stateChanges("length", 0); + + return true; + } + + function delete(bool $softly = true): void + { + $ctx = DatabaseConnection::i()->getContext(); + $ctx->table("playlist_imports")->where("playlist", $this->getId()) + ->delete(); + + parent::delete($softly); + } + + function hasAudio(Audio $audio): bool + { + $ctx = DatabaseConnection::i()->getContext(); + return !is_null($ctx->table("playlist_relations")->where([ + "collection" => $this->getId(), + "media" => $audio->getId() + ])->fetch()); + } + + function getCoverPhotoId(): ?int + { + return $this->getRecord()->cover_photo_id; + } + + function canBeModifiedBy(User $user): bool + { + if(!$user) + return false; + + if($this->getOwner() instanceof User) + return $user->getId() == $this->getOwner()->getId(); + else + return $this->getOwner()->canBeModifiedBy($user); + } + + function getLengthInMinutes(): int + { + return (int)round($this->getLength() / 60, PHP_ROUND_HALF_DOWN); + } + + function fastMakeCover(int $owner, array $file) + { + $cover = new Photo; + $cover->setOwner($owner); + $cover->setDescription("Playlist cover image"); + $cover->setFile($file); + $cover->setCreated(time()); + $cover->save(); + + $this->setCover_photo_id($cover->getId()); + + return $cover; + } + + function getURL(): string + { + return "/playlist" . $this->getOwner()->getRealId() . "_" . $this->getId(); + } + + function incrementListens() + { + $this->stateChanges("listens", ($this->getListens() + 1)); + } + + function getMetaDescription(): string + { + $length = $this->getLengthInMinutes(); + + $props = []; + $props[] = tr("audios_count", $this->size()); + $props[] = "" . tr("listens_count", $this->getListens()) . ""; + if($length > 0) $props[] = tr("minutes_count", $length); + $props[] = tr("created_playlist") . " " . $this->getPublicationTime(); + # if($this->getEditTime()) $props[] = tr("updated_playlist") . " " . $this->getEditTime(); + + return implode(" • ", $props); + } +} diff --git a/Web/Models/Entities/Poll.php b/Web/Models/Entities/Poll.php index 6f2885b1..7e32505f 100644 --- a/Web/Models/Entities/Poll.php +++ b/Web/Models/Entities/Poll.php @@ -279,12 +279,12 @@ class Poll extends Attachable return $poll; } - function save(): void + function save(?bool $log = false): void { if(empty($this->choicesToPersist)) throw new InvalidStateException; - parent::save(); + parent::save($log); foreach($this->choicesToPersist as $option) { DatabaseConnection::i()->getContext()->table("poll_options")->insert([ "poll" => $this->getId(), diff --git a/Web/Models/Entities/Report.php b/Web/Models/Entities/Report.php index d449a2e8..5487056f 100644 --- a/Web/Models/Entities/Report.php +++ b/Web/Models/Entities/Report.php @@ -5,7 +5,7 @@ use Nette\Database\Table\ActiveRow; use openvk\Web\Models\RowModel; use openvk\Web\Models\Entities\Club; use Chandler\Database\DatabaseConnection; -use openvk\Web\Models\Repositories\{Applications, Comments, Notes, Reports, Users, Posts, Photos, Videos, Clubs}; +use openvk\Web\Models\Repositories\{Applications, Comments, Notes, Reports, Audios, Users, Posts, Photos, Videos, Clubs}; use Chandler\Database\DatabaseConnection as DB; use Nette\InvalidStateException as ISE; use Nette\Database\Table\Selection; @@ -74,6 +74,7 @@ class Report extends RowModel else if ($this->getContentType() == "note") return (new Notes)->get($this->getContentId()); else if ($this->getContentType() == "app") return (new Applications)->get($this->getContentId()); else if ($this->getContentType() == "user") return (new Users)->get($this->getContentId()); + else if ($this->getContentType() == "audio") return (new Audios)->get($this->getContentId()); else return null; } diff --git a/Web/Models/Entities/Traits/TAudioStatuses.php b/Web/Models/Entities/Traits/TAudioStatuses.php new file mode 100644 index 00000000..f957a104 --- /dev/null +++ b/Web/Models/Entities/Traits/TAudioStatuses.php @@ -0,0 +1,38 @@ +getRealId() < 0) return true; + return (bool) $this->getRecord()->audio_broadcast_enabled; + } + + function getCurrentAudioStatus() + { + if(!$this->isBroadcastEnabled()) return NULL; + + $audioId = $this->getRecord()->last_played_track; + + if(!$audioId) return NULL; + $audio = (new Audios)->get($audioId); + + if(!$audio || $audio->isDeleted()) + return NULL; + + $listensTable = DatabaseConnection::i()->getContext()->table("audio_listens"); + $lastListen = $listensTable->where([ + "entity" => $this->getRealId(), + "audio" => $audio->getId(), + "time >" => (time() - $audio->getLength()) - 10, + ])->fetch(); + + if($lastListen) + return $audio; + + return NULL; + } +} diff --git a/Web/Models/Entities/Traits/TOwnable.php b/Web/Models/Entities/Traits/TOwnable.php index 9dc9ce2a..08e5fde3 100644 --- a/Web/Models/Entities/Traits/TOwnable.php +++ b/Web/Models/Entities/Traits/TOwnable.php @@ -4,6 +4,12 @@ use openvk\Web\Models\Entities\User; trait TOwnable { + function canBeViewedBy(?User $user): bool + { + // TODO implement normal check in master + return true; + } + function canBeModifiedBy(User $user): bool { if(method_exists($this, "isCreatedBySystem")) diff --git a/Web/Models/Entities/User.php b/Web/Models/Entities/User.php index aaf00ec9..40ef7aef 100644 --- a/Web/Models/Entities/User.php +++ b/Web/Models/Entities/User.php @@ -4,7 +4,7 @@ use morphos\Gender; use openvk\Web\Themes\{Themepack, Themepacks}; use openvk\Web\Util\DateTime; use openvk\Web\Models\RowModel; -use openvk\Web\Models\Entities\{Photo, Message, Correspondence, Gift}; +use openvk\Web\Models\Entities\{Photo, Message, Correspondence, Gift, Audio}; use openvk\Web\Models\Repositories\{Applications, Bans, Comments, Notes, Posts, Users, Clubs, Albums, Gifts, Notifications, Videos, Photos}; use openvk\Web\Models\Exceptions\InvalidUserNameException; use Nette\Database\Table\ActiveRow; @@ -190,7 +190,7 @@ class User extends RowModel function getMorphedName(string $case = "genitive", bool $fullName = true): string { $name = $fullName ? ($this->getLastName() . " " . $this->getFirstName()) : $this->getFirstName(); - if(!preg_match("%^[А-яё\-]+$%", $name)) + if(!preg_match("%[А-яё\-]+$%", $name)) return $name; # name is probably not russian $inflected = inflectName($name, $case, $this->isFemale() ? Gender::FEMALE : Gender::MALE); @@ -455,6 +455,7 @@ class User extends RowModel "length" => 1, "mappings" => [ "photos", + "audios", "videos", "messages", "notes", @@ -462,7 +463,7 @@ class User extends RowModel "news", "links", "poster", - "apps" + "apps", ], ])->get($id); } @@ -482,6 +483,7 @@ class User extends RowModel "friends.add", "wall.write", "messages.write", + "audios.read", ], ])->get($id); } @@ -720,8 +722,8 @@ class User extends RowModel for($i = 0; $i < 10 - $this->get2faBackupCodeCount(); $i++) { $codes[] = [ - owner => $this->getId(), - code => random_int(10000000, 99999999) + "owner" => $this->getId(), + "code" => random_int(10000000, 99999999) ]; } @@ -1010,6 +1012,7 @@ class User extends RowModel "friends.add", "wall.write", "messages.write", + "audios.read", ], ])->set($id, $status)->toInteger()); } @@ -1020,6 +1023,7 @@ class User extends RowModel "length" => 1, "mappings" => [ "photos", + "audios", "videos", "messages", "notes", @@ -1027,7 +1031,7 @@ class User extends RowModel "news", "links", "poster", - "apps" + "apps", ], ])->set($id, (int) $status)->toInteger(); @@ -1223,6 +1227,11 @@ class User extends RowModel return $response; } + function getRealId() + { + return $this->getId(); + } + function toVkApiStruct(): object { $res = (object) []; @@ -1239,7 +1248,47 @@ class User extends RowModel return $res; } + + function getAudiosCollectionSize() + { + return (new \openvk\Web\Models\Repositories\Audios)->getUserCollectionSize($this); + } + + function getBroadcastList(string $filter = "friends", bool $shuffle = false) + { + $dbContext = DatabaseConnection::i()->getContext(); + $entityIds = []; + $query = $dbContext->table("subscriptions")->where("follower", $this->getRealId()); + + if($filter != "all") + $query = $query->where("model = ?", "openvk\\Web\\Models\\Entities\\" . ($filter == "groups" ? "Club" : "User")); + + foreach($query as $_rel) { + $entityIds[] = $_rel->model == "openvk\\Web\\Models\\Entities\\Club" ? $_rel->target * -1 : $_rel->target; + } + + if($shuffle) { + $shuffleSeed = openssl_random_pseudo_bytes(6); + $shuffleSeed = hexdec(bin2hex($shuffleSeed)); + $entityIds = knuth_shuffle($entityIds, $shuffleSeed); + } + + $entityIds = array_slice($entityIds, 0, 10); + + $returnArr = []; + + foreach($entityIds as $id) { + $entit = $id > 0 ? (new Users)->get($id) : (new Clubs)->get(abs($id)); + + if($id > 0 && $entit->isDeleted()) continue; + $returnArr[] = $entit; + } + + return $returnArr; + } + use Traits\TBackDrops; use Traits\TSubscribable; + use Traits\TAudioStatuses; } diff --git a/Web/Models/Entities/Video.php b/Web/Models/Entities/Video.php index 2e56414c..a9d565c5 100644 --- a/Web/Models/Entities/Video.php +++ b/Web/Models/Entities/Video.php @@ -1,7 +1,7 @@ $owner, "id" => $id ])->fetch(); - - return new Album($album); + + return $album ? new Album($album) : NULL; } } diff --git a/Web/Models/Repositories/Audios.php b/Web/Models/Repositories/Audios.php new file mode 100644 index 00000000..64457299 --- /dev/null +++ b/Web/Models/Repositories/Audios.php @@ -0,0 +1,296 @@ +context = DatabaseConnection::i()->getContext(); + $this->audios = $this->context->table("audios"); + $this->rels = $this->context->table("audio_relations"); + + $this->playlists = $this->context->table("playlists"); + $this->playlistImports = $this->context->table("playlist_imports"); + $this->playlistRels = $this->context->table("playlist_relations"); + } + + function get(int $id): ?Audio + { + $audio = $this->audios->get($id); + if(!$audio) + return NULL; + + return new Audio($audio); + } + + function getPlaylist(int $id): ?Playlist + { + $playlist = $this->playlists->get($id); + if(!$playlist) + return NULL; + + return new Playlist($playlist); + } + + function getByOwnerAndVID(int $owner, int $vId): ?Audio + { + $audio = $this->audios->where([ + "owner" => $owner, + "virtual_id" => $vId, + ])->fetch(); + if(!$audio) return NULL; + + return new Audio($audio); + } + + function getPlaylistByOwnerAndVID(int $owner, int $vId): ?Playlist + { + $playlist = $this->playlists->where([ + "owner" => $owner, + "id" => $vId, + ])->fetch(); + if(!$playlist) return NULL; + + return new Playlist($playlist); + } + + function getByEntityID(int $entity, int $offset = 0, ?int $limit = NULL, ?int& $deleted = nullptr): \Traversable + { + $limit ??= OPENVK_DEFAULT_PER_PAGE; + $iter = $this->rels->where("entity", $entity)->limit($limit, $offset)->order("index DESC"); + foreach($iter as $rel) { + $audio = $this->get($rel->audio); + if(!$audio || $audio->isDeleted()) { + $deleted++; + continue; + } + + yield $audio; + } + } + + function getPlaylistsByEntityId(int $entity, int $offset = 0, ?int $limit = NULL, ?int& $deleted = nullptr): \Traversable + { + $limit ??= OPENVK_DEFAULT_PER_PAGE; + $iter = $this->playlistImports->where("entity", $entity)->limit($limit, $offset); + foreach($iter as $rel) { + $playlist = $this->getPlaylist($rel->playlist); + if(!$playlist || $playlist->isDeleted()) { + $deleted++; + continue; + } + + yield $playlist; + } + } + + function getByUser(User $user, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable + { + 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); + } + + function getPlaylistsByUser(User $user, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable + { + return $this->getPlaylistsByEntityId($user->getId(), ($perPage * ($page - 1)), $perPage, $deleted); + } + + function getPlaylistsByClub(Club $club, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable + { + return $this->getPlaylistsByEntityId($club->getId() * -1, ($perPage * ($page - 1)), $perPage, $deleted); + } + + function getCollectionSizeByEntityId(int $id): int + { + return sizeof($this->rels->where("entity", $id)); + } + + function getUserCollectionSize(User $user): int + { + return sizeof($this->rels->where("entity", $user->getId())); + } + + function getClubCollectionSize(Club $club): int + { + return sizeof($this->rels->where("entity", $club->getId() * -1)); + } + + function getUserPlaylistsCount(User $user): int + { + return sizeof($this->playlistImports->where("entity", $user->getId())); + } + + function getClubPlaylistsCount(Club $club): int + { + return sizeof($this->playlistImports->where("entity", $club->getId() * -1)); + } + + function getByUploader(User $user): EntityStream + { + $search = $this->audios->where([ + "owner" => $user->getId(), + "deleted" => 0, + ]); + + return new EntityStream("Audio", $search); + } + + function getGlobal(int $order, ?string $genreId = NULL): EntityStream + { + $search = $this->audios->where([ + "deleted" => 0, + "unlisted" => 0, + "withdrawn" => 0, + ])->order($order == Audios::ORDER_NEW ? "created DESC" : "listens DESC"); + + if(!is_null($genreId)) + $search = $search->where("genre", $genreId); + + return new EntityStream("Audio", $search); + } + + function search(string $query, int $sortMode = 0, bool $performerOnly = false, bool $withLyrics = false): EntityStream + { + $columns = $performerOnly ? "performer" : "performer, name"; + $order = (["created", "length", "listens"][$sortMode] ?? "") . " DESC"; + + $search = $this->audios->where([ + "unlisted" => 0, + "deleted" => 0, + ])->where("MATCH ($columns) AGAINST (? WITH QUERY EXPANSION)", $query)->order($order); + + if($withLyrics) + $search = $search->where("lyrics IS NOT NULL"); + + return new EntityStream("Audio", $search); + } + + function searchPlaylists(string $query): EntityStream + { + $search = $this->playlists->where([ + "deleted" => 0, + ])->where("MATCH (`name`, `description`) AGAINST (? IN BOOLEAN MODE)", $query); + + return new EntityStream("Playlist", $search); + } + + function getNew(): EntityStream + { + return new EntityStream("Audio", $this->audios->where("created >= " . (time() - 259200))->where(["withdrawn" => 0, "deleted" => 0, "unlisted" => 0])->order("created DESC")->limit(25)); + } + + function getPopular(): EntityStream + { + 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 + { + return !is_null($this->rels->where([ + "entity" => $user_id, + "audio" => $audio_id + ])->fetch()); + } + + function find(string $query, array $pars = [], string $sort = "id DESC", int $page = 1, ?int $perPage = NULL): \Traversable + { + $query = "%$query%"; + $result = $this->audios->where([ + "unlisted" => 0, + "deleted" => 0, + ]); + + $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 + { + $query = "%$query%"; + $result = $this->playlists->where("name LIKE ?", $query); + + return new Util\EntityStream("Playlist", $result); + } +} diff --git a/Web/Models/Repositories/Messages.php b/Web/Models/Repositories/Messages.php index 4c870806..538338ed 100644 --- a/Web/Models/Repositories/Messages.php +++ b/Web/Models/Repositories/Messages.php @@ -52,7 +52,6 @@ class Messages $query = file_get_contents(__DIR__ . "/../sql/get-correspondencies-count.tsql"); DatabaseConnection::i()->getConnection()->query(file_get_contents(__DIR__ . "/../sql/mysql-msg-fix.tsql")); $count = DatabaseConnection::i()->getConnection()->query($query, $id, $class, $id, $class)->fetch()->cnt; - bdump($count); return $count; } } diff --git a/Web/Models/shell/processAudio.ps1 b/Web/Models/shell/processAudio.ps1 new file mode 100644 index 00000000..f60a9aed --- /dev/null +++ b/Web/Models/shell/processAudio.ps1 @@ -0,0 +1,39 @@ +$ovkRoot = $args[0] +$storageDir = $args[1] +$fileHash = $args[2] +$hashPart = $fileHash.substring(0, 2) +$filename = $args[3] +$audioFile = [System.IO.Path]::GetTempFileName() +$temp = [System.IO.Path]::GetTempFileName() + +$keyID = $args[4] +$key = $args[5] +$token = $args[6] +$seg = $args[7] + +$shell = Get-WmiObject Win32_process -filter "ProcessId = $PID" +$shell.SetPriority(16384) # because there's no "nice" program in Windows we just set a lower priority for entire tree + +Remove-Item $temp +Remove-Item $audioFile +New-Item -ItemType "directory" $temp +New-Item -ItemType "directory" ("$temp/$fileHash" + '_fragments') +New-Item -ItemType "directory" ("$storageDir/$hashPart/$fileHash" + '_fragments') +Set-Location -Path $temp + +Move-Item $filename $audioFile +ffmpeg -i $audioFile -f dash -encryption_scheme cenc-aes-ctr -encryption_key $key ` + -encryption_kid $keyID -map 0:a -vn -c:a aac -ar 44100 -seg_duration $seg ` + -use_timeline 1 -use_template 1 -init_seg_name ($fileHash + '_fragments/0_0.$ext$') ` + -media_seg_name ($fileHash + '_fragments/chunk$Number%06d$_$RepresentationID$.$ext$') -adaptation_sets 'id=0,streams=a' ` + "$fileHash.mpd" + +ffmpeg -i $audioFile -vn -ar 44100 "original_$token.mp3" +Move-Item "original_$token.mp3" ($fileHash + '_fragments') + +Get-ChildItem -Path ($fileHash + '_fragments/*') | Move-Item -Destination ("$storageDir/$hashPart/$fileHash" + '_fragments') +Move-Item -Path ("$fileHash.mpd") -Destination "$storageDir/$hashPart" + +cd .. +Remove-Item -Recurse $temp +Remove-Item $audioFile diff --git a/Web/Models/shell/processAudio.sh b/Web/Models/shell/processAudio.sh new file mode 100644 index 00000000..fa8346e0 --- /dev/null +++ b/Web/Models/shell/processAudio.sh @@ -0,0 +1,35 @@ +ovkRoot=$1 +storageDir=$2 +fileHash=$3 +hashPart=$(echo $fileHash | cut -c1-2) +filename=$4 +audioFile=$(mktemp) +temp=$(mktemp -d) + +keyID=$5 +key=$6 +token=$7 +seg=$8 + +trap 'rm -f "$temp" "$audioFile"' EXIT + +mkdir -p "$temp/$fileHash"_fragments +mkdir -p "$storageDir/$hashPart/$fileHash"_fragments +cd "$temp" + +mv "$filename" "$audioFile" +ffmpeg -i "$audioFile" -f dash -encryption_scheme cenc-aes-ctr -encryption_key "$key" \ + -encryption_kid "$keyID" -map 0 -vn -c:a aac -ar 44100 -seg_duration "$seg" \ + -use_timeline 1 -use_template 1 -init_seg_name "$fileHash"_fragments/0_0."\$ext\$" \ + -media_seg_name "$fileHash"_fragments/chunk"\$Number"%06d\$_"\$RepresentationID\$"."\$ext\$" -adaptation_sets 'id=0,streams=a' \ + "$fileHash.mpd" + +ffmpeg -i "$audioFile" -vn -ar 44100 "original_$token.mp3" +mv "original_$token.mp3" "$fileHash"_fragments + +mv "$fileHash"_fragments "$storageDir/$hashPart" +mv "$fileHash.mpd" "$storageDir/$hashPart" + +cd .. +rm -rf "$temp" +rm -f "$audioFile" diff --git a/Web/Presenters/AdminPresenter.php b/Web/Presenters/AdminPresenter.php index 14fbbc74..658d2f4b 100644 --- a/Web/Presenters/AdminPresenter.php +++ b/Web/Presenters/AdminPresenter.php @@ -3,7 +3,19 @@ namespace openvk\Web\Presenters; use Chandler\Database\Log; use Chandler\Database\Logs; use openvk\Web\Models\Entities\{Voucher, Gift, GiftCategory, User, BannedLink}; -use openvk\Web\Models\Repositories\{Bans, ChandlerGroups, ChandlerUsers, Photos, Posts, Users, Clubs, Videos, Vouchers, Gifts, BannedLinks}; +use openvk\Web\Models\Repositories\{Audios, + ChandlerGroups, + ChandlerUsers, + Users, + Clubs, + Util\EntityStream, + Vouchers, + Gifts, + BannedLinks, + Bans, + Photos, + Posts, + Videos}; use Chandler\Database\DatabaseConnection; final class AdminPresenter extends OpenVKPresenter @@ -14,9 +26,10 @@ final class AdminPresenter extends OpenVKPresenter private $gifts; private $bannedLinks; private $chandlerGroups; + private $audios; private $logs; - function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gifts, BannedLinks $bannedLinks, ChandlerGroups $chandlerGroups) + function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gifts, BannedLinks $bannedLinks, ChandlerGroups $chandlerGroups, Audios $audios) { $this->users = $users; $this->clubs = $clubs; @@ -24,8 +37,9 @@ final class AdminPresenter extends OpenVKPresenter $this->gifts = $gifts; $this->bannedLinks = $bannedLinks; $this->chandlerGroups = $chandlerGroups; + $this->audios = $audios; $this->logs = DatabaseConnection::i()->getContext()->table("ChandlerLogs"); - + parent::__construct(); } @@ -43,6 +57,15 @@ final class AdminPresenter extends OpenVKPresenter $count = $repo->find($query)->size(); return $repo->find($query)->page($page, 20); } + + private function searchPlaylists(&$count) + { + $query = $this->queryParam("q") ?? ""; + $page = (int) ($this->queryParam("p") ?? 1); + + $count = $this->audios->findPlaylists($query)->size(); + return $this->audios->findPlaylists($query)->page($page, 20); + } function onStartup(): void { @@ -578,6 +601,54 @@ final class AdminPresenter extends OpenVKPresenter $this->redirect("/admin/users/id" . $user->getId()); } + function renderMusic(): void + { + $this->template->mode = in_array($this->queryParam("act"), ["audios", "playlists"]) ? $this->queryParam("act") : "audios"; + if ($this->template->mode === "audios") + $this->template->audios = $this->searchResults($this->audios, $this->template->count); + else + $this->template->playlists = $this->searchPlaylists($this->template->count); + } + + function renderEditMusic(int $audio_id): void + { + $audio = $this->audios->get($audio_id); + $this->template->audio = $audio; + + try { + $this->template->owner = $audio->getOwner()->getId(); + } catch(\Throwable $e) { + $this->template->owner = 1; + } + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $audio->setName($this->postParam("name")); + $audio->setPerformer($this->postParam("performer")); + $audio->setLyrics($this->postParam("text")); + $audio->setGenre($this->postParam("genre")); + $audio->setOwner((int) $this->postParam("owner")); + $audio->setExplicit(!empty($this->postParam("explicit"))); + $audio->setDeleted(!empty($this->postParam("deleted"))); + $audio->setWithdrawn(!empty($this->postParam("withdrawn"))); + $audio->save(); + } + } + + function renderEditPlaylist(int $playlist_id): void + { + $playlist = $this->audios->getPlaylist($playlist_id); + $this->template->playlist = $playlist; + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $playlist->setName($this->postParam("name")); + $playlist->setDescription($this->postParam("description")); + $playlist->setCover_Photo_Id((int) $this->postParam("photo")); + $playlist->setOwner((int) $this->postParam("owner")); + $playlist->setDeleted(!empty($this->postParam("deleted"))); + $playlist->save(); + } + } + function renderLogs(): void { $filter = []; diff --git a/Web/Presenters/AudioPresenter.php b/Web/Presenters/AudioPresenter.php new file mode 100644 index 00000000..8ec17012 --- /dev/null +++ b/Web/Presenters/AudioPresenter.php @@ -0,0 +1,696 @@ +audios = $audios; + } + + function renderPopular(): void + { + $this->renderList(NULL, "popular"); + } + + function renderNew(): void + { + $this->renderList(NULL, "new"); + } + + function renderList(?int $owner = NULL, ?string $mode = "list"): void + { + $this->template->_template = "Audio/List.xml"; + $page = (int)($this->queryParam("p") ?? 1); + $audios = []; + + if ($mode === "list") { + $entity = NULL; + if ($owner < 0) { + $entity = (new Clubs)->get($owner * -1); + if (!$entity || $entity->isBanned()) + $this->redirect("/audios" . $this->user->id); + + $audios = $this->audios->getByClub($entity, $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); + + if(!$entity->getPrivacyPermission("audios.read", $this->user->identity)) + $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); + + $audios = $this->audios->getByUser($entity, $page, 10); + $audiosCount = $this->audios->getUserCollectionSize($entity); + } + + if (!$entity) + $this->notFound(); + + $this->template->owner = $entity; + $this->template->ownerId = $owner; + $this->template->club = $owner < 0 ? $entity : NULL; + $this->template->isMy = ($owner > 0 && ($entity->getId() === $this->user->id)); + $this->template->isMyClub = ($owner < 0 && $entity->canBeModifiedBy($this->user->identity)); + } else if ($mode === "new") { + $audios = $this->audios->getNew(); + $audiosCount = $audios->size(); + } else if ($mode === "playlists") { + if($owner < 0) { + $entity = (new Clubs)->get(abs($owner)); + if (!$entity || $entity->isBanned()) + $this->redirect("/playlists" . $this->user->id); + + $playlists = $this->audios->getPlaylistsByClub($entity, $page, 10); + $playlistsCount = $this->audios->getClubPlaylistsCount($entity); + } else { + $entity = (new Users)->get($owner); + if (!$entity || $entity->isDeleted() || $entity->isBanned()) + $this->redirect("/playlists" . $this->user->id); + + if(!$entity->getPrivacyPermission("audios.read", $this->user->identity)) + $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); + + $playlists = $this->audios->getPlaylistsByUser($entity, $page, 9); + $playlistsCount = $this->audios->getUserPlaylistsCount($entity); + } + + $this->template->playlists = iterator_to_array($playlists); + $this->template->playlistsCount = $playlistsCount; + $this->template->owner = $entity; + $this->template->ownerId = $owner; + $this->template->club = $owner < 0 ? $entity : NULL; + $this->template->isMy = ($owner > 0 && ($entity->getId() === $this->user->id)); + $this->template->isMyClub = ($owner < 0 && $entity->canBeModifiedBy($this->user->identity)); + } else { + $audios = $this->audios->getPopular(); + $audiosCount = $audios->size(); + } + + // $this->renderApp("owner=$owner"); + if ($audios !== []) { + $this->template->audios = iterator_to_array($audios); + $this->template->audiosCount = $audiosCount; + } + + $this->template->mode = $mode; + $this->template->page = $page; + + if(in_array($mode, ["list", "new", "popular"]) && $this->user->identity) + $this->template->friendsAudios = $this->user->identity->getBroadcastList("all", true); + } + + function renderEmbed(int $owner, int $id): void + { + $audio = $this->audios->getByOwnerAndVID($owner, $id); + if(!$audio) { + header("HTTP/1.1 404 Not Found"); + exit("" . tr("audio_embed_not_found") . "."); + } else if($audio->isDeleted()) { + header("HTTP/1.1 410 Not Found"); + exit("" . tr("audio_embed_deleted") . "."); + } else if($audio->isWithdrawn()) { + header("HTTP/1.1 451 Unavailable for legal reasons"); + exit("" . tr("audio_embed_withdrawn") . "."); + } else if(!$audio->canBeViewedBy(NULL)) { + header("HTTP/1.1 403 Forbidden"); + exit("" . tr("audio_embed_forbidden") . "."); + } else if(!$audio->isAvailable()) { + header("HTTP/1.1 425 Too Early"); + exit("" . tr("audio_embed_processing") . "."); + } + + $this->template->audio = $audio; + } + + function renderUpload(): void + { + $this->assertUserLoggedIn(); + + $group = NULL; + $isAjax = $this->postParam("ajax", false) == 1; + if(!is_null($this->queryParam("gid"))) { + $gid = (int) $this->queryParam("gid"); + $group = (new Clubs)->get($gid); + if(!$group) + $this->flashFail("err", tr("forbidden"), tr("not_enough_permissions_comment"), null, $isAjax); + + if(!$group->canUploadAudio($this->user->identity)) + $this->flashFail("err", tr("forbidden"), tr("not_enough_permissions_comment"), null, $isAjax); + } + + $this->template->group = $group; + + if($_SERVER["REQUEST_METHOD"] !== "POST") + return; + + $upload = $_FILES["blob"]; + if(isset($upload) && file_exists($upload["tmp_name"])) { + if($upload["size"] > self::MAX_AUDIO_SIZE) + $this->flashFail("err", tr("error"), tr("media_file_corrupted_or_too_large"), null, $isAjax); + } else { + $err = !isset($upload) ? 65536 : $upload["error"]; + $err = str_pad(dechex($err), 9, "0", STR_PAD_LEFT); + $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"); + $name = $this->postParam("name"); + $lyrics = $this->postParam("lyrics"); + $genre = empty($this->postParam("genre")) ? "Other" : $this->postParam("genre"); + $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"), null, $isAjax); + + $audio = new Audio; + $audio->setOwner($this->user->id); + $audio->setName($name); + $audio->setPerformer($performer); + $audio->setLyrics(empty($lyrics) ? NULL : $lyrics); + $audio->setGenre($genre); + $audio->setExplicit($nsfw); + + try { + $audio->setFile($upload); + } 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 на данный момент не реализована. Следите за обновлениями: https://github.com/openvk/openvk/pull/512/commits", null, $isAjax); + } catch(\Exception $ex) { + $this->flashFail("err", tr("error"), tr("ffmpeg_not_installed"), null, $isAjax); + } + + $audio->save(); + $audio->add($group ?? $this->user->identity); + + if(!$isAjax) + $this->redirect(is_null($group) ? "/audios" . $this->user->id : "/audios-" . $group->getId()); + else { + $redirectLink = "/audios"; + + if(!is_null($group)) + $redirectLink .= $group->getRealId(); + else + $redirectLink .= $this->user->id; + + $pagesCount = (int)ceil((new Audios)->getCollectionSizeByEntityId(isset($group) ? $group->getRealId() : $this->user->id) / 10); + $redirectLink .= "?p=".$pagesCount; + + $this->returnJson([ + "success" => true, + "redirect_link" => $redirectLink, + ]); + } + } + + function renderListen(int $id): void + { + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $this->assertNoCSRF(); + + if(is_null($this->user)) + $this->returnJson(["success" => false]); + + $audio = $this->audios->get($id); + + if ($audio && !$audio->isDeleted() && !$audio->isWithdrawn()) { + if(!empty($this->postParam("playlist"))) { + $playlist = (new Audios)->getPlaylist((int)$this->postParam("playlist")); + + if(!$playlist || $playlist->isDeleted() || !$playlist->canBeViewedBy($this->user->identity) || !$playlist->hasAudio($audio)) + $playlist = NULL; + } + + $listen = $audio->listen($this->user->identity, $playlist); + + $returnArr = ["success" => $listen]; + + if($playlist) + $returnArr["new_playlists_listens"] = $playlist->getListens(); + + $this->returnJson($returnArr); + } + + $this->returnJson(["success" => false]); + } else { + $this->redirect("/"); + } + } + + function renderSearch(): void + { + $this->redirect("/search?type=audios"); + } + + function renderNewPlaylist(): void + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(true); + + $owner = $this->user->id; + + if ($this->requestParam("gid")) { + $club = (new Clubs)->get((int) abs((int)$this->requestParam("gid"))); + if (!$club || $club->isBanned() || !$club->canBeModifiedBy($this->user->identity)) + $this->redirect("/audios" . $this->user->id); + + $owner = ($club->getId() * -1); + + $this->template->club = $club; + } + + $this->template->owner = $owner; + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $title = $this->postParam("title"); + $description = $this->postParam("description"); + $audios = !empty($this->postParam("audios")) ? array_slice(explode(",", $this->postParam("audios")), 0, 1000) : []; + + if(empty($title) || iconv_strlen($title) < 1) + $this->flashFail("err", tr("error"), tr("set_playlist_name")); + + $playlist = new Playlist; + $playlist->setOwner($owner); + $playlist->setName(substr($title, 0, 125)); + $playlist->setDescription(substr($description, 0, 2045)); + + if($_FILES["cover"]["error"] === UPLOAD_ERR_OK) { + if(!str_starts_with($_FILES["cover"]["type"], "image")) + $this->flashFail("err", tr("error"), tr("not_a_photo")); + + try { + $playlist->fastMakeCover($this->user->id, $_FILES["cover"]); + } catch(\Throwable $e) { + $this->flashFail("err", tr("error"), tr("invalid_cover_photo")); + } + } + + $playlist->save(); + + foreach($audios as $audio) { + $audio = $this->audios->get((int)$audio); + + if(!$audio || $audio->isDeleted() || !$audio->canBeViewedBy($this->user->identity)) + continue; + + $playlist->add($audio); + } + + $playlist->bookmark(isset($club) ? $club : $this->user->identity); + $this->redirect("/playlist" . $owner . "_" . $playlist->getId()); + } else { + if(isset($club)) { + $this->template->audios = iterator_to_array($this->audios->getByClub($club, 1, 10)); + $count = (new Audios)->getClubCollectionSize($club); + } else { + $this->template->audios = iterator_to_array($this->audios->getByUser($this->user->identity, 1, 10)); + $count = (new Audios)->getUserCollectionSize($this->user->identity); + } + + $this->template->pagesCount = ceil($count / 10); + } + } + + function renderPlaylistAction(int $id) { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(true); + $this->assertNoCSRF(); + + if ($_SERVER["REQUEST_METHOD"] !== "POST") { + header("HTTP/1.1 405 Method Not Allowed"); + $this->redirect("/"); + } + + $playlist = $this->audios->getPlaylist($id); + + if(!$playlist || $playlist->isDeleted()) + $this->flashFail("err", "error", tr("invalid_playlist"), null, true); + + switch ($this->queryParam("act")) { + case "bookmark": + if(!$playlist->isBookmarkedBy($this->user->identity)) + $playlist->bookmark($this->user->identity); + else + $this->flashFail("err", "error", tr("playlist_already_bookmarked"), null, true); + + break; + case "unbookmark": + if($playlist->isBookmarkedBy($this->user->identity)) + $playlist->unbookmark($this->user->identity); + else + $this->flashFail("err", "error", tr("playlist_not_bookmarked"), null, true); + + break; + case "delete": + if($playlist->canBeModifiedBy($this->user->identity)) { + $tmOwner = $playlist->getOwner(); + $playlist->delete(); + } else + $this->flashFail("err", "error", tr("access_denied"), null, true); + + $this->returnJson(["success" => true, "id" => $tmOwner->getRealId()]); + break; + default: + break; + } + + $this->returnJson(["success" => true]); + } + + function renderEditPlaylist(int $owner_id, int $virtual_id) + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + + $playlist = $this->audios->getPlaylistByOwnerAndVID($owner_id, $virtual_id); + $page = (int)($this->queryParam("p") ?? 1); + if (!$playlist || $playlist->isDeleted() || !$playlist->canBeModifiedBy($this->user->identity)) + $this->notFound(); + + $this->template->playlist = $playlist; + $this->template->page = $page; + + $audios = iterator_to_array($playlist->fetch(1, $playlist->size())); + $this->template->audios = array_slice($audios, 0, 10); + $audiosIds = []; + + foreach($audios as $aud) + $audiosIds[] = $aud->getId(); + + $this->template->audiosIds = implode(",", array_unique($audiosIds)) . ","; + $this->template->ownerId = $owner_id; + $this->template->owner = $playlist->getOwner(); + $this->template->pagesCount = $pagesCount = ceil($playlist->size() / 10); + + if($_SERVER["REQUEST_METHOD"] !== "POST") + return; + + $title = $this->postParam("title"); + $description = $this->postParam("description"); + $new_audios = !empty($this->postParam("audios")) ? explode(",", rtrim($this->postParam("audios"), ",")) : []; + + if(empty($title) || iconv_strlen($title) < 1) + $this->flashFail("err", tr("error"), tr("set_playlist_name")); + + $playlist->setName(ovk_proc_strtr($title, 125)); + $playlist->setDescription(ovk_proc_strtr($description, 2045)); + $playlist->setEdited(time()); + $playlist->resetLength(); + + if($_FILES["new_cover"]["error"] === UPLOAD_ERR_OK) { + if(!str_starts_with($_FILES["new_cover"]["type"], "image")) + $this->flashFail("err", tr("error"), tr("not_a_photo")); + + try { + $playlist->fastMakeCover($this->user->id, $_FILES["new_cover"]); + } catch(\Throwable $e) { + $this->flashFail("err", tr("error"), tr("invalid_cover_photo")); + } + } + + $playlist->save(); + + DatabaseConnection::i()->getContext()->table("playlist_relations")->where([ + "collection" => $playlist->getId() + ])->delete(); + + foreach ($new_audios as $new_audio) { + $audio = (new Audios)->get((int)$new_audio); + + if(!$audio || $audio->isDeleted()) + continue; + + $playlist->add($audio); + } + + $this->redirect("/playlist".$playlist->getPrettyId()); + } + + function renderPlaylist(int $owner_id, int $virtual_id): void + { + $playlist = $this->audios->getPlaylistByOwnerAndVID($owner_id, $virtual_id); + $page = (int)($this->queryParam("p") ?? 1); + if (!$playlist || $playlist->isDeleted()) + $this->notFound(); + + $this->template->playlist = $playlist; + $this->template->page = $page; + $this->template->audios = iterator_to_array($playlist->fetch($page, 10)); + $this->template->ownerId = $owner_id; + $this->template->owner = $playlist->getOwner(); + $this->template->isBookmarked = $this->user->identity && $playlist->isBookmarkedBy($this->user->identity); + $this->template->isMy = $this->user->identity && $playlist->getOwner()->getId() === $this->user->id; + $this->template->canEdit = $this->user->identity && $playlist->canBeModifiedBy($this->user->identity); + } + + 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"); + $this->redirect("/"); + } + + $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($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($audio->isInLibraryOf($this->user->identity)) + $audio->remove($this->user->identity); + else + $this->flashFail("err", "error", tr("do_not_have_audio"), null, true); + + break; + case "remove_club": + $club = (new Clubs)->get((int)$this->postParam("club")); + + if(!$club || !$club->canBeModifiedBy($this->user->identity)) + $this->flashFail("err", "error", tr("access_denied"), null, true); + + if($audio->isInLibraryOf($club)) + $audio->remove($club); + else + $this->flashFail("err", "error", tr("group_hasnt_audio"), null, true); + + break; + case "add_to_club": + $club = (new Clubs)->get((int)$this->postParam("club")); + + if(!$club || !$club->canBeModifiedBy($this->user->identity)) + $this->flashFail("err", "error", tr("access_denied"), null, true); + + if(!$audio->isInLibraryOf($club)) + $audio->add($club); + else + $this->flashFail("err", "error", tr("group_has_audio"), null, true); + + break; + case "delete": + if($audio->canBeModifiedBy($this->user->identity)) + $audio->delete(); + else + $this->flashFail("err", "error", tr("access_denied"), null, true); + + break; + case "edit": + $audio = $this->audios->get($audio_id); + if (!$audio || $audio->isDeleted() || $audio->isWithdrawn()) + $this->flashFail("err", "error", tr("invalid_audio"), null, true); + + if ($audio->getOwner()->getId() !== $this->user->id) + $this->flashFail("err", "error", tr("access_denied"), null, true); + + $performer = $this->postParam("performer"); + $name = $this->postParam("name"); + $lyrics = $this->postParam("lyrics"); + $genre = empty($this->postParam("genre")) ? "undefined" : $this->postParam("genre"); + $nsfw = (int)($this->postParam("explicit") ?? 0) === 1; + $unlisted = (int)($this->postParam("unlisted") ?? 0) === 1; + 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"), null, true); + + $audio->setName($name); + $audio->setPerformer($performer); + $audio->setLyrics(empty($lyrics) ? NULL : $lyrics); + $audio->setGenre($genre); + $audio->setExplicit($nsfw); + $audio->setSearchability($unlisted); + $audio->setEdited(time()); + $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(), + "unlisted" => $audio->isUnlisted(), + ]]); + break; + + default: + break; + } + + $this->returnJson(["success" => true]); + } + + function renderPlaylists(int $owner) + { + $this->renderList($owner, "playlists"); + } + + function renderApiGetContext() + { + if ($_SERVER["REQUEST_METHOD"] !== "POST") { + header("HTTP/1.1 405 Method Not Allowed"); + $this->redirect("/"); + } + + $ctx_type = $this->postParam("context"); + $ctx_id = (int)($this->postParam("context_entity")); + $page = (int)($this->postParam("page") ?? 1); + $perPage = 10; + + switch($ctx_type) { + default: + case "entity_audios": + if($ctx_id >= 0) { + $entity = $ctx_id != 0 ? (new Users)->get($ctx_id) : $this->user->identity; + + if(!$entity || !$entity->getPrivacyPermission("audios.read", $this->user->identity)) + $this->flashFail("err", "Error", "Can't get queue", 80, true); + + $audios = $this->audios->getByUser($entity, $page, $perPage); + $audiosCount = $this->audios->getUserCollectionSize($entity); + } else { + $entity = (new Clubs)->get(abs($ctx_id)); + + if(!$entity || $entity->isBanned()) + $this->flashFail("err", "Error", "Can't get queue", 80, true); + + $audios = $this->audios->getByClub($entity, $page, $perPage); + $audiosCount = $this->audios->getClubCollectionSize($entity); + } + break; + case "new_audios": + $audios = $this->audios->getNew(); + $audiosCount = $audios->size(); + break; + case "popular_audios": + $audios = $this->audios->getPopular(); + $audiosCount = $audios->size(); + break; + case "playlist_context": + $playlist = $this->audios->getPlaylist($ctx_id); + + if (!$playlist || $playlist->isDeleted()) + $this->flashFail("err", "Error", "Can't get queue", 80, true); + + $audios = $playlist->fetch($page, 10); + $audiosCount = $playlist->size(); + break; + case "search_context": + $stream = $this->audios->search($this->postParam("query"), 2, $this->postParam("type") === "by_performer"); + $audios = $stream->page($page, 10); + $audiosCount = $stream->size(); + break; + } + + $pagesCount = ceil($audiosCount / $perPage); + + # костылёк для получения плееров в пикере аудиозаписей + if((int)($this->postParam("returnPlayers")) === 1) { + $this->template->audios = $audios; + $this->template->page = $page; + $this->template->pagesCount = $pagesCount; + $this->template->count = $audiosCount; + + return 0; + } + + $audiosArr = []; + + foreach($audios as $audio) { + $audiosArr[] = [ + "id" => $audio->getId(), + "name" => $audio->getTitle(), + "performer" => $audio->getPerformer(), + "keys" => $audio->getKeys(), + "url" => $audio->getUrl(), + "length" => $audio->getLength(), + "available" => $audio->isAvailable(), + "withdrawn" => $audio->isWithdrawn(), + ]; + } + + $resultArr = [ + "success" => true, + "page" => $page, + "perPage" => $perPage, + "pagesCount" => $pagesCount, + "count" => $audiosCount, + "items" => $audiosArr, + ]; + + $this->returnJson($resultArr); + } +} diff --git a/Web/Presenters/BlobPresenter.php b/Web/Presenters/BlobPresenter.php index 7bb3e2be..5987281d 100644 --- a/Web/Presenters/BlobPresenter.php +++ b/Web/Presenters/BlobPresenter.php @@ -18,6 +18,8 @@ final class BlobPresenter extends OpenVKPresenter function renderFile(/*string*/ $dir, string $name, string $format) { + header("Access-Control-Allow-Origin: *"); + $dir = $this->getDirName($dir); $base = realpath(OPENVK_ROOT . "/storage/$dir"); $path = realpath(OPENVK_ROOT . "/storage/$dir/$name.$format"); @@ -37,5 +39,5 @@ final class BlobPresenter extends OpenVKPresenter readfile($path); exit; - } + } } diff --git a/Web/Presenters/CommentPresenter.php b/Web/Presenters/CommentPresenter.php index e005af86..86ccb126 100644 --- a/Web/Presenters/CommentPresenter.php +++ b/Web/Presenters/CommentPresenter.php @@ -2,7 +2,7 @@ namespace openvk\Web\Presenters; use openvk\Web\Models\Entities\{Comment, Notifications\MentionNotification, Photo, Video, User, Topic, Post}; use openvk\Web\Models\Entities\Notifications\CommentNotification; -use openvk\Web\Models\Repositories\{Comments, Clubs, Videos, Photos}; +use openvk\Web\Models\Repositories\{Comments, Clubs, Videos, Photos, Audios}; final class CommentPresenter extends OpenVKPresenter { @@ -103,8 +103,27 @@ final class CommentPresenter extends OpenVKPresenter } } } + + $audios = []; + + if(!empty($this->postParam("audios"))) { + $un = rtrim($this->postParam("audios"), ","); + $arr = explode(",", $un); + + if(sizeof($arr) < 11) { + foreach($arr as $dat) { + $ids = explode("_", $dat); + $audio = (new Audios)->getByOwnerAndVID((int)$ids[0], (int)$ids[1]); + + if(!$audio || $audio->isDeleted()) + continue; + + $audios[] = $audio; + } + } + } - if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1) + if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1 && sizeof($audios) < 1) $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_empty")); try { @@ -126,6 +145,9 @@ final class CommentPresenter extends OpenVKPresenter if(sizeof($videos) > 0) foreach($videos as $vid) $comment->attach($vid); + + foreach($audios as $audio) + $comment->attach($audio); if($entity->getOwner()->getId() !== $this->user->identity->getId()) if(($owner = $entity->getOwner()) instanceof User) diff --git a/Web/Presenters/GroupPresenter.php b/Web/Presenters/GroupPresenter.php index d3a46fd5..eb8f446c 100644 --- a/Web/Presenters/GroupPresenter.php +++ b/Web/Presenters/GroupPresenter.php @@ -3,7 +3,7 @@ namespace openvk\Web\Presenters; use openvk\Web\Models\Entities\{Club, Photo, Post}; use Nette\InvalidStateException; use openvk\Web\Models\Entities\Notifications\ClubModeratorNotification; -use openvk\Web\Models\Repositories\{Clubs, Users, Albums, Managers, Topics}; +use openvk\Web\Models\Repositories\{Clubs, Users, Albums, Managers, Topics, Audios}; use Chandler\Security\Authenticator; final class GroupPresenter extends OpenVKPresenter @@ -31,6 +31,8 @@ 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)->getRandomThreeAudiosByEntityId($club->getRealId()); + $this->template->audiosCount = (new Audios)->getClubCollectionSize($club); } $this->template->club = $club; @@ -218,6 +220,7 @@ final class GroupPresenter extends OpenVKPresenter $club->setAdministrators_List_Display(empty($this->postParam("administrators_list_display")) ? 0 : $this->postParam("administrators_list_display")); $club->setEveryone_Can_Create_Topics(empty($this->postParam("everyone_can_create_topics")) ? 0 : 1); $club->setDisplay_Topics_Above_Wall(empty($this->postParam("display_topics_above_wall")) ? 0 : 1); + $club->setEveryone_can_upload_audios(empty($this->postParam("upload_audios")) ? 0 : 1); $club->setHide_From_Global_Feed(empty($this->postParam("hide_from_global_feed")) ? 0 : 1); $website = $this->postParam("website") ?? ""; diff --git a/Web/Presenters/MessengerPresenter.php b/Web/Presenters/MessengerPresenter.php index d5ffb988..e04e1adc 100644 --- a/Web/Presenters/MessengerPresenter.php +++ b/Web/Presenters/MessengerPresenter.php @@ -128,7 +128,7 @@ final class MessengerPresenter extends OpenVKPresenter $messages = []; $correspondence = new Correspondence($this->user->identity, $correspondent); - foreach($correspondence->getMessages(1, $lastMsg === 0 ? NULL : $lastMsg) as $message) + foreach($correspondence->getMessages(1, $lastMsg === 0 ? NULL : $lastMsg, NULL, 0) as $message) $messages[] = $message->simplify(); header("Content-Type: application/json"); diff --git a/Web/Presenters/OpenVKPresenter.php b/Web/Presenters/OpenVKPresenter.php index 80ab0621..a171c2e3 100644 --- a/Web/Presenters/OpenVKPresenter.php +++ b/Web/Presenters/OpenVKPresenter.php @@ -198,6 +198,9 @@ abstract class OpenVKPresenter extends SimplePresenter { $user = Authenticator::i()->getUser(); + if(!$this->template) + $this->template = new \stdClass; + $this->template->isXmas = intval(date('d')) >= 1 && date('m') == 12 || intval(date('d')) <= 15 && date('m') == 1 ? true : false; $this->template->isTimezoned = Session::i()->get("_timezoneOffset"); diff --git a/Web/Presenters/PhotosPresenter.php b/Web/Presenters/PhotosPresenter.php index 0a8b87e4..aeb8ba1e 100644 --- a/Web/Presenters/PhotosPresenter.php +++ b/Web/Presenters/PhotosPresenter.php @@ -336,7 +336,10 @@ final class PhotosPresenter extends OpenVKPresenter if(is_null($this->user) || $this->user->id != $ownerId) $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); - $redirect = $photo->getAlbum()->getOwner() instanceof User ? "/id0" : "/club" . $ownerId; + if(!is_null($album = $photo->getAlbum())) + $redirect = $album->getOwner() instanceof User ? "/id0" : "/club" . $ownerId; + else + $redirect = "/id0"; $photo->isolate(); $photo->delete(); diff --git a/Web/Presenters/ReportPresenter.php b/Web/Presenters/ReportPresenter.php index a87154c8..0c4cbd83 100644 --- a/Web/Presenters/ReportPresenter.php +++ b/Web/Presenters/ReportPresenter.php @@ -23,7 +23,7 @@ final class ReportPresenter extends OpenVKPresenter if ($_SERVER["REQUEST_METHOD"] === "POST") $this->assertNoCSRF(); - $act = in_array($this->queryParam("act"), ["post", "photo", "video", "group", "comment", "note", "app", "user"]) ? $this->queryParam("act") : NULL; + $act = in_array($this->queryParam("act"), ["post", "photo", "video", "group", "comment", "note", "app", "user", "audio"]) ? $this->queryParam("act") : NULL; if (!$this->queryParam("orig")) { $this->template->reports = $this->reports->getReports(0, (int)($this->queryParam("p") ?? 1), $act, $_SERVER["REQUEST_METHOD"] !== "POST"); @@ -88,7 +88,7 @@ final class ReportPresenter extends OpenVKPresenter if(!$id) exit(json_encode([ "error" => tr("error_segmentation") ])); - if(in_array($this->queryParam("type"), ["post", "photo", "video", "group", "comment", "note", "app", "user"])) { + if(in_array($this->queryParam("type"), ["post", "photo", "video", "group", "comment", "note", "app", "user", "audio"])) { if (count(iterator_to_array($this->reports->getDuplicates($this->queryParam("type"), $id, NULL, $this->user->id))) <= 0) { $report = new Report; $report->setUser_id($this->user->id); diff --git a/Web/Presenters/SearchPresenter.php b/Web/Presenters/SearchPresenter.php index fadf9954..d80c06c3 100644 --- a/Web/Presenters/SearchPresenter.php +++ b/Web/Presenters/SearchPresenter.php @@ -1,7 +1,7 @@ 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,18 +96,21 @@ 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."); $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; } } diff --git a/Web/Presenters/SupportPresenter.php b/Web/Presenters/SupportPresenter.php index 8163dbf8..b834d712 100644 --- a/Web/Presenters/SupportPresenter.php +++ b/Web/Presenters/SupportPresenter.php @@ -67,7 +67,7 @@ final class SupportPresenter extends OpenVKPresenter $this->template->count = $this->tickets->getTicketsCountByUserId($this->user->id); if($this->template->mode === "list") { $this->template->page = (int) ($this->queryParam("p") ?? 1); - $this->template->tickets = $this->tickets->getTicketsByUserId($this->user->id, $this->template->page); + $this->template->tickets = iterator_to_array($this->tickets->getTicketsByUserId($this->user->id, $this->template->page)); } if($this->template->mode === "new") diff --git a/Web/Presenters/UserPresenter.php b/Web/Presenters/UserPresenter.php index 51ddc6aa..56d5685b 100644 --- a/Web/Presenters/UserPresenter.php +++ b/Web/Presenters/UserPresenter.php @@ -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,7 +45,10 @@ 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)->getRandomThreeAudiosByEntityId($user->getId()); + $this->template->audiosCount = (new Audios)->getUserCollectionSize($user); + $this->template->audioStatus = $user->getCurrentAudioStatus(); + $this->template->user = $user; } } @@ -55,7 +58,7 @@ final class UserPresenter extends OpenVKPresenter $this->assertUserLoggedIn(); $user = $this->users->get($id); - $page = abs($this->queryParam("p") ?? 1); + $page = abs((int)($this->queryParam("p") ?? 1)); if(!$user) $this->notFound(); elseif (!$user->getPrivacyPermission('friends.read', $this->user->identity ?? NULL)) @@ -169,6 +172,7 @@ final class UserPresenter extends OpenVKPresenter if ($this->postParam("gender") <= 1 && $this->postParam("gender") >= 0) $user->setSex($this->postParam("gender")); + $user->setAudio_broadcast_enabled($this->checkbox("broadcast_music")); if(!empty($this->postParam("phone")) && $this->postParam("phone") !== $user->getPhone()) { if(!OPENVK_ROOT_CONF["openvk"]["credentials"]["smsc"]["enable"]) @@ -241,6 +245,7 @@ final class UserPresenter extends OpenVKPresenter } $user->setStatus(empty($this->postParam("status")) ? NULL : $this->postParam("status")); + $user->setAudio_broadcast_enabled($this->postParam("broadcast") == 1); $user->save(); $this->returnJson([ @@ -430,10 +435,11 @@ final class UserPresenter extends OpenVKPresenter "friends.add", "wall.write", "messages.write", + "audios.read", ]; foreach($settings as $setting) { $input = $this->postParam(str_replace(".", "_", $setting)); - $user->setPrivacySetting($setting, min(3, abs($input ?? $user->getPrivacySetting($setting)))); + $user->setPrivacySetting($setting, min(3, (int)abs((int)$input ?? $user->getPrivacySetting($setting)))); } } else if($_GET['act'] === "finance.top-up") { $token = $this->postParam("key0") . $this->postParam("key1") . $this->postParam("key2") . $this->postParam("key3"); @@ -474,6 +480,7 @@ final class UserPresenter extends OpenVKPresenter } else if($_GET['act'] === "lMenu") { $settings = [ "menu_bildoj" => "photos", + "menu_muziko" => "audios", "menu_filmetoj" => "videos", "menu_mesagoj" => "messages", "menu_notatoj" => "notes", diff --git a/Web/Presenters/VKAPIPresenter.php b/Web/Presenters/VKAPIPresenter.php index 431379c5..963c9ccc 100644 --- a/Web/Presenters/VKAPIPresenter.php +++ b/Web/Presenters/VKAPIPresenter.php @@ -233,8 +233,13 @@ final class VKAPIPresenter extends OpenVKPresenter $this->badMethodCall($object, $method, $parameter->getName()); } - settype($val, $parameter->getType()->getName()); - $params[] = $val; + try { + settype($val, $parameter->getType()->getName()); + $params[] = $val; + } catch (\Throwable $e) { + // Just ignore the exception, since + // some args are intended for internal use + } } define("VKAPI_DECL_VER", $this->requestParam("v") ?? "4.100", false); diff --git a/Web/Presenters/WallPresenter.php b/Web/Presenters/WallPresenter.php index 2f9d611d..a01c8262 100644 --- a/Web/Presenters/WallPresenter.php +++ b/Web/Presenters/WallPresenter.php @@ -3,7 +3,7 @@ namespace openvk\Web\Presenters; use openvk\Web\Models\Exceptions\TooMuchOptionsException; use openvk\Web\Models\Entities\{Poll, Post, Photo, Video, Club, User}; use openvk\Web\Models\Entities\Notifications\{MentionNotification, RepostNotification, WallPostNotification}; -use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums, Notes, Videos, Comments, Photos}; +use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums, Notes, Videos, Comments, Photos, Audios}; use Chandler\Database\DatabaseConnection; use Nette\InvalidStateException as ISE; use Bhaktaraz\RSSGenerator\Item; @@ -154,7 +154,7 @@ final class WallPresenter extends OpenVKPresenter $this->template->paginatorConf = (object) [ "count" => sizeof($posts), "page" => (int) ($_GET["p"] ?? 1), - "amount" => sizeof($posts->page((int) ($_GET["p"] ?? 1), $perPage)), + "amount" => $posts->page((int) ($_GET["p"] ?? 1), $perPage)->count(), "perPage" => $perPage, ]; $this->template->posts = []; @@ -182,7 +182,7 @@ final class WallPresenter extends OpenVKPresenter $this->template->paginatorConf = (object) [ "count" => $count, "page" => (int) ($_GET["p"] ?? 1), - "amount" => sizeof($posts), + "amount" => $posts->getRowCount(), "perPage" => $pPage, ]; foreach($posts as $post) @@ -311,8 +311,27 @@ final class WallPresenter extends OpenVKPresenter } } } + + $audios = []; + + if(!empty($this->postParam("audios"))) { + $un = rtrim($this->postParam("audios"), ","); + $arr = explode(",", $un); + + if(sizeof($arr) < 11) { + foreach($arr as $dat) { + $ids = explode("_", $dat); + $audio = (new Audios)->getByOwnerAndVID((int)$ids[0], (int)$ids[1]); + + if(!$audio || $audio->isDeleted()) + continue; + + $audios[] = $audio; + } + } + } - if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1 && !$poll && !$note) + if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1 && sizeof($audios) < 1 && !$poll && !$note) $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big")); try { @@ -341,6 +360,9 @@ final class WallPresenter extends OpenVKPresenter if(!is_null($note)) $post->attach($note); + + foreach($audios as $audio) + $post->attach($audio); if($wall > 0 && $wall !== $this->user->identity->getId()) (new WallPostNotification($wallOwner, $post, $this->user->identity))->emit(); diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml index f8a975e0..31e0bc5c 100644 --- a/Web/Presenters/templates/@layout.xml +++ b/Web/Presenters/templates/@layout.xml @@ -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 @@ +
diff --git a/Web/Presenters/templates/Admin/@layout.xml b/Web/Presenters/templates/Admin/@layout.xml index f254dd9a..055bd0a0 100644 --- a/Web/Presenters/templates/Admin/@layout.xml +++ b/Web/Presenters/templates/Admin/@layout.xml @@ -97,6 +97,9 @@ID | +{_admin_author} | +{_peformer} | +{_admin_title} | +{_genre} | +Explicit | +{_withdrawn} | +{_deleted} | +{_created} | +{_actions} | +
---|---|---|---|---|---|---|---|---|---|
{$audio->getId()} | +
+ {var $owner = $audio->getOwner()}
+
+
+ |
+ {$audio->getPerformer()} | +{$audio->getTitle()} | +{$audio->getGenre()} | +{$audio->isExplicit() ? tr("yes") : tr("no")} | ++ {$audio->isWithdrawn() ? tr("yes") : tr("no")} + | ++ {$audio->isDeleted() ? tr("yes") : tr("no")} + | +{$audio->getPublicationTime()} | ++ + + + | +
ID | +{_admin_author} | +{_name} | +{_created_playlist} | +{_actions} | +|||||
{$playlist->getId()} | +
+ {var $owner = $playlist->getOwner()}
+
+
+ |
+
+
+
+ |
+ {$playlist->getCreationTime()} | ++ + + + | +
+- {_supported_formats}
- {_max_load_photos}
diff --git a/Web/Presenters/templates/Report/Tabs.xml b/Web/Presenters/templates/Report/Tabs.xml index e878ccb3..6003f202 100644 --- a/Web/Presenters/templates/Report/Tabs.xml +++ b/Web/Presenters/templates/Report/Tabs.xml @@ -46,6 +46,9 @@
Пользователи
+
+ {_audios}
+
{if $graffiti}
diff --git a/Web/di.yml b/Web/di.yml
index 57d46f14..81e06117 100644
--- a/Web/di.yml
+++ b/Web/di.yml
@@ -7,6 +7,7 @@ services:
- openvk\Web\Presenters\CommentPresenter
- openvk\Web\Presenters\PhotosPresenter
- openvk\Web\Presenters\VideosPresenter
+ - openvk\Web\Presenters\AudioPresenter
- openvk\Web\Presenters\BlobPresenter
- openvk\Web\Presenters\GroupPresenter
- openvk\Web\Presenters\SearchPresenter
@@ -33,6 +34,7 @@ services:
- openvk\Web\Models\Repositories\Albums
- openvk\Web\Models\Repositories\Clubs
- openvk\Web\Models\Repositories\Videos
+ - openvk\Web\Models\Repositories\Audios
- openvk\Web\Models\Repositories\Notes
- openvk\Web\Models\Repositories\Tickets
- openvk\Web\Models\Repositories\Messages
diff --git a/Web/routes.yml b/Web/routes.yml
index dea8ddd5..d12ccbc0 100644
--- a/Web/routes.yml
+++ b/Web/routes.yml
@@ -187,6 +187,34 @@ routes:
handler: "Videos->edit"
- url: "/video{num}_{num}/remove"
handler: "Videos->remove"
+ - url: "/player/upload"
+ handler: "Audio->upload"
+ - url: "/audios{num}"
+ handler: "Audio->list"
+ - url: "/audios/popular"
+ handler: "Audio->popular"
+ - url: "/audios/new"
+ handler: "Audio->new"
+ - url: "/audio{num}_{num}/embed.xhtml"
+ handler: "Audio->embed"
+ - url: "/audio{num}/listen"
+ handler: "Audio->listen"
+ - url: "/audios/search"
+ handler: "Audio->search"
+ - url: "/audios/newPlaylist"
+ handler: "Audio->newPlaylist"
+ - url: "/audios/context"
+ handler: "Audio->apiGetContext"
+ - url: "/playlist{num}_{num}"
+ handler: "Audio->playlist"
+ - url: "/playlist{num}_{num}/edit"
+ handler: "Audio->editPlaylist"
+ - url: "/playlist{num}/action"
+ handler: "Audio->playlistAction"
+ - url: "/playlists{num}"
+ handler: "Audio->playlists"
+ - url: "/audio{num}/action"
+ handler: "Audio->action"
- url: "/{?!club}{num}"
handler: "Group->view"
placeholders:
@@ -221,24 +249,6 @@ routes:
handler: "Topics->edit"
- url: "/topic{num}_{num}/delete"
handler: "Topics->delete"
- - url: "/audios{num}"
- handler: "Audios->app"
- - url: "/audios{num}.json"
- handler: "Audios->apiListSongs"
- - url: "/audios/popular.json"
- handler: "Audios->apiListPopSongs"
- - url: "/audios/playlist{num}.json"
- handler: "Audios->apiListPlaylists"
- - url: "/audios/search.json"
- handler: "Audios->apiSearch"
- - url: "/audios/add.json"
- handler: "Audios->apiAdd"
- - url: "/audios/playlist.json"
- handler: "Audios->apiAddPlaylist"
- - url: "/audios/upload.json"
- handler: "Audios->apiUpload"
- - url: "/audios/beacon"
- handler: "Audios->apiBeacon"
- url: "/im"
handler: "Messenger->index"
- url: "/im/sel{num}"
@@ -341,6 +351,12 @@ routes:
handler: "Admin->bannedLink"
- url: "/admin/bannedLink/id{num}/unban"
handler: "Admin->unbanLink"
+ - url: "/admin/music"
+ handler: "Admin->music"
+ - url: "/admin/music/{num}/edit"
+ handler: "Admin->editMusic"
+ - url: "/admin/playlist/{num}/edit"
+ handler: "Admin->editPlaylist"
- url: "/admin/user{num}/bans"
handler: "Admin->bansHistory"
- url: "/upload/photo/{text}"
diff --git a/Web/static/css/audios.css b/Web/static/css/audios.css
new file mode 100644
index 00000000..5decbb89
--- /dev/null
+++ b/Web/static/css/audios.css
@@ -0,0 +1,661 @@
+.noOverflow {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.overflowedName {
+ position: absolute;
+ z-index: 99;
+}
+
+.musicIcon {
+ background-image: url('/assets/packages/static/openvk/img/audios_controls.png');
+ background-repeat: no-repeat;
+ cursor: pointer;
+}
+
+.musicIcon:hover {
+ filter: brightness(99%);
+}
+
+.musicIcon.pressed {
+ filter: brightness(150%);
+}
+
+.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);
+ position: relative;
+}
+
+.bigPlayer.floating {
+ position: fixed;
+ z-index: 199;
+ width: 627px;
+ margin-top: -76px;
+}
+
+.bigPlayer .paddingLayer {
+ padding: 0px 0px 0px 14px;
+}
+
+.bigPlayer .paddingLayer .playButtons {
+ padding: 12px 0px;
+}
+
+.bigPlayer .paddingLayer .playButtons .playButton {
+ width: 22px;
+ height: 22px;
+ float: left;
+ background-position-x: -72px;
+}
+
+.bigPlayer .paddingLayer .playButtons .playButton.pause {
+ background-position-x: -168px;
+}
+
+.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: 11%;
+}
+
+.bigPlayer .paddingLayer .additionalButtons .repeatButton {
+ width: 14px;
+ height: 16px;
+ background-position-y: -49px;
+ background-position-x: -31px;
+ margin-left: 7px;
+ float: left;
+}
+
+.broadcastButton {
+ width: 16px;
+ height: 12px;
+ background-position-y: -50px;
+ background-position-x: -64px;
+ margin-left: 6px;
+ float: left;
+}
+
+.broadcastButton.atProfile {
+ width: 13px;
+ height: 12px;
+ background-position-y: -50px;
+ background-position-x: -64px;
+ margin-left: 0px !important;
+ margin-right: 5px;
+ float: left;
+}
+
+.bigPlayer .paddingLayer .additionalButtons .shuffleButton {
+ width: 14px;
+ height: 16px;
+ background-position: -50px -50px;
+ margin-left: 7px;
+ float: left;
+}
+
+.bigPlayer .paddingLayer .additionalButtons .deviceButton {
+ width: 12px;
+ height: 16px;
+ background-position: -202px -50px;
+ margin-left: 7px;
+ 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: 63%;
+ position: relative;
+}
+
+.bigPlayer .paddingLayer .bigPlayerTip {
+ display: none;
+ z-index: 999;
+ background: #cecece;
+ padding: 3px;
+ top: -3px;
+ position: absolute;
+ transition: all .1s ease-out;
+ user-select: none;
+}
+
+.bigPlayer .paddingLayer .volumePanel {
+ width: 73px;
+ float: left;
+}
+
+.bigPlayer .paddingLayer .slider, .audioEmbed .track .slider {
+ width: 18px;
+ height: 7px;
+ background: #606060;
+ position: absolute;
+ bottom: 0;
+ top: 0px;
+ pointer-events: none;
+}
+
+.bigPlayer .paddingLayer .trackInfo .timer {
+ float: right;
+ margin-right: 8px;
+ font-size: 10px;
+}
+
+.bigPlayer .paddingLayer .trackInfo .trackName {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width: 81%;
+ display: inline-block;
+}
+
+.bigPlayer .paddingLayer .trackInfo .timer span {
+ font-size: 10px;
+}
+
+.bigPlayer .paddingLayer .trackInfo b:hover {
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+.audioEmbed .track > .selectableTrack, .bigPlayer .selectableTrack {
+ margin-top: 3px;
+ width: calc(100% - 8px);
+ border-top: #606060 1px solid;
+ height: 6px;
+ position: relative;
+ user-select: none;
+}
+
+#audioEmbed {
+ cursor: pointer;
+ 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.nowPlaying {
+ background: #606060;
+ border: 1px solid #4f4f4f;
+ box-sizing: border-box;
+}
+
+.audioEntry.nowPlaying .playIcon {
+ background-position-y: -16px !important;
+}
+
+.audioEntry.nowPlaying:hover {
+ background: #4e4e4e !important;
+}
+
+.audioEntry.nowPlaying .performer a {
+ color: #f4f4f4 !important;
+}
+
+.audioEntry .performer a {
+ color: #4C4C4C;
+}
+
+.audioEntry.nowPlaying .title {
+ color: #fff;
+}
+
+.audioEntry.nowPlaying .status {
+ color: white;
+}
+
+.audioEntry.nowPlaying .volume .nobold {
+ color: white !important;
+}
+
+.audioEntry.nowPlaying .buttons .musicIcon, .audioEntry.nowPlaying .explicitMark {
+ filter: brightness(187%) opacity(72%);
+}
+
+.audioEntry {
+ height: 100%;
+ position: relative;
+ width: 100%;
+}
+
+.audioEntry .playerButton {
+ position: relative;
+ padding: 10px 9px 9px 9px;
+ width: 16px;
+ height: 16px;
+}
+
+.audioEntry .subTracks {
+ display: flex;
+ padding-bottom: 5px;
+ padding-left: 8px;
+ padding-right: 12px;
+}
+
+.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 {
+ overflow: hidden;
+ display: grid;
+ grid-template-columns: 1fr;
+ width: 85%;
+ height: 23px;
+}
+
+.audioEntry .status strong {
+ color: #4C4C4C;
+}
+
+.audioEmbed .track {
+ display: none;
+ padding: 4px 0;
+}
+
+.audioEmbed .track, .audioEmbed.playing .track {
+ display: unset;
+}
+
+.audioEntry:hover {
+ background: #EEF0F2;
+ border-radius: 2px;
+}
+
+.audioEntry:hover .buttons {
+ display: block;
+}
+
+.audioEntry:hover .volume .hideOnHover {
+ display: none;
+}
+
+.audioEntry .buttons {
+ display: none;
+ width: 62px;
+ height: 20px;
+ position: absolute;
+ right: 3%;
+ top: 2px;
+ /* чтоб избежать заедания во время ховера кнопки добавления */
+ clip-path: inset(0 0 0 0);
+}
+
+.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;
+ margin-left: 2px;
+}
+
+.audioEntry .buttons .add-icon-group {
+ width: 14px;
+ height: 11px;
+ float: right;
+ background-position: -94px -52px;
+ margin-top: 3px;
+ transition: margin-right 0.1s ease-out, opacity 0.1s ease-out;
+}
+
+.audioEntry .buttons .report-icon {
+ width: 12px;
+ height: 11px;
+ float: right;
+ background-position: -67px -51px;
+ margin-top: 3px;
+ margin-right: 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;
+ margin-left: 2px;
+ float: right;
+ background-position: -108px -52px;
+}
+
+.audioEntry .buttons .remove-icon-group {
+ margin-top: 3px;
+ width: 13px;
+ height: 11px;
+ float: right;
+ background-position: -122px -52px;
+ margin-left: 3px;
+ margin-right: 3px;
+}
+
+.audioEmbed .lyrics {
+ display: none;
+ padding: 6px 33px 10px 33px;
+}
+
+.audioEmbed .lyrics.showed {
+ display: block !important;
+}
+
+.audioEntry .withLyrics {
+ user-select: none;
+ color: #507597;
+}
+
+.audioEntry .withLyrics:hover {
+ text-decoration: underline;
+}
+
+.audioEmbed.withdrawn .status > *, .audioEmbed.processed .status > *, .audioEmbed.withdrawn .playerButton > *, .audioEmbed.processed .playerButton > * {
+ pointer-events: none;
+}
+
+.audioEmbed.withdrawn {
+ filter: opacity(0.8);
+}
+
+.playlistCover img {
+ max-width: 135px;
+ max-height: 135px;
+}
+
+.playlistBlock {
+ margin-top: 14px;
+}
+
+.playlistContainer {
+ display: grid;
+ grid-template-columns: repeat(3, 146px);
+ gap: 18px 10px;
+}
+
+.playlistContainer .playlistCover {
+ width: 111px;
+ height: 111px;
+ display: flex;
+ background: #c4c4c4;
+}
+
+.playlistContainer .playlistCover img {
+ max-width: 111px;
+ max-height: 111px;
+ margin: auto;
+}
+
+.ovk-diag-body .searchBox {
+ background: #e6e6e6;
+ padding-top: 10px;
+ height: 35px;
+ padding-left: 10px;
+ padding-right: 10px;
+ display: flex;
+}
+
+.ovk-diag-body .searchBox input {
+ height: 24px;
+ margin-right: -1px;
+ width: 77%;
+}
+
+.ovk-diag-body .searchBox select {
+ width: 29%;
+ padding-left: 8px;
+ height: 24px;
+}
+
+.ovk-diag-body .audiosInsert {
+ height: 82%;
+ padding: 9px 5px 9px 9px;
+ overflow-y: auto;
+}
+
+.attachAudio {
+ float: left;
+ width: 28%;
+ height: 26px;
+ padding-top: 11px;
+ text-align: center;
+}
+
+.attachAudio span {
+ user-select: none;
+}
+
+.attachAudio:hover {
+ background: rgb(236, 236, 236);
+ cursor: pointer;
+}
+
+.playlistCover img {
+ cursor: pointer;
+}
+
+.explicitMark {
+ margin-top: 2px;
+ margin-left: 3px;
+ width: 11px;
+ height: 11px;
+ background: url('/assets/packages/static/openvk/img/explicit.svg');
+ background-repeat: no-repeat;
+}
+
+.audioStatus span {
+ color: #2B587A;
+}
+
+.audioStatus span:hover {
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+.audioStatus {
+ padding-top: 2px;
+ padding-bottom: 3px;
+}
+
+/* 🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣*/
+.audiosDiv center span {
+ color: #707070;
+ margin: 120px 0px !important;
+ display: block;
+}
+
+.audiosDiv center {
+ margin-left: -10px;
+}
+
+.playlistInfo {
+ display: flex;
+ flex-direction: column;
+}
+
+.playlistInfo .playlistName {
+ font-weight: 600;
+}
+
+.searchList.floating {
+ position: fixed;
+ z-index: 199;
+ width: 156px;
+ margin-top: -65px !important;
+}
+
+.audiosSearchBox input[type='search'] {
+ height: 25px;
+ width: 77%;
+ padding-left: 21px;
+ padding-top: 4px;
+ background: rgb(255, 255, 255) url("/assets/packages/static/openvk/img/search_icon.png") 5px 6px no-repeat;
+}
+
+.audiosSearchBox {
+ padding-bottom: 10px;
+ padding-top: 7px;
+ display: flex;
+}
+
+.audiosSearchBox select {
+ width: 30%;
+ padding-left: 7px;
+ margin-left: -2px;
+}
+
+.audioStatus {
+ color: #2B587A;
+ margin-top: -3px;
+}
+
+.audioStatus::before {
+ background-image: url('/assets/packages/static/openvk/img/audios_controls.png');
+ background-repeat: no-repeat;
+ width: 11px;
+ height: 11px;
+ background-position: -66px -51px;
+ margin-top: 1px;
+ display: inline-block;
+ vertical-align: bottom;
+ content: "";
+ padding-right: 2px;
+}
+
+.friendsAudiosList {
+ margin-left: -7px;
+ margin-top: 8px;
+}
+
+.friendsAudiosList .elem {
+ display: flex;
+ padding: 1px 1px;
+ width: 148px;
+}
+
+.friendsAudiosList .elem img {
+ width: 30px;
+ border-radius: 2px;
+ object-fit: cover;
+ height: 31px;
+ min-width: 30px;
+}
+
+.friendsAudiosList .elem .additionalInfo {
+ margin-left: 7px;
+ padding-top: 1px;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.friendsAudiosList .elem .additionalInfo .name {
+ color: #2B587A;
+}
+
+.friendsAudiosList #used .elem .additionalInfo .name {
+ color: #F4F4F4;
+}
+
+.friendsAudiosList .elem .additionalInfo .desc {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ color: #878A8F;
+ font-size: 11px;
+}
+
+.friendsAudiosList #used .elem .additionalInfo .desc {
+ color: #F4F4F4;
+}
+
+.friendsAudiosList .elem:hover {
+ background: #E8EBF0;
+ cursor: pointer;
+}
+
+.friendsAudiosList #used .elem:hover {
+ background: #787878;
+ cursor: pointer;
+}
+
+.editContainer {
+ display:table;
+ clear:both;
+ width:100%;
+ margin-top: 10px;
+}
+
+.editContainer .playerContainer {
+ width: 78%;
+ float: left;
+ max-width: 78%;
+ min-width: 68%;
+}
+
+.addToPlaylist {
+ width: 22%;
+}
diff --git a/Web/static/css/avatar.2.css b/Web/static/css/avatar.2.css
index 2990eb3d..42daa14d 100644
--- a/Web/static/css/avatar.2.css
+++ b/Web/static/css/avatar.2.css
@@ -81,3 +81,10 @@ div.ovk-video > div > img
object-fit: cover;
border-radius: 100px;
}
+
+.friendsAudiosList .elem img {
+ width: 30px;
+ border-radius: 100px;
+ height: 31px;
+ min-width: 30px;
+}
diff --git a/Web/static/css/main.css b/Web/static/css/main.css
index 47d67317..7a087705 100644
--- a/Web/static/css/main.css
+++ b/Web/static/css/main.css
@@ -405,6 +405,7 @@ h1 {
width: 200px;
text-align: left;
cursor: pointer;
+ font-family: tahoma, verdana, arial, sans-serif;
}
.profile_link_form {
@@ -1242,64 +1243,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;
@@ -1476,7 +1419,7 @@ body.scrolled .toTop:hover {
display: none;
}
-.post-has-videos {
+.post-has-videos, .post-has-audios {
margin-top: 11px;
margin-left: 3px;
color: #3c3c3c;
@@ -1493,7 +1436,7 @@ body.scrolled .toTop:hover {
margin-left: 2px;
}
-.post-has-video {
+.post-has-video, .post-has-audio {
padding-bottom: 4px;
cursor: pointer;
}
@@ -1502,6 +1445,10 @@ body.scrolled .toTop:hover {
text-decoration: underline;
}
+.post-has-audio:hover span {
+ text-decoration: underline;
+}
+
.post-has-video::before {
content: " ";
width: 14px;
@@ -1515,6 +1462,19 @@ body.scrolled .toTop:hover {
margin-bottom: -1px;
}
+.post-has-audio::before {
+ content: " ";
+ width: 14px;
+ height: 15px;
+ display: inline-block;
+ vertical-align: bottom;
+ background-image: url("/assets/packages/static/openvk/img/audio.png");
+ background-repeat: no-repeat;
+ margin: 3px;
+ margin-left: 2px;
+ margin-bottom: -1px;
+}
+
.post-opts {
margin-top: 10px;
}
@@ -2122,6 +2082,45 @@ table td[width="120"] {
margin: 0 auto;
}
+#upload_container {
+ background: white;
+ padding: 30px 80px 20px;
+ margin: 10px 25px 30px;
+ border: 1px solid #d6d6d6;
+}
+
+#upload_container h4 {
+ border-bottom: solid 1px #daE1E8;
+ text-align: left;
+ padding: 0 0 4px 0;
+ margin: 0;
+}
+
+#audio_upload {
+ width: 350px;
+ margin: 20px auto;
+ margin-bottom: 10px;
+ padding: 15px 0;
+ border: 2px solid #ccc;
+ background-color: #EFEFEF;
+ text-align: center;
+}
+
+ul {
+ list-style: url(/assets/packages/static/openvk/img/bullet.gif) outside;
+ margin: 10px 0;
+ padding-left: 30px;
+ color: black;
+}
+
+li {
+ padding: 1px 0;
+}
+
+#upload_container ul {
+ padding-left: 15px;
+}
+
#votesBalance {
margin-top: 10px;
padding: 7px;
@@ -2472,8 +2471,7 @@ a.poll-retract-vote {
display: none;
}
-.searchOptions
-{
+.searchOptions {
overflow: hidden;
width:25.5%;
border-top:1px solid #E5E7E6;
@@ -2484,8 +2482,7 @@ a.poll-retract-vote {
margin-right: -7px;
}
-.searchBtn
-{
+.searchBtn {
border: solid 1px #575757;
background-color: #696969;
color:white;
@@ -2497,52 +2494,47 @@ 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;
- border: solid 0.125rem #696969;
- background:linear-gradient(#888888,#858585);
+ border: solid 0.125rem #4F4F4F;
+ background: #606060;
margin-bottom:2px;
padding-left:9px;
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;
@@ -2553,26 +2545,27 @@ a.poll-retract-vote {
padding-left:9px;
}
-.searchList li a
-{
+.searchList li a {
min-width:100%;
}
-.searchList li:hover
-{
- margin-left:0px;
- color: #2B587A !important;
- background:#ebebeb;
- padding:2px;
- padding-top:5px;
- padding-bottom:5px;
- margin-bottom:2px;
- padding-left:9px;
- width:91%;
+.searchList a {
+ min-width: 88%;
}
-.whatFind
-{
+.searchList a:hover {
+ margin-left: 0px;
+ color: #2B587A !important;
+ background: #ebebeb;
+ padding: 2px;
+ padding-top: 5px;
+ padding-bottom: 5px;
+ margin-bottom: 2px;
+ padding-left: 9px;
+ width: 89.9%;
+}
+
+.whatFind {
color:rgb(128, 128, 128);
background:none;
border:none;
@@ -2584,8 +2577,7 @@ a.poll-retract-vote {
margin-top: 0.5px;
}
-.searchOptionName
-{
+.searchOptionName {
cursor:pointer;
background-color: #EAEAEA;
padding-left:5px;
@@ -2597,8 +2589,7 @@ a.poll-retract-vote {
border-bottom: 2px solid #E4E4E4;
}
-.searchOption
-{
+.searchOption {
user-select: none;
}
@@ -2875,7 +2866,7 @@ body.article .floating_sidebar, body.article .page_content {
.lagged {
filter: opacity(0.5);
- cursor: progress;
+ cursor: not-allowed;
user-select: none;
}
@@ -2889,7 +2880,7 @@ body.article .floating_sidebar, body.article .page_content {
pointer-events: none;
}
-.lagged * {
+.lagged *, .lagged {
pointer-events: none;
}
@@ -2974,3 +2965,40 @@ 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;
+}
+
+hr {
+ background-color: #d8d8d8;
+ border: none;
+ height: 1px;
+}
+
+.searchList hr {
+ width: 153px;
+ margin-left: 0px;
+ margin-top: 6px;
+}
+
+.showMore, .showMoreAudiosPlaylist {
+ width: 100%;
+ text-align: center;
+ background: #d5d5d5;
+ height: 22px;
+ padding-top: 9px;
+ cursor: pointer;
+}
+
+#upload_container.uploading {
+ background: white url('/assets/packages/static/openvk/img/progressbar.gif') !important;
+ background-position-x: 0% !important;
+ background-position-y: 0% !important;
+ background-repeat: repeat !important;
+ background-repeat: no-repeat !important;
+ background-position: 50% !important;
+}
diff --git a/Web/static/img/audio.png b/Web/static/img/audio.png
new file mode 100644
index 00000000..62d9dff2
Binary files /dev/null and b/Web/static/img/audio.png differ
diff --git a/Web/static/img/audios_controls.png b/Web/static/img/audios_controls.png
new file mode 100644
index 00000000..bf21832a
Binary files /dev/null and b/Web/static/img/audios_controls.png differ
diff --git a/Web/static/img/bullet.gif b/Web/static/img/bullet.gif
new file mode 100644
index 00000000..bd5c540f
Binary files /dev/null and b/Web/static/img/bullet.gif differ
diff --git a/Web/static/img/explicit.svg b/Web/static/img/explicit.svg
new file mode 100644
index 00000000..df6f6c15
--- /dev/null
+++ b/Web/static/img/explicit.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/Web/static/img/favicons/favicon24_paused.png b/Web/static/img/favicons/favicon24_paused.png
new file mode 100644
index 00000000..c8fdef26
Binary files /dev/null and b/Web/static/img/favicons/favicon24_paused.png differ
diff --git a/Web/static/img/favicons/favicon24_playing.png b/Web/static/img/favicons/favicon24_playing.png
new file mode 100644
index 00000000..ce4478b8
Binary files /dev/null and b/Web/static/img/favicons/favicon24_playing.png differ
diff --git a/Web/static/img/play_buttons.gif b/Web/static/img/play_buttons.gif
new file mode 100644
index 00000000..b5a8078d
Binary files /dev/null and b/Web/static/img/play_buttons.gif differ
diff --git a/Web/static/img/progressbar.gif b/Web/static/img/progressbar.gif
new file mode 100644
index 00000000..09d6437c
Binary files /dev/null and b/Web/static/img/progressbar.gif differ
diff --git a/Web/static/img/song.jpg b/Web/static/img/song.jpg
new file mode 100644
index 00000000..21e5a3a3
Binary files /dev/null and b/Web/static/img/song.jpg differ
diff --git a/Web/static/img/tour/audios.png b/Web/static/img/tour/audios.png
new file mode 100644
index 00000000..a15affe0
Binary files /dev/null and b/Web/static/img/tour/audios.png differ
diff --git a/Web/static/img/tour/audios_playlists.png b/Web/static/img/tour/audios_playlists.png
new file mode 100644
index 00000000..a15affe0
Binary files /dev/null and b/Web/static/img/tour/audios_playlists.png differ
diff --git a/Web/static/img/tour/audios_search.png b/Web/static/img/tour/audios_search.png
new file mode 100644
index 00000000..a15affe0
Binary files /dev/null and b/Web/static/img/tour/audios_search.png differ
diff --git a/Web/static/img/tour/audios_upload.png b/Web/static/img/tour/audios_upload.png
new file mode 100644
index 00000000..a15affe0
Binary files /dev/null and b/Web/static/img/tour/audios_upload.png differ
diff --git a/Web/static/js/al_music.js b/Web/static/js/al_music.js
new file mode 100644
index 00000000..ac449c0d
--- /dev/null
+++ b/Web/static/js/al_music.js
@@ -0,0 +1,1434 @@
+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 fastError(message) {
+ MessageBox(tr("error"), message, [tr("ok")], [Function.noop])
+}
+
+// elapsed это вроде прошедшие, а оставшееся это remaining но ладно уже
+function getElapsedTime(fullTime, time) {
+ let timer = fullTime - time
+
+ if(timer < 0) return "-00:00"
+
+ return "-" + fmtTime(timer)
+}
+
+window.savedAudiosPages = {}
+
+class playersSearcher {
+ constructor(context_type, context_id) {
+ this.context_type = context_type
+ this.context_id = context_id
+ this.searchType = "by_name"
+ this.query = ""
+ this.page = 1
+ this.successCallback = () => {}
+ this.errorCallback = () => {}
+ this.beforesendCallback = () => {}
+ this.clearContainer = () => {}
+ }
+
+ execute() {
+ $.ajax({
+ type: "POST",
+ url: "/audios/context",
+ data: {
+ context: this.context_type,
+ hash: u("meta[name=csrf]").attr("value"),
+ page: this.page,
+ query: this.query,
+ context_entity: this.context_id,
+ type: this.searchType,
+ returnPlayers: 1,
+ },
+ beforeSend: () => {
+ this.beforesendCallback()
+ },
+ error: () => {
+ this.errorCallback()
+ },
+ success: (response) => {
+ this.successCallback(response, this)
+ }
+ })
+ }
+
+ movePage(page) {
+ this.page = page
+ this.execute()
+ }
+}
+
+class bigPlayer {
+ tracks = {
+ currentTrack: null,
+ nextTrack: null,
+ previousTrack: null,
+ tracks: []
+ }
+
+ context = {
+ context_type: null,
+ context_id: 0,
+ pagesCount: 0,
+ playedPages: [],
+ object: [],
+ }
+
+ nodes = {
+ dashPlayer: null,
+ audioPlayer: null,
+ thisPlayer: null,
+ playButtons: null,
+ }
+
+ timeType = 0
+
+ findTrack(id) {
+ return this.tracks["tracks"].find(item => item.id == id)
+ }
+
+ constructor(context, context_id, page = 1) {
+ this.context["context_type"] = context
+ this.context["context_id"] = context_id
+ this.context["playedPages"].push(String(page))
+
+ this.nodes["thisPlayer"] = document.querySelector(".bigPlayer")
+ this.nodes["thisPlayer"].classList.add("lagged")
+ this.nodes["audioPlayer"] = document.createElement("audio")
+
+ this.player = () => { return this.nodes["audioPlayer"] }
+ this.nodes["playButtons"] = this.nodes["thisPlayer"].querySelector(".playButtons")
+ this.nodes["dashPlayer"] = dashjs.MediaPlayer().create()
+
+ let formdata = new FormData()
+ formdata.append("context", context)
+ formdata.append("context_entity", context_id)
+ formdata.append("query", context_id)
+ formdata.append("hash", u("meta[name=csrf]").attr("value"))
+ formdata.append("page", page)
+
+ ky.post("/audios/context", {
+ hooks: {
+ afterResponse: [
+ async (_request, _options, response) => {
+ if(response.status !== 200) {
+ fastError(tr("unable_to_load_queue"))
+ return
+ }
+
+ let contextObject = await response.json()
+
+ if(!contextObject.success) {
+ fastError(tr("unable_to_load_queue"))
+ return
+ }
+
+ this.nodes["thisPlayer"].classList.remove("lagged")
+ this.tracks["tracks"] = contextObject["items"]
+ this.context["pagesCount"] = contextObject["pagesCount"]
+
+ if(localStorage.lastPlayedTrack != null && this.tracks["tracks"].find(item => item.id == localStorage.lastPlayedTrack) != null) {
+ this.setTrack(localStorage.lastPlayedTrack)
+ this.pause()
+ }
+
+ console.info("Context is successfully loaded")
+ }
+ ]
+ },
+ body: formdata,
+ timeout: 20000,
+ })
+
+ u(this.nodes["playButtons"].querySelector(".playButton")).on("click", (e) => {
+ if(this.player().paused)
+ this.play()
+ else
+ this.pause()
+ })
+
+ u(this.player()).on("timeupdate", (e) => {
+ const time = this.player().currentTime;
+ const ps = ((time * 100) / this.tracks["currentTrack"].length).toFixed(3)
+ this.nodes["thisPlayer"].querySelector(".time").innerHTML = fmtTime(time)
+ this.timeType == 0 ? this.nodes["thisPlayer"].querySelector(".elapsedTime").innerHTML = getElapsedTime(this.tracks["currentTrack"].length, time)
+ : null
+
+ if (ps <= 100)
+ this.nodes["thisPlayer"].querySelector(".selectableTrack .slider").style.left = `${ ps}%`;
+
+ })
+
+ u(this.player()).on("volumechange", (e) => {
+ const volume = this.player().volume;
+ const ps = Math.ceil((volume * 100) / 1);
+
+ if (ps <= 100)
+ this.nodes["thisPlayer"].querySelector(".volumePanel .selectableTrack .slider").style.left = `${ ps}%`;
+
+ localStorage.volume = volume
+ })
+
+ u(".bigPlayer .track > div").on("click mouseup", (e) => {
+ if(this.tracks["currentTrack"] == null)
+ return
+
+ let rect = this.nodes["thisPlayer"].querySelector(".selectableTrack").getBoundingClientRect();
+
+ const width = e.clientX - rect.left;
+ const time = Math.ceil((width * this.tracks["currentTrack"].length) / (rect.right - rect.left));
+
+ this.player().currentTime = time;
+ })
+
+ u(".bigPlayer .trackPanel .selectableTrack").on("mousemove", (e) => {
+ if(this.tracks["currentTrack"] == null)
+ return
+
+ let rect = this.nodes["thisPlayer"].querySelector(".selectableTrack").getBoundingClientRect();
+
+ const width = e.clientX - rect.left;
+ const time = Math.ceil((width * this.tracks["currentTrack"].length) / (rect.right - rect.left));
+
+ document.querySelector(".bigPlayer .track .bigPlayerTip").style.display = "block"
+ document.querySelector(".bigPlayer .track .bigPlayerTip").innerHTML = fmtTime(time)
+ document.querySelector(".bigPlayer .track .bigPlayerTip").style.left = `min(${width - 15}px, 315.5px)`
+ })
+
+ u(".bigPlayer .nextButton").on("mouseover mouseleave", (e) => {
+ if(this.tracks["currentTrack"] == null)
+ return
+
+ if(e.type == "mouseleave") {
+ $(".nextTrackTip").remove()
+ return
+ }
+
+ e.currentTarget.parentNode.insertAdjacentHTML("afterbegin", `
+
+ ${ovk_proc_strtr(escapeHtml(this.findTrack(this.tracks["previousTrack"]).name), 20) ?? ""}
+
+ `)
+
+ document.querySelector(".nextTrackTip").style.display = "block"
+ })
+
+ u(".bigPlayer .backButton").on("mouseover mouseleave", (e) => {
+ if(this.tracks["currentTrack"] == null)
+ return
+
+ if(e.type == "mouseleave") {
+ $(".previousTrackTip").remove()
+ return
+ }
+
+ e.currentTarget.parentNode.insertAdjacentHTML("afterbegin", `
+
+ ${ovk_proc_strtr(escapeHtml(this.findTrack(this.tracks["nextTrack"]).name), 20) ?? ""}
+
+ `)
+
+ document.querySelector(".previousTrackTip").style.display = "block"
+ })
+
+ u(".bigPlayer .trackPanel .selectableTrack").on("mouseleave", (e) => {
+ if(this.tracks["currentTrack"] == null)
+ return
+
+ document.querySelector(".bigPlayer .track .bigPlayerTip").style.display = "none"
+ })
+
+ u(".bigPlayer .volumePanel > div").on("click mouseup mousemove", (e) => {
+ if(this.tracks["currentTrack"] == null)
+ return
+
+ if(e.type == "mousemove") {
+ let buttonsPresseed = _bsdnUnwrapBitMask(e.buttons)
+ if(!buttonsPresseed[0])
+ return;
+ }
+
+ let rect = this.nodes["thisPlayer"].querySelector(".volumePanel .selectableTrack").getBoundingClientRect();
+
+ const width = e.clientX - rect.left;
+ const volume = Math.max(0, (width * 1) / (rect.right - rect.left));
+
+ this.player().volume = volume;
+ })
+
+ u(".bigPlayer .elapsedTime").on("click", (e) => {
+ if(this.tracks["currentTrack"] == null)
+ return
+
+ this.timeType == 0 ? (this.timeType = 1) : (this.timeType = 0)
+
+ localStorage.playerTimeType = this.timeType
+
+ this.nodes["thisPlayer"].querySelector(".elapsedTime").innerHTML = this.timeType == 1 ?
+ fmtTime(this.tracks["currentTrack"].length)
+ : getElapsedTime(this.tracks["currentTrack"].length, this.player().currentTime)
+ })
+
+ u(".bigPlayer .additionalButtons .repeatButton").on("click", (e) => {
+ if(this.tracks["currentTrack"] == null)
+ return
+
+ e.currentTarget.classList.toggle("pressed")
+
+ if(e.currentTarget.classList.contains("pressed"))
+ this.player().loop = true
+ else
+ this.player().loop = false
+ })
+
+ u(".bigPlayer .additionalButtons .shuffleButton").on("click", (e) => {
+ if(this.tracks["currentTrack"] == null)
+ return
+
+ this.tracks["tracks"].sort(() => Math.random() - 0.59)
+ this.setTrack(this.tracks["tracks"].at(0).id)
+ })
+
+ // хз что она делала в самом вк, но тут сделаем вид что это просто мут музыки
+ u(".bigPlayer .additionalButtons .deviceButton").on("click", (e) => {
+ if(this.tracks["currentTrack"] == null)
+ return
+
+ e.currentTarget.classList.toggle("pressed")
+
+ this.player().muted = e.currentTarget.classList.contains("pressed")
+ })
+
+ u(".bigPlayer .arrowsButtons .nextButton").on("click", (e) => {
+ this.showPreviousTrack()
+ })
+
+ u(".bigPlayer .arrowsButtons .backButton").on("click", (e) => {
+ this.showNextTrack()
+ })
+
+ u(".bigPlayer .trackInfo b").on("click", (e) => {
+ window.location.assign(`/search?query=${e.currentTarget.innerHTML}&type=audios&only_performers=on`)
+ })
+
+ u(document).on("keydown", (e) => {
+ if(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", " "].includes(e.key)) {
+ if(document.querySelector(".ovk-diag-cont") != null)
+ return
+
+ e.preventDefault()
+ }
+
+ switch(e.key) {
+ case "ArrowUp":
+ this.player().volume = Math.min(0.99, this.player().volume + 0.1)
+ break
+ case "ArrowDown":
+ this.player().volume = Math.max(0, this.player().volume - 0.1)
+ break
+ case "ArrowLeft":
+ this.player().currentTime = this.player().currentTime - 3
+ break
+ 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([87, 65, 83, 68, 82, 77].includes(e.keyCode)) {
+ if(document.querySelector(".ovk-diag-cont") != null)
+ return
+
+ e.preventDefault()
+ }
+
+ switch(e.keyCode) {
+ case 87:
+ case 65:
+ this.showPreviousTrack()
+ break
+ case 83:
+ case 68:
+ this.showNextTrack()
+ break
+ case 82:
+ document.querySelector(".bigPlayer .additionalButtons .repeatButton").click()
+ break
+ case 77:
+ document.querySelector(".bigPlayer .additionalButtons .deviceButton").click()
+ break
+ }
+ })
+
+ u(this.player()).on("ended", (e) => {
+ e.preventDefault()
+
+ // в начало очереди
+ if(!this.tracks.nextTrack) {
+ if(!this.context["playedPages"].includes("1")) {
+ $.ajax({
+ type: "POST",
+ url: "/audios/context",
+ data: {
+ context: this["context"].context_type,
+ context_entity: this["context"].context_id,
+ hash: u("meta[name=csrf]").attr("value"),
+ page: 1
+ },
+ success: (response_2) => {
+ this.tracks["tracks"] = response_2["items"].concat(this.tracks["tracks"])
+ this.context["playedPages"].push(String(1))
+
+ this.setTrack(this.tracks["tracks"][0].id)
+ }
+ })
+ } else {
+ this.setTrack(this.tracks.tracks[0].id)
+ }
+
+ return
+ }
+
+ this.showNextTrack()
+ })
+
+ u(this.player()).on("loadstart", (e) => {
+ let playlist = this.context.context_type == "playlist_context" ? this.context.context_id : null
+
+ let tempThisId = this.tracks.currentTrack.id
+ setTimeout(() => {
+ if(tempThisId != this.tracks.currentTrack.id) return
+
+ $.ajax({
+ type: "POST",
+ url: `/audio${this.tracks["currentTrack"].id}/listen`,
+ data: {
+ hash: u("meta[name=csrf]").attr("value"),
+ playlist: playlist
+ },
+ success: (response) => {
+ if(response.success) {
+ console.info("Listen is counted.")
+
+ if(response.new_playlists_listens)
+ document.querySelector("#listensCount").innerHTML = tr("listens_count", response.new_playlists_listens)
+ } else
+ console.info("Listen is not counted.")
+ }
+ })
+ }, 2000)
+ })
+
+ if(localStorage.volume != null && localStorage.volume < 1 && localStorage.volume > 0)
+ this.player().volume = localStorage.volume
+ else
+ this.player().volume = 0.75
+
+ if(localStorage.playerTimeType == 'null' || localStorage.playerTimeType == null)
+ this.timeType = 0
+ else
+ this.timeType = localStorage.playerTimeType
+
+ navigator.mediaSession.setActionHandler('play', () => { this.play() });
+ navigator.mediaSession.setActionHandler('pause', () => { this.pause() });
+ navigator.mediaSession.setActionHandler('previoustrack', () => { this.showPreviousTrack() });
+ navigator.mediaSession.setActionHandler('nexttrack', () => { this.showNextTrack() });
+ navigator.mediaSession.setActionHandler("seekto", (details) => {
+ this.player().currentTime = details.seekTime;
+ });
+ }
+
+ play() {
+ if(this.tracks["currentTrack"] == null)
+ return
+
+ document.querySelectorAll('audio').forEach(el => el.pause());
+ document.querySelector(`.audioEmbed[data-realid='${this.tracks["currentTrack"].id}'] .audioEntry .playerButton .playIcon`) != null ? document.querySelector(`.audioEmbed[data-realid='${this.tracks["currentTrack"].id}'] .audioEntry .playerButton .playIcon`).classList.add("paused") : void(0)
+ this.player().play()
+ this.nodes["playButtons"].querySelector(".playButton").classList.add("pause")
+ document.querySelector('link[rel="icon"], link[rel="shortcut icon"]').setAttribute("href", "/assets/packages/static/openvk/img/favicons/favicon24_paused.png")
+
+ navigator.mediaSession.playbackState = "playing"
+ }
+
+ pause() {
+ if(this.tracks["currentTrack"] == null)
+ return
+
+ document.querySelector(`.audioEmbed[data-realid='${this.tracks["currentTrack"].id}'] .audioEntry .playerButton .playIcon`) != null ? document.querySelector(`.audioEmbed[data-realid='${this.tracks["currentTrack"].id}'] .audioEntry .playerButton .playIcon`).classList.remove("paused") : void(0)
+ this.player().pause()
+ this.nodes["playButtons"].querySelector(".playButton").classList.remove("pause")
+ document.querySelector('link[rel="icon"], link[rel="shortcut icon"]').setAttribute("href", "/assets/packages/static/openvk/img/favicons/favicon24_playing.png")
+
+ navigator.mediaSession.playbackState = "paused"
+ }
+
+ showPreviousTrack() {
+ if(this.tracks["currentTrack"] == null || this.tracks["previousTrack"] == null)
+ return
+
+ this.setTrack(this.tracks["previousTrack"])
+ }
+
+ showNextTrack() {
+ if(this.tracks["currentTrack"] == null || this.tracks["nextTrack"] == null)
+ return
+
+ this.setTrack(this.tracks["nextTrack"])
+ }
+
+ updateButtons() {
+ // перепутал некст и бек.
+ let prevButton = this.nodes["thisPlayer"].querySelector(".nextButton")
+ let nextButton = this.nodes["thisPlayer"].querySelector(".backButton")
+
+ if(this.tracks["previousTrack"] == null)
+ prevButton.classList.add("lagged")
+ else
+ prevButton.classList.remove("lagged")
+
+ if(this.tracks["nextTrack"] == null)
+ nextButton.classList.add("lagged")
+ else
+ nextButton.classList.remove("lagged")
+
+ if(document.querySelector(".nextTrackTip") != null) {
+ let track = this.findTrack(this.tracks["previousTrack"])
+ document.querySelector(".nextTrackTip").innerHTML = `
+ ${track != null ? ovk_proc_strtr(escapeHtml(track.name), 20) : ""}
+ `
+ }
+
+ if(document.querySelector(".previousTrackTip") != null) {
+ let track = this.findTrack(this.tracks["nextTrack"])
+ document.querySelector(".previousTrackTip").innerHTML = `
+ ${track != null ? ovk_proc_strtr(escapeHtml(track.name ?? ""), 20) : ""}
+ `
+ }
+ }
+
+ setTrack(id) {
+ if(this.tracks["tracks"] == null) {
+ console.info("Context is not loaded yet. Wait please")
+ return 0;
+ }
+
+ document.querySelectorAll(".audioEntry.nowPlaying").forEach(el => el.classList.remove("nowPlaying"))
+ let obj = this.tracks["tracks"].find(item => item.id == id)
+
+ if(obj == null) {
+ fastError("No audio in context")
+ return
+ }
+
+ this.nodes["thisPlayer"].querySelector(".trackInfo span").innerHTML = escapeHtml(obj.name)
+ this.nodes["thisPlayer"].querySelector(".trackInfo b").innerHTML = escapeHtml(obj.performer)
+ this.nodes["thisPlayer"].querySelector(".trackInfo .time").innerHTML = fmtTime(obj.length)
+ this.tracks["currentTrack"] = obj
+
+ let indexOfCurrentTrack = this.tracks["tracks"].indexOf(obj) ?? 0
+ this.tracks["nextTrack"] = this.tracks["tracks"].at(indexOfCurrentTrack + 1) != null ? this.tracks["tracks"].at(indexOfCurrentTrack + 1).id : null
+
+ if(indexOfCurrentTrack - 1 >= 0)
+ this.tracks["previousTrack"] = this.tracks["tracks"].at(indexOfCurrentTrack - 1).id
+ else
+ this.tracks["previousTrack"] = null
+
+ if(this.tracks["nextTrack"] == null && Math.max(...this.context["playedPages"]) < this.context["pagesCount"]
+ || this.tracks["previousTrack"] == null && (Math.min(...this.context["playedPages"]) > 1)) {
+
+ // idk how it works
+ let lesser = this.tracks["previousTrack"] == null ? (Math.min(...this.context["playedPages"]) > 1)
+ : Math.max(...this.context["playedPages"]) > this.context["pagesCount"]
+
+ let formdata = new FormData()
+ formdata.append("context", this.context["context_type"])
+ formdata.append("context_entity", this.context["context_id"])
+ formdata.append("hash", u("meta[name=csrf]").attr("value"))
+
+ if(lesser)
+ formdata.append("page", Math.min(...this.context["playedPages"]) - 1)
+ else
+ formdata.append("page", Number(Math.max(...this.context["playedPages"])) + 1)
+
+ ky.post("/audios/context", {
+ hooks: {
+ afterResponse: [
+ async (_request, _options, response) => {
+ let newArr = await response.json()
+
+ if(lesser)
+ this.tracks["tracks"] = newArr["items"].concat(this.tracks["tracks"])
+ else
+ this.tracks["tracks"] = this.tracks["tracks"].concat(newArr["items"])
+
+ this.context["playedPages"].push(String(newArr["page"]))
+
+ if(lesser)
+ this.tracks["previousTrack"] = this.tracks["tracks"].at(this.tracks["tracks"].indexOf(obj) - 1).id
+ else
+ this.tracks["nextTrack"] = this.tracks["tracks"].at(indexOfCurrentTrack + 1) != null ? this.tracks["tracks"].at(indexOfCurrentTrack + 1).id : null
+
+ this.updateButtons()
+ console.info("Context is successfully loaded")
+ }
+ ]
+ },
+ body: formdata
+ })
+ }
+
+ if(this.tracks["currentTrack"].available == false || this.tracks["currentTrack"].withdrawn)
+ this.showNextTrack()
+
+ this.updateButtons()
+
+ const protData = {
+ "org.w3.clearkey": {
+ "clearkeys": obj.keys
+ }
+ };
+
+ this.nodes["dashPlayer"].initialize(this.player(), obj.url, false);
+ this.nodes["dashPlayer"].setProtectionData(protData);
+
+ this.play()
+
+ let playerAtPage = document.querySelector(`.audioEmbed[data-realid='${this.tracks["currentTrack"].id}'] .audioEntry`)
+ if(playerAtPage != null)
+ playerAtPage.classList.add("nowPlaying")
+
+ document.querySelectorAll(`.audioEntry .playerButton .playIcon.paused`).forEach(el => el.classList.remove("paused"))
+
+ localStorage.lastPlayedTrack = this.tracks["currentTrack"].id
+
+ if(this.timeType == 1)
+ this.nodes["thisPlayer"].querySelector(".elapsedTime").innerHTML = fmtTime(this.tracks["currentTrack"].length)
+
+ let album = document.querySelector(".playlistBlock")
+
+ navigator.mediaSession.metadata = new MediaMetadata({
+ title: obj.name,
+ artist: obj.performer,
+ album: album == null ? "OpenVK Audios" : album.querySelector(".playlistInfo h4").innerHTML,
+ artwork: [{ src: album == null ? "/assets/packages/static/openvk/img/song.jpg" : album.querySelector(".playlistCover img").src }],
+ });
+
+ navigator.mediaSession.setPositionState({
+ duration: this.tracks["currentTrack"].length
+ })
+ }
+}
+
+document.addEventListener("DOMContentLoaded", function() {
+ if(document.querySelector(".bigPlayer") != null) {
+ let context = document.querySelector("input[name='bigplayer_context']")
+
+ if(!context)
+ return
+
+ let type = context.dataset.type
+ let entity = context.dataset.entity
+ window.player = new bigPlayer(type, entity, context.dataset.page)
+
+ let bigplayer = document.querySelector('.bigPlayerDetector')
+
+ let bigPlayerObserver = new IntersectionObserver(entries => {
+ 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"
+ }
+ });
+ }, {
+ root: null,
+ rootMargin: "0px",
+ threshold: 0
+ });
+
+ if(bigplayer != null)
+ bigPlayerObserver.observe(bigplayer);
+ }
+
+ $(document).on("mouseover mouseleave", `.audioEntry .mediaInfo`, (e) => {
+ const info = e.currentTarget.closest(".mediaInfo")
+ const overfl = info.querySelector(".info")
+
+ if(e.originalEvent.type == "mouseleave" || e.originalEvent.type == "mouseout") {
+ info.classList.add("noOverflow")
+ info.classList.remove("overflowedName")
+ } else {
+ info.classList.remove("noOverflow")
+ info.classList.add("overflowedName")
+ }
+ })
+})
+
+$(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`);
+ 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 playerObject = document.querySelector(`#audioEmbed-${ id}`)
+
+ if(document.querySelector(".bigPlayer") != null) {
+ playButton.on("click", () => {
+ if(window.player.tracks["tracks"] == null)
+ return
+
+ if(window.player.tracks["currentTrack"] == null || window.player.tracks["currentTrack"].id != playerObject.dataset.realid)
+ window.player.setTrack(playerObject.dataset.realid)
+ else
+ document.querySelector(".bigPlayer .playButton").click()
+ })
+
+ return
+ }
+
+ 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 = ((time * 100) / length).toFixed(3);
+ volumeSpan.html(fmtTime(Math.floor(time)));
+
+ if (ps <= 100)
+ playerObject.querySelector(".lengthTrack .slider").style.left = `${ ps}%`;
+ });
+
+ u(audio).on("volumechange", (e) => {
+ const volume = audio.volume;
+ const ps = Math.ceil((volume * 100) / 1);
+
+ if (ps <= 100)
+ playerObject.querySelector(".volumeTrack .slider").style.left = `${ ps}%`;
+ })
+
+ const playButtonImageUpdate = () => {
+ if (!audio.paused) {
+ playButton.addClass("paused")
+ document.querySelector('link[rel="icon"], link[rel="shortcut icon"]').setAttribute("href", "/assets/packages/static/openvk/img/favicons/favicon24_paused.png")
+ } else {
+ playButton.removeClass("paused")
+ document.querySelector('link[rel="icon"], link[rel="shortcut icon"]').setAttribute("href", "/assets/packages/static/openvk/img/favicons/favicon24_playing.png")
+ }
+
+ if(!$(`#audioEmbed-${ id}`).hasClass("havePlayed")) {
+ $(`#audioEmbed-${ id}`).addClass("havePlayed")
+
+ $(`#audioEmbed-${ id} .track`).toggle()
+
+ $.post(`/audio${playerObject.dataset.realid}/listen`, {
+ hash: u("meta[name=csrf]").attr("value")
+ });
+ }
+ };
+
+ const hideTracks = () => {
+ $(`#audioEmbed-${ id} .track`).toggle()
+ $(`#audioEmbed-${ id}`).removeClass("havePlayed")
+ }
+
+ u(audio).on("play", 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();
+ const width = e.clientX - rect.left;
+ const time = Math.ceil((width * length) / (rect.right - rect.left));
+
+ audio.currentTime = time;
+ });
+
+ u(`#audioEmbed-${ id} .volumeTrack > div`).on("click mouseup mousemove", (e) => {
+ if(e.type == "mousemove") {
+ let buttonsPresseed = _bsdnUnwrapBitMask(e.buttons)
+ if(!buttonsPresseed[0])
+ return;
+ }
+
+ let rect = document.querySelector("#audioEmbed-" + id + " .volumeTrack").getBoundingClientRect();
+
+ const width = e.clientX - rect.left;
+ const volume = (width * 1) / (rect.right - rect.left);
+
+ audio.volume = volume;
+ });
+
+ audio.volume = localStorage.volume ?? 0.75
+ u(audio).trigger("volumechange")
+}
+
+$(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_audio"), `
+
+ ${tr("performer")}
+
+
+
+
+ ${tr("audio_name")}
+
+
+
+
+ ${tr("genre")}
+
+
+
+
+ ${tr("lyrics")}
+
+
+
+
+
+ `, [tr("save"), 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();
+ let t_explicit = document.querySelector(".ovk-diag-body input[name=explicit]").checked;
+ let t_unlisted = document.querySelector(".ovk-diag-body input[name=searchable]").checked;
+
+ $.ajax({
+ type: "POST",
+ url: `/audio${id}/action?act=edit`,
+ data: {
+ name: t_name,
+ performer: t_perf,
+ genre: t_genre,
+ lyrics: t_lyrics,
+ unlisted: Number(t_unlisted),
+ explicit: Number(t_explicit),
+ hash: u("meta[name=csrf]").attr("value")
+ },
+ success: (response) => {
+ if(response.success) {
+ let perf = player.querySelector(".performer a")
+ perf.innerHTML = escapeHtml(response.new_info.performer)
+ perf.setAttribute("href", "/search?query=&type=audios&sort=id&only_performers=on&query="+response.new_info.performer)
+
+ e.target.setAttribute("data-performer", escapeHtml(response.new_info.performer))
+
+ let name = player.querySelector(".title")
+ name.innerHTML = escapeHtml(response.new_info.name)
+
+ e.target.setAttribute("data-title", escapeHtml(response.new_info.name))
+
+ if(response.new_info.lyrics_unformatted != "") {
+ if(player.querySelector(".lyrics") != null) {
+ player.querySelector(".lyrics").innerHTML = response.new_info.lyrics
+ player.querySelector(".title").classList.add("withLyrics")
+ } else {
+ player.insertAdjacentHTML("beforeend", `
+
+ ${response.new_info.lyrics}
+
+ `)
+
+ player.querySelector(".title").classList.add("withLyrics")
+ }
+ } else {
+ $(player.querySelector(".lyrics")).remove()
+ player.querySelector(".title").classList.remove("withLyrics")
+ }
+
+ e.target.setAttribute("data-lyrics", response.new_info.lyrics_unformatted)
+ e.target.setAttribute("data-explicit", Number(response.new_info.explicit))
+
+ if(Number(response.new_info.explicit) == 1) {
+ if(!player.querySelector(".mediaInfo .explicitMark"))
+ player.querySelector(".mediaInfo").insertAdjacentHTML("beforeend", `
+
+ `)
+ } else {
+ $(player.querySelector(".mediaInfo .explicitMark")).remove()
+ }
+
+ e.target.setAttribute("data-searchable", Number(!response.new_info.unlisted))
+ player.setAttribute("data-genre", response.new_info.genre)
+
+ let url = new URL(location.href)
+ let page = "1"
+
+ if(url.searchParams.p != null)
+ page = String(url.searchParams.p)
+
+ window.savedAudiosPages[page] = null
+ } else
+ fastError(response.flash.message)
+ }
+ });
+ },
+
+ Function.noop
+ ]);
+
+ window.openvk.audio_genres.forEach(elGenre => {
+ document.querySelector(".ovk-diag-body select[name=genre]").insertAdjacentHTML("beforeend", `
+
+ `)
+ })
+
+ u(".ovk-diag-body #_fullyDeleteAudio").on("click", (e) => {
+ u("body").removeClass("dimmed");
+ document.querySelector("html").style.overflowY = "scroll"
+
+ u(".ovk-diag-cont").remove();
+
+ $.ajax({
+ type: "POST",
+ url: `/audio${id}/action?act=delete`,
+ data: {
+ hash: u("meta[name=csrf]").attr("value")
+ },
+ success: (response) => {
+ if(response.success)
+ u(player).remove()
+ else
+ fastError(response.flash.message)
+ }
+ });
+ })
+})
+
+$(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) => {
+ e.stopImmediatePropagation()
+
+ 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.target.classList.add("lagged")
+ }
+ ],
+ afterResponse: [
+ async (_request, _options, response) => {
+ let json = await response.json()
+
+ if(json.success) {
+ e.target.classList.remove("remove-icon")
+ e.target.classList.add("add-icon")
+ e.target.classList.remove("lagged")
+
+ let withd = e.target.closest(".audioEmbed.withdrawn")
+
+ if(withd != null)
+ u(withd).remove()
+ } else
+ fastError(json.flash.message)
+ }
+ ]
+ }, body: formdata
+ })
+})
+
+$(document).on("click", ".musicIcon.remove-icon-group", (e) => {
+ e.stopImmediatePropagation()
+
+ let id = e.currentTarget.dataset.id
+
+ let formdata = new FormData()
+ formdata.append("hash", u("meta[name=csrf]").attr("value"))
+ formdata.append("club", e.currentTarget.dataset.club)
+
+ ky.post(`/audio${id}/action?act=remove_club`, {
+ hooks: {
+ beforeRequest: [
+ (_request) => {
+ e.currentTarget.classList.add("lagged")
+ }
+ ],
+ afterResponse: [
+ async (_request, _options, response) => {
+ let json = await response.json()
+
+ if(json.success)
+ $(e.currentTarget.closest(".audioEmbed")).remove()
+ else
+ fastError(json.flash.message)
+ }
+ ]
+ }, body: formdata
+ })
+})
+
+$(document).on("click", ".musicIcon.add-icon-group", async (ev) => {
+ let body = `
+ ${tr("what_club_add")}
+
+
+
+
+
+ `
+ MessageBox(tr("add_audio_to_club"), body, [tr("close")], [Function.noop])
+
+ document.querySelector(".ovk-diag-body").style.padding = "11px"
+
+ if(window.openvk.writeableClubs == null) {
+ try {
+ window.openvk.writeableClubs = await API.Groups.getWriteableClubs()
+ } catch (e) {
+ document.querySelector(".errorPlace").innerHTML = tr("no_access_clubs")
+ document.querySelector(".ovk-diag-body input[name='addButton']").classList.add("lagged")
+
+ return
+ }
+ }
+
+ window.openvk.writeableClubs.forEach(el => {
+ document.querySelector("#addIconsWindow").insertAdjacentHTML("beforeend", `
+
+ `)
+ })
+
+ $(".ovk-diag-body").on("click", "input[name='addButton']", (e) => {
+ $.ajax({
+ type: "POST",
+ 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.target.classList.add("lagged")
+ document.querySelector(".errorPlace").innerHTML = ""
+ },
+ success: (response) => {
+ if(!response.success)
+ document.querySelector(".errorPlace").innerHTML = response.flash.message
+
+ e.currentTarget.classList.remove("lagged")
+ }
+ })
+ })
+})
+
+$(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.target.classList.add("lagged")
+ }
+ ],
+ afterResponse: [
+ async (_request, _options, response) => {
+ let json = await response.json()
+
+ if(json.success) {
+ e.target.classList.remove("add-icon")
+ e.target.classList.add("remove-icon")
+ e.target.classList.remove("lagged")
+ } else
+ fastError(json.flash.message)
+ }
+ ]
+ }, body: formdata
+ })
+})
+
+$(document).on("click", "#_deletePlaylist", (e) => {
+ let id = e.currentTarget.dataset.id
+
+ MessageBox(tr("warning"), tr("sure_delete_playlist"), [tr("yes"), tr("no")], [() => {
+ $.ajax({
+ type: "POST",
+ url: `/playlist${id}/action?act=delete`,
+ data: {
+ hash: u("meta[name=csrf]").attr("value"),
+ },
+ beforeSend: () => {
+ e.currentTarget.classList.add("lagged")
+ },
+ success: (response) => {
+ if(response.success) {
+ window.location.assign("/playlists" + response.id)
+ } else {
+ fastError(response.flash.message)
+ }
+ }
+ })
+ }, Function.noop])
+})
+
+$(document).on("click", "#_audioAttachment", (e) => {
+ let form = e.currentTarget.closest("form")
+ let body = `
+
+
+
+
+
+
+ `
+ MessageBox(tr("select_audio"), body, [tr("ok")], [Function.noop])
+
+ document.querySelector(".ovk-diag-body").style.padding = "0"
+ document.querySelector(".ovk-diag-cont").style.width = "580px"
+ document.querySelector(".ovk-diag-body").style.height = "335px"
+
+ let searcher = new playersSearcher("entity_audios", 0)
+ searcher.successCallback = (response, thisc) => {
+ let domparser = new DOMParser()
+ let result = domparser.parseFromString(response, "text/html")
+
+ let pagesCount = result.querySelector("input[name='pagesCount']").value
+ let count = Number(result.querySelector("input[name='count']").value)
+
+ if(count < 1) {
+ document.querySelector(".audiosInsert").innerHTML = thisc.context_type == "entity_audios" ? tr("no_audios_thisuser") : tr("no_results")
+ return
+ }
+
+ result.querySelectorAll(".audioEmbed").forEach(el => {
+ let id = el.dataset.prettyid
+ let name = el.dataset.name
+ let isAttached = (form.querySelector("input[name='audios']").value.includes(`${id},`))
+ document.querySelector(".audiosInsert").insertAdjacentHTML("beforeend", `
+
+ ${el.outerHTML}
+
+ ${isAttached ? tr("detach_audio") : tr("attach_audio")}
+
+
+ `)
+ })
+
+ u("#loader").remove()
+
+ if(thisc.page < pagesCount) {
+ document.querySelector(".audiosInsert").insertAdjacentHTML("beforeend", `
+
+ ${tr("show_more_audios")}
+ `)
+ }
+ }
+
+ searcher.errorCallback = () => {
+ fastError("Error when loading players.")
+ }
+
+ searcher.beforesendCallback = () => {
+ document.querySelector(".audiosInsert").insertAdjacentHTML("beforeend", `
`)
+ }
+
+ searcher.clearContainer = () => {
+ document.querySelector(".audiosInsert").innerHTML = ""
+ }
+
+ searcher.movePage(1)
+
+ $(".audiosInsert").on("click", "#showMoreAudios", (e) => {
+ u(e.currentTarget).remove()
+ searcher.movePage(Number(e.currentTarget.dataset.page))
+ })
+
+ $(".searchBox input").on("change", async (e) => {
+ await new Promise(r => setTimeout(r, 500));
+
+ if(e.currentTarget.value === document.querySelector(".searchBox input").value) {
+ searcher.clearContainer()
+
+ if(e.currentTarget.value == "") {
+ searcher.context_type = "entity_audios"
+ searcher.context_id = 0
+ searcher.query = ""
+
+ searcher.movePage(1)
+
+ return
+ }
+
+ searcher.context_type = "search_context"
+ searcher.context_id = 0
+ searcher.query = e.currentTarget.value
+
+ searcher.movePage(1)
+ return;
+ }
+ })
+
+ $(".searchBox select").on("change", async (e) => {
+ searcher.clearContainer()
+ searcher.searchType = e.currentTarget.value
+
+ $(".searchBox input").trigger("change")
+ return;
+ })
+
+ function insertAttachment(id) {
+ let audios = form.querySelector("input[name='audios']")
+
+ if(!audios.value.includes(id + ",")) {
+ if(audios.value.split(",").length > 10) {
+ NewNotification(tr("error"), tr("max_attached_audios"))
+ return false
+ }
+
+ form.querySelector("input[name='audios']").value += (id + ",")
+
+ return true
+ } else {
+ form.querySelector("input[name='audios']").value = form.querySelector("input[name='audios']").value.replace(id + ",", "")
+
+ return false
+ }
+ }
+
+ $(".audiosInsert").on("click", ".attachAudio", (ev) => {
+ if(!insertAttachment(ev.currentTarget.dataset.attachmentdata)) {
+ u(`.post-has-audios .post-has-audio[data-id='${ev.currentTarget.dataset.attachmentdata}']`).remove()
+ ev.currentTarget.querySelector("span").innerHTML = tr("attach_audio")
+ } else {
+ ev.currentTarget.querySelector("span").innerHTML = tr("detach_audio")
+
+ form.querySelector(".post-has-audios").insertAdjacentHTML("beforeend", `
+
+ ${ovk_proc_strtr(escapeHtml(ev.currentTarget.dataset.name), 40)}
+
+ `)
+
+ u(`#unattachAudio[data-id='${ev.currentTarget.dataset.attachmentdata}']`).on("click", (e) => {
+ let id = ev.currentTarget.dataset.attachmentdata
+ form.querySelector("input[name='audios']").value = form.querySelector("input[name='audios']").value.replace(id + ",", "")
+
+ u(e.currentTarget).remove()
+ })
+ }
+ })
+})
+
+$(document).on("click", ".audioEmbed.processed", (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])
+})
+
+$(document).on("click", ".musicIcon.report-icon", (e) => {
+ MessageBox(tr("report_question"), `
+ ${tr("going_to_report_audio")}
+
+
+ ${el.outerHTML}
+
+
+ ${isAttached ? tr("remove_from_playlist") : tr("add_to_playlist")}
+
+
+ `)
+ })
+
+ if(count < 1)
+ document.querySelector(".playlistAudiosContainer").insertAdjacentHTML("beforeend", `
+ ${tr("no_results")}
+ `)
+
+ if(Number(thisc.page) >= pagesCount)
+ u(".showMoreAudiosPlaylist").remove()
+ else {
+ if(document.querySelector(".showMoreAudiosPlaylist") != null) {
+ document.querySelector(".showMoreAudiosPlaylist").setAttribute("data-page", thisc.page + 1)
+
+ if(thisc.query != "") {
+ document.querySelector(".showMoreAudiosPlaylist").setAttribute("data-query", thisc.query)
+ }
+
+ document.querySelector(".showMoreAudiosPlaylist").style.display = "block"
+ } else {
+ document.querySelector(".playlistAudiosContainer").parentNode.insertAdjacentHTML("beforeend", `
+
+ ${tr("show_more_audios")}
+
+ `)
+ }
+ }
+
+ u("#loader").remove()
+}
+
+searcher.beforesendCallback = () => {
+ document.querySelector(".playlistAudiosContainer").parentNode.insertAdjacentHTML("beforeend", `
`)
+
+ if(document.querySelector(".showMoreAudiosPlaylist") != null)
+ document.querySelector(".showMoreAudiosPlaylist").style.display = "none"
+}
+
+searcher.errorCallback = () => {
+ fastError("Error when loading players")
+}
+
+searcher.clearContainer = () => {
+ document.querySelector(".playlistAudiosContainer").innerHTML = ""
+}
+
+$(document).on("click", ".showMoreAudiosPlaylist", (e) => {
+ searcher.movePage(Number(e.currentTarget.dataset.page))
+})
+
+$(document).on("change", "input#playlist_query", async (e) => {
+ e.preventDefault()
+
+ await new Promise(r => setTimeout(r, 500));
+
+ if(e.currentTarget.value === document.querySelector("input#playlist_query").value) {
+ searcher.clearContainer()
+
+ if(e.currentTarget.value == "") {
+ searcher.context_type = "entity_audios"
+ searcher.context_id = 0
+ searcher.query = ""
+
+ searcher.movePage(1)
+
+ return
+ }
+
+ searcher.context_type = "search_context"
+ searcher.context_id = 0
+ searcher.query = e.currentTarget.value
+
+ searcher.movePage(1)
+ return;
+ }
+})
diff --git a/Web/static/js/al_wall.js b/Web/static/js/al_wall.js
index 39ee2199..5acba57f 100644
--- a/Web/static/js/al_wall.js
+++ b/Web/static/js/al_wall.js
@@ -155,18 +155,6 @@ function setupWallPostInputHandlers(id) {
return;
}
});
-
- u("#wall-post-input" + id).on("input", function(e) {
- var boost = 5;
- var textArea = e.target;
- textArea.style.height = "5px";
- var newHeight = textArea.scrollHeight;
- textArea.style.height = newHeight + boost + "px";
- return;
-
- // revert to original size if it is larger (possibly changed by user)
- // textArea.style.height = (newHeight > originalHeight ? (newHeight + boost) : originalHeight) + "px";
- });
u("#wall-post-input" + id).on("dragover", function(e) {
e.preventDefault()
@@ -182,6 +170,18 @@ function setupWallPostInputHandlers(id) {
});
}
+u(document).on("input", "textarea", function(e) {
+ var boost = 5;
+ var textArea = e.target;
+ textArea.style.height = "5px";
+ var newHeight = textArea.scrollHeight;
+ textArea.style.height = newHeight + boost + "px";
+ return;
+
+ // revert to original size if it is larger (possibly changed by user)
+ // textArea.style.height = (newHeight > originalHeight ? (newHeight + boost) : originalHeight) + "px";
+});
+
function OpenMiniature(e, photo, post, photo_id, type = "post") {
/*
костыли но смешные однако
diff --git a/Web/static/js/package.json b/Web/static/js/package.json
index f9459ce4..f5c37008 100644
--- a/Web/static/js/package.json
+++ b/Web/static/js/package.json
@@ -2,6 +2,8 @@
"dependencies": {
"@atlassian/aui": "^9.6.0",
"create-react-class": "^15.7.0",
+ "dashjs": "^4.3.0",
+ "id3js": "^2.1.1",
"handlebars": "^4.7.7",
"jquery": "^3.0.0",
"knockout": "^3.5.1",
diff --git a/Web/static/js/yarn.lock b/Web/static/js/yarn.lock
index fa61ed9b..289fc81e 100644
--- a/Web/static/js/yarn.lock
+++ b/Web/static/js/yarn.lock
@@ -41,6 +41,11 @@ backbone@1.4.1:
dependencies:
underscore ">=1.8.3"
+codem-isoboxer@0.3.6:
+ version "0.3.6"
+ resolved "https://registry.yarnpkg.com/codem-isoboxer/-/codem-isoboxer-0.3.6.tgz#867f670459b881d44f39168d5ff2a8f14c16151d"
+ integrity sha512-LuO8/7LW6XuR5ERn1yavXAfodGRhuY2yP60JTZIw5yNYMCE5lUVbk3NFUCJxjnphQH+Xemp5hOGb1LgUXm00Xw==
+
core-js@^1.0.0:
version "1.2.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
@@ -64,6 +69,18 @@ dompurify@2.4.5:
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.5.tgz#0e89a27601f0bad978f9a924e7a05d5d2cccdd87"
integrity sha512-jggCCd+8Iqp4Tsz0nIvpcb22InKEBrGz5dw3EQJMs8HPJDsKbFIO3STYtAvCfDx26Muevn1MHVI0XxjgFfmiSA==
+dashjs@^4.3.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/dashjs/-/dashjs-4.3.0.tgz#cccda5a490cabf6c3b48aa887ec8c8ac0df1a233"
+ integrity sha512-cqpnJaPQpEY4DsEdF9prwD00+5dp5EGHCFc7yo9n2uuAH9k4zPkZJwXQ8dXmVRhPf3M89JfKSoAYIP3dbXmqcg==
+ dependencies:
+ codem-isoboxer "0.3.6"
+ es6-promise "^4.2.8"
+ fast-deep-equal "2.0.1"
+ html-entities "^1.2.1"
+ imsc "^1.0.2"
+ localforage "^1.7.1"
+
encoding@^0.1.11:
version "0.1.13"
resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9"
@@ -71,6 +88,11 @@ encoding@^0.1.11:
dependencies:
iconv-lite "^0.6.2"
+es6-promise@^4.2.8:
+ version "4.2.8"
+ resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
+ integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
+
event-lite@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/event-lite/-/event-lite-0.1.2.tgz#838a3e0fdddef8cc90f128006c8e55a4e4e4c11b"
@@ -81,6 +103,11 @@ fancy-file-input@2.0.4:
resolved "https://registry.yarnpkg.com/fancy-file-input/-/fancy-file-input-2.0.4.tgz#698c216482e07649a827681c4db3054fddc9a32b"
integrity sha512-l+J0WwDl4nM/zMJ/C8qleYnXMUJKsLng7c5uWH/miAiHoTvPDtEoLW1tmVO6Cy2O8i/1VfA+2YOwg/Q3+kgO6w==
+fast-deep-equal@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
+ integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
+
fbjs@^0.8.0:
version "0.8.17"
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
@@ -94,6 +121,10 @@ fbjs@^0.8.0:
setimmediate "^1.0.5"
ua-parser-js "^0.7.18"
+html-entities@^1.2.1:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.4.0.tgz#cfbd1b01d2afaf9adca1b10ae7dffab98c71d2dc"
+ integrity sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==
handlebars@^4.7.7:
version "4.7.7"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1"
@@ -113,11 +144,28 @@ iconv-lite@^0.6.2:
dependencies:
safer-buffer ">= 2.1.2 < 3.0.0"
+id3js@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/id3js/-/id3js-2.1.1.tgz#0c307d0d2f194bc5fa7a809bbed0b1a93577f16d"
+ integrity sha512-9Gi+sG0RHSa5qn8hkwi2KCl+2jV8YrtiZidXbOO3uLfRAxc2jilRg0fiQ3CbeoAmR7G7ap3RVs1kqUVhIyZaog==
+
ieee754@^1.1.8:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
+immediate@~3.0.5:
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
+ integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=
+
+imsc@^1.0.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/imsc/-/imsc-1.1.3.tgz#e96a60a50d4000dd7b44097272768b9fd6a4891d"
+ integrity sha512-IY0hMkVTNoqoYwKEp5UvNNKp/A5jeJUOrIO7judgOyhHT+xC6PA4VBOMAOhdtAYbMRHx9DTgI8p6Z6jhYQPFDA==
+ dependencies:
+ sax "1.2.1"
+
int64-buffer@^0.1.9:
version "0.1.10"
resolved "https://registry.yarnpkg.com/int64-buffer/-/int64-buffer-0.1.10.tgz#277b228a87d95ad777d07c13832022406a473423"
@@ -173,6 +221,13 @@ ky@^0.19.0:
resolved "https://registry.yarnpkg.com/ky/-/ky-0.19.0.tgz#d6ad117e89efe2d85a1c2e91462d48ca1cda1f7a"
integrity sha512-RkDgbg5ahMv1MjHfJI2WJA2+Qbxq0iNSLWhreYiCHeHry9Q12sedCnP5KYGPt7sydDvsyH+8UcG6Kanq5mpsyw==
+lie@3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
+ integrity sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=
+ dependencies:
+ immediate "~3.0.5"
+
literallycanvas@^0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/literallycanvas/-/literallycanvas-0.5.2.tgz#7d4800a8d9c4b38a593e91695d52466689586abd"
@@ -180,6 +235,13 @@ literallycanvas@^0.5.2:
dependencies:
react-addons-pure-render-mixin "^15.1"
+localforage@^1.7.1:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4"
+ integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==
+ dependencies:
+ lie "3.1.1"
+
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
@@ -273,6 +335,11 @@ requirejs@^2.3.6:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+sax@1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
+ integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o=
+
setimmediate@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
diff --git a/bootstrap.php b/bootstrap.php
index c4e5319b..96136116 100644
--- a/bootstrap.php
+++ b/bootstrap.php
@@ -64,6 +64,33 @@ function ovk_proc_strtr(string $string, int $length = 0): string
return $newString . ($string !== $newString ? "…" : ""); #if cut hasn't happened, don't append "..."
}
+function knuth_shuffle(iterable $arr, int $seed): array
+{
+ $data = is_array($arr) ? $arr : iterator_to_array($arr);
+ $retVal = [];
+ $ind = [];
+ $count = sizeof($data);
+
+ srand($seed, MT_RAND_PHP);
+
+ for($i = 0; $i < $count; ++$i)
+ $ind[$i] = 0;
+
+ for($i = 0; $i < $count; ++$i) {
+ do {
+ $index = rand() % $count;
+ } while($ind[$index] != 0);
+
+ $ind[$index] = 1;
+ $retVal[$i] = $data[$index];
+ }
+
+ # Reseed
+ srand(hexdec(bin2hex(openssl_random_pseudo_bytes(4))));
+
+ return $retVal;
+}
+
function bmask(int $input, array $options = []): Bitmask
{
return new Bitmask($input, $options["length"] ?? 1, $options["mappings"] ?? []);
diff --git a/composer.json b/composer.json
index c68dd4d1..373d667d 100644
--- a/composer.json
+++ b/composer.json
@@ -10,12 +10,13 @@
"wapmorgan/binary-stream": "dev-master",
"al/emoji-detector": "dev-master",
"ezyang/htmlpurifier": "dev-master",
- "scssphp/scssphp": "dev-master",
+ "scssphp/scssphp": "dev-main",
"lfkeitel/phptotp": "dev-master",
"chillerlan/php-qrcode": "dev-main",
"vearutop/php-obscene-censor-rus": "dev-master",
"erusev/parsedown": "dev-master",
"bhaktaraz/php-rss-generator": "dev-master",
+ "ext-openssl": "*",
"ext-simplexml": "*",
"symfony/console": "5.4.x-dev",
"wapmorgan/morphos": "dev-master",
diff --git a/install/init-static-db.sql b/install/init-static-db.sql
index 78b5e64a..7678587e 100644
--- a/install/init-static-db.sql
+++ b/install/init-static-db.sql
@@ -54,27 +54,6 @@ CREATE TABLE `attachments` (
`index` bigint(20) UNSIGNED NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
-CREATE TABLE `audios` (
- `id` bigint(20) UNSIGNED NOT NULL,
- `owner` bigint(20) UNSIGNED NOT NULL,
- `virtual_id` bigint(20) UNSIGNED NOT NULL,
- `created` bigint(20) UNSIGNED NOT NULL,
- `edited` bigint(20) UNSIGNED DEFAULT NULL,
- `hash` char(128) COLLATE utf8mb4_unicode_520_ci NOT NULL,
- `deleted` tinyint(4) DEFAULT 0,
- `name` varchar(190) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '(no name)',
- `performer` varchar(190) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT 'Unknown',
- `genre` varchar(190) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT 'K-POP',
- `lyrics` longtext COLLATE utf8mb4_unicode_520_ci DEFAULT NULL,
- `explicit` tinyint(4) NOT NULL DEFAULT 0
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
-
-CREATE TABLE `audio_relations` (
- `user` bigint(20) UNSIGNED NOT NULL,
- `audio` bigint(20) UNSIGNED NOT NULL,
- `index` bigint(20) UNSIGNED NOT NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
-
CREATE TABLE `comments` (
`id` bigint(20) UNSIGNED NOT NULL,
`owner` bigint(20) NOT NULL,
diff --git a/install/sqls/00041-music.sql b/install/sqls/00041-music.sql
new file mode 100644
index 00000000..49edbaf0
--- /dev/null
+++ b/install/sqls/00041-music.sql
@@ -0,0 +1,100 @@
+-- Apply these two commands if you installed OpenVK before 12th November 2023 OR if it's just doesn't work out of box, then apply this file again
+-- DROP TABLE `audios`;
+-- DROP TABLE `audio_relations`;
+
+CREATE TABLE IF NOT EXISTS `audios` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `owner` bigint unsigned NOT NULL,
+ `virtual_id` bigint unsigned NOT NULL,
+ `created` bigint unsigned NOT NULL,
+ `edited` bigint unsigned DEFAULT NULL,
+ `hash` char(128) NOT NULL,
+ `length` smallint unsigned NOT NULL,
+ `segment_size` decimal(20,6) NOT NULL DEFAULT '6.000000' COMMENT 'Size in seconds of each segment',
+ `kid` binary(16) NOT NULL,
+ `key` binary(16) NOT NULL,
+ `token` binary(28) NOT NULL COMMENT 'Key to access original file',
+ `listens` bigint unsigned NOT NULL DEFAULT '0',
+ `performer` varchar(256) NOT NULL,
+ `name` varchar(256) NOT NULL,
+ `lyrics` text,
+ `genre` enum('Blues','Big Band','Classic Rock','Chorus','Country','Easy Listening','Dance','Acoustic','Disco','Humour','Funk','Speech','Grunge','Chanson','Hip-Hop','Opera','Jazz','Chamber Music','Metal','Sonata','New Age','Symphony','Oldies','Booty Bass','Other','Primus','Pop','Porn Groove','R&B','Satire','Rap','Slow Jam','Reggae','Club','Rock','Tango','Techno','Samba','Industrial','Folklore','Alternative','Ballad','Ska','Power Ballad','Death Metal','Rhythmic Soul','Pranks','Freestyle','Soundtrack','Duet','Euro-Techno','Punk Rock','Ambient','Drum Solo','Trip-Hop','A Cappella','Vocal','Euro-House','Jazz+Funk','Dance Hall','Fusion','Goa','Trance','Drum & Bass','Classical','Club-House','Instrumental','Hardcore','Acid','Terror','House','Indie','Game','BritPop','Sound Clip','Negerpunk','Gospel','Polsk Punk','Noise','Beat','AlternRock','Christian Gangsta Rap','Bass','Heavy Metal','Soul','Black Metal','Punk','Crossover','Space','Contemporary Christian','Meditative','Christian Rock','Instrumental Pop','Merengue','Instrumental Rock','Salsa','Ethnic','Thrash Metal','Gothic','Anime','Darkwave','JPop','Techno-Industrial','Synthpop','Electronic','Abstract','Pop-Folk','Art Rock','Eurodance','Baroque','Dream','Bhangra','Southern Rock','Big Beat','Comedy','Breakbeat','Cult','Chillout','Gangsta Rap','Downtempo','Top 40','Dub','Christian Rap','EBM','Pop / Funk','Eclectic','Jungle','Electro','Native American','Electroclash','Cabaret','Emo','New Wave','Experimental','Psychedelic','Garage','Rave','Global','Showtunes','IDM','Trailer','Illbient','Lo-Fi','Industro-Goth','Tribal','Jam Band','Acid Punk','Krautrock','Acid Jazz','Leftfield','Polka','Lounge','Retro','Math Rock','Musical','New Romantic','Rock & Roll','Nu-Breakz','Hard Rock','Post-Punk','Folk','Post-Rock','Folk-Rock','Psytrance','National Folk','Shoegaze','Swing','Space Rock','Fast Fusion','Trop Rock','Bebob','World Music','Latin','Neoclassical','Revival','Audiobook','Celtic','Audio Theatre','Bluegrass','Neue Deutsche Welle','Avantgarde','Podcast','Gothic Rock','Indie Rock','Progressive Rock','G-Funk','Psychedelic Rock','Dubstep','Symphonic Rock','Garage Rock','Slow Rock','Psybient','Psychobilly','Touhou') DEFAULT NULL,
+ `explicit` tinyint(1) NOT NULL DEFAULT '0',
+ `withdrawn` tinyint(1) NOT NULL DEFAULT '0',
+ `processed` tinyint unsigned NOT NULL DEFAULT '0',
+ `checked` bigint NOT NULL DEFAULT '0' COMMENT 'Last time the audio availability was checked',
+ `unlisted` tinyint(1) NOT NULL DEFAULT '0',
+ `deleted` tinyint(1) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`id`),
+ KEY `owner_virtual_id` (`owner`,`virtual_id`),
+ KEY `genre` (`genre`),
+ KEY `unlisted` (`unlisted`),
+ KEY `listens` (`listens`),
+ KEY `deleted` (`deleted`),
+ KEY `length` (`length`),
+ KEY `listens_genre` (`listens`,`genre`),
+ FULLTEXT KEY `performer_name` (`performer`,`name`),
+ FULLTEXT KEY `lyrics` (`lyrics`),
+ FULLTEXT KEY `performer` (`performer`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
+
+CREATE TABLE IF NOT EXISTS `audio_listens` (
+ `entity` bigint NOT NULL,
+ `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,
+ KEY `user_time` (`entity`,`time`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
+
+CREATE TABLE IF NOT EXISTS `audio_relations` (
+ `entity` bigint NOT NULL,
+ `audio` bigint unsigned NOT NULL,
+ `index` bigint unsigned NOT NULL AUTO_INCREMENT,
+ PRIMARY KEY (`index`),
+ KEY `user` (`entity`) USING BTREE,
+ KEY `entity_audio` (`entity`,`audio`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
+
+CREATE TABLE IF NOT EXISTS `playlists` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `owner` bigint NOT NULL,
+ `name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci NOT NULL,
+ `description` varchar(2048) DEFAULT NULL,
+ `cover_photo_id` bigint unsigned DEFAULT NULL,
+ `length` int unsigned NOT NULL DEFAULT '0',
+ `special_type` tinyint unsigned NOT NULL DEFAULT '0',
+ `created` bigint unsigned DEFAULT NULL,
+ `listens` bigint(20) unsigned NOT NULL DEFAULT 0,
+ `edited` bigint unsigned DEFAULT NULL,
+ `deleted` tinyint unsigned DEFAULT '0',
+ PRIMARY KEY (`id`),
+ KEY `owner_deleted` (`owner`,`deleted`),
+ FULLTEXT KEY `title_description` (`name`,`description`),
+ FULLTEXT KEY `title` (`name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
+
+CREATE TABLE IF NOT EXISTS `playlist_imports` (
+ `entity` bigint NOT NULL,
+ `playlist` bigint unsigned NOT NULL,
+ `index` bigint unsigned NOT NULL AUTO_INCREMENT,
+ PRIMARY KEY (`index`) USING BTREE,
+ KEY `user` (`entity`) USING BTREE,
+ KEY `entity_audio` (`entity`,`playlist`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
+
+CREATE TABLE IF NOT EXISTS `playlist_relations` (
+ `collection` bigint unsigned NOT NULL,
+ `media` bigint unsigned NOT NULL,
+ `index` bigint unsigned NOT NULL AUTO_INCREMENT,
+ PRIMARY KEY (`index`) USING BTREE,
+ KEY `playlist` (`collection`) USING BTREE,
+ KEY `audio` (`media`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
+
+ALTER TABLE `groups` ADD `everyone_can_upload_audios` TINYINT(1) NOT NULL DEFAULT '0' AFTER `backdrop_2`;
+ALTER TABLE `profiles` ADD `last_played_track` BIGINT(20) UNSIGNED NULL DEFAULT NULL AFTER `client_name`, ADD `audio_broadcast_enabled` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `last_played_track`;
+ALTER TABLE `groups` ADD `last_played_track` BIGINT(20) UNSIGNED NULL DEFAULT NULL AFTER `everyone_can_upload_audios`, ADD `audio_broadcast_enabled` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `last_played_track`;
diff --git a/locales/en.strings b/locales/en.strings
index b1d211d1..ce4e53a2 100644
--- a/locales/en.strings
+++ b/locales/en.strings
@@ -35,18 +35,18 @@
"2fa_code_2" = "Two-factor authentication code";
"password_successfully_reset" = "Your password has been successfully reset.";
-"password_reset_email_sent" = "If you are registered, you will receive instructions by email.";
+"password_reset_email_sent" = "If you are registered, you will receive instructions via email.";
"password_reset_error" = "An unexpected error occurred while resetting the password.";
"password_reset_rate_limit_error" = "You can't do it that often, sorry.";
-"email_sent" = "Mail has been successfully sent.";
-"email_sent_desc" = "If your email addess exists, you will receive instructions.";
+"email_sent" = "Email has been successfully sent.";
+"email_sent_desc" = "If this email address exists, you will receive instructions.";
"email_error" = "An unexpected error occurred while sending the email.";
"email_rate_limit_error" = "You can't do it that often, sorry.";
-"email_verify_success" = "Your Email has been verified. Have a great time!";
+"email_verify_success" = "Your email has been verified. Have a great time!";
-"registration_disabled_info" = "Registration has been disabled by the system administrator. If possible, ask for an invitation from your friend, if he is registered on this site.";
+"registration_disabled_info" = "Registration has been disabled by the system administrator. If possible, request an invitation from your friend if they are registered on this site.";
"registration_closed" = "Registration is closed.";
"invites_you_to" = "$1 invites you to $2";
@@ -66,13 +66,13 @@
"birth_date" = "Birth date";
"registration_date" = "Registration date";
"hometown" = "Hometown";
-"this_is_you" = "it's you";
+"this_is_you" = "that's you";
"edit_page" = "Edit page";
"edit_group" = "Edit group";
"change_status" = "change status";
"name" = "Name";
"surname" = "Surname";
-"gender" = "Gender";
+"gender" = "Sex";
"male" = "male";
"female" = "female";
"description" = "Description";
@@ -94,7 +94,7 @@
"desc_none" = "no description";
"send" = "Send";
-"years_zero" = "0 year old";
+"years_zero" = "0 years old";
"years_one" = "1 year old";
"years_other" = "$1 years old";
@@ -313,9 +313,9 @@
"group_display_all_administrators" = "Display all administrators";
"group_dont_display_administrators_list" = "Display nothing";
-"group_changeowner_modal_title" = "Owner's permissions transfer";
-"group_changeowner_modal_text" = "Attention! You are transferring owner rights to user $1. This action is irreversible. After the transfer, you will remain an administrator, but you can easily stop being one.";
-"group_owner_setted" = "The new owner ($1) has been successfully assigned to the community $2. You have been granted administrator rights in the community. If you want to return the owner role, contact site technical support.";
+"group_changeowner_modal_title" = "Transfer Owner's Permissions";
+"group_changeowner_modal_text" = "Attention! You are transferring ownership rights to user $1. This action is irreversible. After the transfer, you will remain an administrator, but you can easily step down from this role.";
+"group_owner_setted" = "The new owner ($1) has been successfully assigned to the community $2. You have been granted administrator rights in the community. If you wish to revert to the owner role, please contact site technical support.";
"participants_zero" = "No participants";
"participants_one" = "$1 participant";
@@ -333,11 +333,11 @@
"meetings_one" = "$1 meeting";
"meetings_other" = "$1 meetings";
-"open_new_group" = "Open a new group";
-"open_group_desc" = "Can't find the right group? Open your own...";
-"search_group" = "Search group";
-"search_by_groups" = "Search by groups";
-"search_group_desc" = "Here you can browse through the existing groups and choose a group to suit your needs...";
+"open_new_group" = "Start a New Group";
+"open_group_desc" = "Unable to find the perfect group? Start your own...";
+"search_group" = "Search for a Group";
+"search_by_groups" = "Explore Groups";
+"search_group_desc" = "Browse through existing groups and find the one that suits your needs...";
"group_banned" = "Unfortunately, we had to block the $1 group.";
@@ -408,13 +408,13 @@
"max_filesize" = "Max filesize is $1 MB";
"uploading_photos_from_computer" = "Uploading photos from Your computer";
-"supported_formats" = "Supported file formats: JPG, PNG and GIF.";
+"supported_formats" = "Supported file formats: JPG, PNG, and GIF.";
"max_load_photos" = "You can upload up to 10 photos at a time.";
"tip" = "Tip";
-"tip_ctrl" = "to select multiple photos at once, hold down the Ctrl key when selecting files in Windows or the CMD key in Mac OS.";
-"album_poster" = "Album poster";
-"select_photo" = "Select photos";
-"upload_new_photo" = "Upload new photo";
+"tip_ctrl" = "To select multiple photos at once, hold down the Ctrl key when selecting files in Windows or the CMD key in Mac OS.";
+"album_poster" = "Album Poster";
+"select_photo" = "Select Photos";
+"upload_new_photo" = "Upload New Photo";
"is_x_photos_zero" = "Just zero photos.";
"is_x_photos_one" = "Just one photo.";
@@ -443,11 +443,11 @@
"notes_zero" = "No notes";
"notes_one" = "$1 note";
"notes_other" = "$1 notes";
-"notes_start_screen" = "With notes, you can share your events with friends and see what's going on with them.";
-"note_preview" = "Preview draft";
-"note_preview_warn" = "Preview mode active";
-"note_preview_warn_details" = "Notes may change their behaviour or looks after being saved. Also, try not to use preview very often.";
-"note_preview_empty_err" = "Preview of empty or nameless note? Why, Mr. White?";
+"notes_start_screen" = "Use notes to share your events with friends and stay updated on theirs.";
+"note_preview" = "Preview Draft";
+"note_preview_warn" = "Preview Mode Activated";
+"note_preview_warn_details" = "Note behavior or appearance may change after saving. Avoid excessive use of preview.";
+"note_preview_empty_err" = "Previewing an empty or nameless note? Why, Mr. White?";
"notes_list_zero" = "No notes found";
"notes_list_one" = "$1 note found";
@@ -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";
@@ -559,15 +560,16 @@
"results_other" = "$1 results";
"privacy_setting_access_page" = "Who can view my page";
-"privacy_setting_read_info" = "Who can see main information of my page";
+"privacy_setting_read_info" = "Who can see the main information on my page";
"privacy_setting_see_groups" = "Who can see my groups and meetings";
"privacy_setting_see_photos" = "Who can see my photos";
"privacy_setting_see_videos" = "Who can see my videos";
"privacy_setting_see_notes" = "Who can see my notes";
"privacy_setting_see_friends" = "Who can see my friends";
"privacy_setting_add_to_friends" = "Who can add me to friends";
-"privacy_setting_write_wall" = "Who can publish post on my wall";
+"privacy_setting_write_wall" = "Who can publish posts on my wall";
"privacy_setting_write_messages" = "Who can write messages to me";
+"privacy_setting_view_audio" = "Who can see my audios";
"privacy_value_anybody" = "Anybody";
"privacy_value_anybody_dative" = "Anybody";
"privacy_value_users" = "OpenVK users";
@@ -608,58 +610,58 @@
"email_change_confirm_message" = "Please confirm your new email address for the change to take effect. We have sent instructions to it.";
-"profile_deactivate" = "Delete account";
-"profile_deactivate_button" = "Delete account";
-"profile_deactivate_header" = "We are sorry that you want to delete your page. Therefore, you can specify the reason for deletion and your message about it. We read your feedback and try to make the site better!";
-"profile_deactivate_reason_header" = "Please select a reason why you are leaving";
+"profile_deactivate" = "Delete Account";
+"profile_deactivate_button" = "Delete Account";
+"profile_deactivate_header" = "We're sorry to see you go. Please share the reason for deleting your account and any feedback you have. Your input helps us improve the site!";
+"profile_deactivate_reason_header" = "Please select a reason for leaving";
"profile_deactivate_reason_1" = "I have another profile and don't need this one";
"profile_deactivate_reason_1_text" = "I created a new page and now I want to wipe my past.";
-"profile_deactivate_reason_2" = "Website takes away too much of my time";
-"profile_deactivate_reason_2_text" = "Even though this site is nice and beautiful, it takes away my time that I need for work and life.";
-"profile_deactivate_reason_3" = "Website harbors too much inappropriate content";
+"profile_deactivate_reason_2" = "The website consumes too much of my time";
+"profile_deactivate_reason_2_text" = "While the site is nice, it's taking away time I need for work and life.";
+"profile_deactivate_reason_3" = "The website has inappropriate content";
"profile_deactivate_reason_3_text" = "I have found enough porn and pirated content to last me a lifetime, I'm leaving now.";
-"profile_deactivate_reason_4" = "I am worried about the safety of my data";
-"profile_deactivate_reason_4_text" = "I'm being watched and I'm scared to be here. I'm sorry, I have to leave.";
+"profile_deactivate_reason_4" = "I am concerned about the safety of my data";
+"profile_deactivate_reason_4_text" = "I feel uneasy about my privacy here. I have to leave.";
"profile_deactivate_reason_5" = "No one comments on my posts";
-"profile_deactivate_reason_5_text" = "No one watches me here and it's sad. You will regret that I left.";
+"profile_deactivate_reason_5_text" = "This lack of engagement is disheartening. You'll regret losing me.";
"profile_deactivate_reason_6" = "Other reason";
"profile_deactivated_msg" = "Your account has been deleted.
";
-"tour_section_12_title_1" = "Profile and group backgrounds";
-"tour_section_12_text_1" = "You can set two images as the background of your page";
-"tour_section_12_text_2" = "They will be displayed on the sides of those who come to your page";
-"tour_section_12_text_3" = "Hint: before setting the background, try to experiment with the layout: try to mirror the future background image, or even just create a nice gradient";
+"tour_section_12_title_1" = "Profile and Group Backgrounds";
+"tour_section_12_text_1" = "You can set two images as the background of your page.";
+"tour_section_12_text_2" = "They will be displayed on the sides for those who visit your page.";
+"tour_section_12_text_3" = "Hint: Before setting the background, experiment with the layout—try mirroring the future background image or creating a nice gradient.";
"tour_section_12_title_2" = "Avatars";
-"tour_section_12_text_2_1" = "You can set the option to show the user's avatar: standard, rounded and square (1:1)";
-"tour_section_12_text_2_2" = "These settings will only be visible to you";
-"tour_section_12_title_3" = "Editing the left menu";
-"tour_section_12_text_3_1" = "If necessary, you can hide unnecessary sections of the site";
-"tour_section_12_text_3_2" = "Remember: The essential sections (My Page; My Friends; My Answers; My Settings) cannot be hidden";
-"tour_section_12_title_4" = "View of posts";
-"tour_section_12_text_4_1" = "If you're tired of the old wall design that was in the once-popular original VKontakte, you can always change the look of the posts to Microblog";
-"tour_section_12_text_4_2" = "The view of the posts can be changed between the two options at any time";
-"tour_section_12_text_4_3" = "Note that if you selected the old view of the posts, the latest comments will not be loaded";
-"tour_section_12_bottom_text_1" = "Background setup page";
-"tour_section_12_bottom_text_2" = "Examples of pages with set backgrounds";
-"tour_section_12_bottom_text_3" = "With this feature you can add more personality to your profile";
-"tour_section_12_bottom_text_4" = "Old post view";
+"tour_section_12_text_2_1" = "You can choose to display the user's avatar in standard, rounded, or square (1:1) styles.";
+"tour_section_12_text_2_2" = "These settings are visible only to you.";
+"tour_section_12_title_3" = "Editing the Left Menu";
+"tour_section_12_text_3_1" = "Hide unnecessary sections of the site if needed.";
+"tour_section_12_text_3_2" = "Remember: Essential sections like My Page, My Friends, My Answers, and My Settings cannot be hidden.";
+"tour_section_12_title_4" = "View of Posts";
+"tour_section_12_text_4_1" = "If you're tired of the old wall design from the once-popular original VKontakte, change the post look to Microblog.";
+"tour_section_12_text_4_2" = "You can switch between the two post views at any time.";
+"tour_section_12_text_4_3" = "Note: If you choose the old post view, the latest comments will not be loaded.";
+"tour_section_12_bottom_text_1" = "Background Setup Page";
+"tour_section_12_bottom_text_2" = "Examples of Pages with Set Backgrounds";
+"tour_section_12_bottom_text_3" = "Add more personality to your profile with this feature.";
+"tour_section_12_bottom_text_4" = "Old Post View";
"tour_section_12_bottom_text_5" = "Microblog";
"tour_section_13_title_1" = "Promocodes";
-"tour_section_13_text_1" = "OpenVK has a promo code system that aims to add some currency (rating percentages, votes, and so on)";
-"tour_section_13_text_2" = "Such coupons are created for some significant events and holidays. Follow the OpenVK Telegram channel";
-"tour_section_13_text_3" = "After activating a promocode, the currency set by the administrators will be transferred to you";
-"tour_section_13_text_4" = "Remember: All promocodes have a limited activation period";
-"tour_section_13_bottom_text_1" = "Promocodes consist of 24 digits and letters";
-"tour_section_13_bottom_text_2" = "Successful activation (for example, we were credited with 100 votes)";
-"tour_section_13_bottom_text_3" = "Attention: Once a promocode is activated on your page, the same promocode cannot be reactivated";
+"tour_section_13_text_1" = "OpenVK features a promo code system designed to add various currencies such as rating percentages, votes, and more.";
+"tour_section_13_text_2" = "These coupons are created for significant events and holidays. Stay updated on the latest by following the OpenVK Telegram channel.";
+"tour_section_13_text_3" = "Upon activating a promo code, the currency determined by administrators will be transferred to your account.";
+"tour_section_13_text_4" = "Remember: All promo codes have a limited activation period.";
+"tour_section_13_bottom_text_1" = "Promo codes consist of 24 digits and letters.";
+"tour_section_13_bottom_text_2" = "Successful activation, for example, results in the crediting of 100 votes.";
+"tour_section_13_bottom_text_3" = "Attention: Once a promo code is activated on your page, the same code cannot be reactivated.";
-"tour_section_14_title_1" = "Mobile version";
-"tour_section_14_text_1" = "At the moment there is no mobile web version of the site yet, but there is a mobile app for Android";
-"tour_section_14_text_2" = "OpenVK Legacy is an OpenVK mobile app for retro Android devices with the design of the original VKontakte 3.0 app from 2013";
-"tour_section_14_text_3" = "The minimum supported version is Android 2.1 Eclair, which means devices from the early 2010s will be quite useful";
+"tour_section_14_title_1" = "Mobile Version";
+"tour_section_14_text_1" = "Currently, there is no mobile web version of the site, but there is an Android mobile app.";
+"tour_section_14_text_2" = "OpenVK Legacy is an OpenVK mobile app designed for retro Android devices, featuring the design of the original VKontakte 3.0 app from 2013.";
+"tour_section_14_text_3" = "The minimum supported version is Android 2.1 Eclair, making devices from the early 2010s quite useful.";
-"tour_section_14_title_2" = "Where can I download this?";
-"tour_section_14_text_2_1" = "Release versions are downloaded through the official F-Droid repository";
-"tour_section_14_text_2_2" = "If you are a beta tester, new versions of the app are posted to a separate update channel";
-"tour_section_14_text_2_3" = "Note: The app may have various bugs and problems, if you encounter a mistake, please report it to the official app group";
+"tour_section_14_title_2" = "Where to Download?";
+"tour_section_14_text_2_1" = "Release versions can be downloaded through the official F-Droid repository.";
+"tour_section_14_text_2_2" = "If you're a beta tester, new app versions are posted to a separate update channel.";
+"tour_section_14_text_2_3" = "Note: The app may have various bugs and issues. If you encounter an error, please report it to the official app group.";
"tour_section_14_bottom_text_1" = "Screenshots";
-"tour_section_14_bottom_text_2" = "This concludes the tour of the site. If you want to try our mobile app, create your own group here, invite your friends or find new ones, or just have some fun in general, you can do it right now with a small registration";
-"tour_section_14_bottom_text_3" = "This concludes the tour of the site";
+"tour_section_14_bottom_text_2" = "That concludes the tour of the site. If you're interested in trying our mobile app, creating your group, inviting friends, or just having fun in general, you can do it now with a quick registration.";
+"tour_section_14_bottom_text_3" = "This concludes the site tour.";
/* Search */
@@ -1732,6 +1886,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";
@@ -1749,14 +1905,16 @@
"s_any" = "any";
"reset" = "Reset";
-"closed_group_post" = "This is a post from private group";
+"closed_group_post" = "This post was published in a private group";
"deleted_target_comment" = "This comment belongs to deleted post";
"no_results" = "No results";
+"s_only_performers" = "Performers only";
+"s_with_lyrics" = "With lyrics";
/* BadBrowser */
-"deprecated_browser" = "Deprecated browser";
+"deprecated_browser" = "Deprecated Browser";
"deprecated_browser_description" = "To view this content, you will need Firefox ESR 52+ or an equivalent World Wide Web navigator. Sorry about that.";
/* Statistics */
@@ -1777,21 +1935,22 @@
/* Sudo */
-"you_entered_as" = "You logged as";
+"you_entered_as" = "You are logged in as";
"please_rights" = "please respect the right to privacy of other people's correspondence and do not abuse user swapping.";
"click_on" = "Click";
-"there" = "there";
-"to_leave" = "to logout";
+"there" = "here";
+"to_leave" = "to log out";
/* Phone number */
"verify_phone_number" = "Confirm phone number";
-"we_sended_first" = "We sended SMS with code on number";
+"we_sended_first" = "We sent an SMS with code to the given number";
"we_sended_end" = "enter it here";
/* Mobile */
"mobile_friends" = "Friends";
"mobile_photos" = "Photos";
+"mobile_audios" = "Audios";
"mobile_videos" = "Videos";
"mobile_messages" = "Messages";
"mobile_notes" = "Notes";
@@ -1805,6 +1964,9 @@
"mobile_user_info_hide" = "Hide";
"mobile_user_info_show_details" = "Show details";
+"my" = "My";
+"enter_a_name_or_artist" = "Enter a name or artist...";
+
/* Moderation */
"section" = "Section";
diff --git a/locales/list.yml b/locales/list.yml
index 64a43612..eed50bb7 100644
--- a/locales/list.yml
+++ b/locales/list.yml
@@ -74,6 +74,11 @@ list:
name: "Soviet"
native_name: "Советский"
author: "mohooks"
+ - code: "ru_lat"
+ flag: "ru"
+ name: "Russian (Latin)"
+ native_name: "Russkij (Latinica)"
+ author: "IsamiRi (@isamirivers)"
- code: "udm"
flag: "udm"
name: "Udmurtskiy"
diff --git a/locales/ru.strings b/locales/ru.strings
index 49871399..63f40462 100644
--- a/locales/ru.strings
+++ b/locales/ru.strings
@@ -461,6 +461,7 @@
"my_videos" = "Мои Видеозаписи";
"my_messages" = "Мои Сообщения";
"my_notes" = "Мои Заметки";
+"my_audios" = "Мои Аудиозаписи";
"my_groups" = "Мои Группы";
"my_feed" = "Мои Новости";
"my_feedback" = "Мои Ответы";
@@ -540,6 +541,7 @@
"privacy_setting_add_to_friends" = "Кто может называть меня другом";
"privacy_setting_write_wall" = "Кто может писать у меня на стене";
"privacy_setting_write_messages" = "Кто может писать мне сообщения";
+"privacy_setting_view_audio" = "Кому видно мои аудиозаписи";
"privacy_value_anybody" = "Все желающие";
"privacy_value_anybody_dative" = "Всем желающим";
"privacy_value_users" = "Пользователям OpenVK";
@@ -672,9 +674,128 @@
"upload_new_video" = "Загрузить новое видео";
"max_attached_videos" = "Максимум 10 видеозаписей";
"max_attached_photos" = "Максимум 10 фотографий";
+"max_attached_audios" = "Максимум 10 аудиозаписей";
"no_videos" = "У вас нет видео.";
"no_videos_results" = "Нет результатов.";
+/* Audios */
+
+"audios" = "Аудиозаписи";
+"audio" = "Аудиозапись";
+"playlist" = "Плейлист";
+"upload_audio" = "Загрузить аудио";
+"upload_audio_to_group" = "Загрузить аудио в группу";
+
+"performer" = "Исполнитель";
+"audio_name" = "Название";
+"genre" = "Жанр";
+"lyrics" = "Текст";
+
+"select_another_file" = "Выбрать другой файл";
+
+"limits" = "Ограничения";
+"select_audio" = "Выберите аудиозапись на Вашем компьютере";
+"audio_requirements" = "Аудиозапись должна быть длинной от $1c до $2 минут, весить до $3мб и содержать аудиопоток.";
+"audio_requirements_2" = "Аудиозапись не должна нарушать авторские и смежные права.";
+"you_can_also_add_audio_using" = "Вы также можете добавить аудиозапись из числа уже загруженных файлов, воспользовавшись";
+"search_audio_inst" = "поиском по аудио";
+
+"audio_embed_not_found" = "Аудиозапись не найдена";
+"audio_embed_deleted" = "Аудиозапись была удалена";
+"audio_embed_withdrawn" = "Аудиозапись была изъята по обращению правообладателя.";
+"audio_embed_forbidden" = "Настройки приватности пользователя не позволяют встраивать эту композицию";
+"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" = "Моя музыка";
+"music_user" = "Музыка пользователя";
+"music_club" = "Музыка группы";
+"audio_new" = "Новое";
+"audio_popular" = "Популярное";
+"audio_search" = "Поиск";
+
+"my_audios_small" = "Мои аудиозаписи";
+"my_playlists" = "Мои плейлисты";
+"playlists" = "Плейлисты";
+"audios_explicit" = "Содержит нецензурную лексику";
+"withdrawn" = "Изъято";
+"deleted" = "Удалено";
+"owner" = "Владелец";
+"searchable" = "Доступно в поиске";
+
+"select_audio" = "Выбрать аудиозаписи";
+"no_playlists_thisuser" = "Вы ещё не добавляли плейлистов.";
+"no_playlists_user" = "Этот пользователь ещё не добавлял плейлистов.";
+"no_playlists_club" = "Эта группа ещё не добавляла плейлистов.";
+
+"no_audios_thisuser" = "Вы ещё не добавляли аудиозаписей.";
+"no_audios_user" = "Этот пользователь ещё не добавлял аудиозаписей.";
+"no_audios_club" = "Эта группа ещё не добавляла аудиозаписей.";
+
+"new_playlist" = "Новый плейлист";
+"created_playlist" = "создан";
+"updated_playlist" = "обновлён";
+"bookmark" = "Добавить в коллекцию";
+"unbookmark" = "Убрать из коллекции";
+"empty_playlist" = "В этом плейлисте нет аудиозаписей.";
+"edit_playlist" = "Редактировать плейлист";
+"unable_to_load_queue" = "Не удалось загрузить очередь.";
+
+"fully_delete_audio" = "Полностью удалить аудиозапись";
+"attach_audio" = "Прикрепить аудиозапись";
+"detach_audio" = "Открепить аудиозапись";
+
+"show_more_audios" = "Показать больше аудиозаписей";
+"add_to_playlist" = "Добавить в плейлист";
+"remove_from_playlist" = "Удалить из плейлиста";
+"delete_playlist" = "Удалить плейлист";
+"playlist_cover" = "Обложка плейлиста";
+"playlists_user" = "Плейлисты польз.";
+"playlists_club" = "Плейлисты группы";
+"change_cover" = "Сменить обложку";
+"playlist_cover" = "Обложка плейлиста";
+
+"minutes_count_zero" = "длится ноль минут";
+"minutes_count_one" = "длится одну минуту";
+"minutes_count_few" = "длится $1 минуты";
+"minutes_count_many" = "длится $1 минут";
+"minutes_count_other" = "длится $1 минут";
+
+"listens_count_zero" = "нет прослушиваний";
+"listens_count_one" = "одно прослушивание";
+"listens_count_few" = "$1 прослушивания";
+"listens_count_many" = "$1 прослушиваний";
+"listens_count_other" = "$1 прослушиваний";
+
+"add_audio_to_club" = "Добавить аудио в группу";
+"what_club_add" = "В какую группу вы хотите добавить песню?";
+"group_has_audio" = "У группы уже есть эта песня.";
+"group_hasnt_audio" = "У группы нет этой песни.";
+
+"by_name" = "по композициям";
+"by_performer" = "по исполнителю";
+"no_access_clubs" = "Нет групп, где вы являетесь администратором.";
+"audio_successfully_uploaded" = "Аудио успешно загружено и на данный момент обрабатывается.";
+
+"broadcast_audio" = "Транслировать аудио в статус";
+"sure_delete_playlist" = "Вы действительно хотите удалить этот плейлист?";
+"edit_audio" = "Редактировать аудиозапись";
+"audios_group" = "Аудиозаписи группы";
+"playlists_group" = "Плейлисты группы";
+
+"play_tip" = "Проигрывание/пауза";
+"repeat_tip" = "Повторение";
+"shuffle_tip" = "Перемешать";
+"mute_tip" = "Заглушить";
+
/* Notifications */
"feedback" = "Ответы";
@@ -915,6 +1036,7 @@
"going_to_report_photo" = "Вы собираетесь пожаловаться на данную фотографию.";
"going_to_report_user" = "Вы собираетесь пожаловаться на данного пользователя.";
"going_to_report_video" = "Вы собираетесь пожаловаться на данную видеозапись.";
+"going_to_report_audio" = "Вы собираетесь пожаловаться на данную аудиозапись.";
"going_to_report_post" = "Вы собираетесь пожаловаться на данную запись.";
"going_to_report_comment" = "Вы собираетесь пожаловаться на данный комментарий.";
@@ -1030,6 +1152,7 @@
"topics_other" = "$1 тем";
"created" = "Создано";
"everyone_can_create_topics" = "Все могут создавать темы";
+"everyone_can_upload_audios" = "Все могут загружать аудиозаписи";
"display_list_of_topics_above_wall" = "Отображать список тем над стеной";
"topic_changes_saved_comment" = "Обновлённый заголовок и настройки появятся на странице с темой.";
"failed_to_create_topic" = "Не удалось создать тему";
@@ -1047,6 +1170,7 @@
"no_data" = "Нет данных";
"no_data_description" = "Тут ничего нет... Пока...";
"error" = "Ошибка";
+"error_generic" = "Произошла ошибка общего характера: ";
"error_shorturl" = "Данный короткий адрес уже занят.";
"error_segmentation" = "Ошибка сегментации";
"error_upload_failed" = "Не удалось загрузить фото";
@@ -1055,6 +1179,8 @@
"error_weak_password" = "Ненадёжный пароль. Пароль должен содержать не менее 8 символов, цифры, прописные и строчные буквы";
"error_shorturl_incorrect" = "Короткий адрес имеет некорректный формат.";
"error_repost_fail" = "Не удалось поделиться записью";
+
+"error_insufficient_info" = "Вы не указали необходимую информацию.";
"error_data_too_big" = "Аттрибут '$1' не может быть длиннее $2 $3";
"forbidden" = "Ошибка доступа";
"unknown_error" = "Неизвестная ошибка";
@@ -1181,6 +1307,21 @@
"group_owner_is_banned" = "Создатель сообщества успешно забанен.";
"group_is_banned" = "Сообщество успешно забанено";
"description_too_long" = "Описание слишком длинное.";
+"invalid_audio" = "Такой аудиозаписи не существует.";
+"do_not_have_audio" = "У вас нет этой аудиозаписи.";
+"do_have_audio" = "У вас уже есть эта аудиозапись.";
+
+"set_playlist_name" = "Укажите название плейлиста.";
+"playlist_already_bookmarked" = "Плейлист уже есть в вашей коллекции.";
+"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 */
@@ -1277,6 +1418,10 @@
"admin_gift_moved_successfully" = "Подарок успешно перемещён";
"admin_gift_moved_to_recycle" = "Теперь подарок находится в корзине.";
+"admin_original_file" = "Оригинальный файл";
+"admin_audio_length" = "Длина";
+"admin_cover_id" = "Обложка (ID фото)";
+"admin_music" = "Музыка";
"logs" = "Логи";
"logs_anything" = "Любое";
@@ -1508,8 +1653,16 @@
"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_bottom_text_1" = "Важно: песня не должна нарушать авторские права";
+"tour_section_6_title_2" = "Создавайте плейлисты";
+"tour_section_6_text_6" = "Вы можете создавать сборники треков во вкладке \"Мои плейлисты\"";
+"tour_section_6_text_7" = "Можно также добавлять чужие плейлисты в свою коллекцию";
"tour_section_7_title_1" = "Следите за тем, что пишут ваши друзья";
@@ -1620,6 +1773,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" = "По дате";
@@ -1641,6 +1796,8 @@
"deleted_target_comment" = "Этот комментарий принадлежит к удалённой записи";
"no_results" = "Результатов нет";
+"s_only_performers" = "Только исполнители";
+"s_with_lyrics" = "С текстом";
/* BadBrowser */
@@ -1680,6 +1837,7 @@
/* Mobile */
"mobile_friends" = "Друзья";
"mobile_photos" = "Фотографии";
+"mobile_audios" = "Аудиозаписи";
"mobile_videos" = "Видеозаписи";
"mobile_messages" = "Сообщения";
"mobile_notes" = "Заметки";
@@ -1693,6 +1851,9 @@
"mobile_user_info_hide" = "Скрыть";
"mobile_user_info_show_details" = "Показать подробнее";
+"my" = "Мои";
+"enter_a_name_or_artist" = "Введите название или автора...";
+
/* Moderation */
"section" = "Раздел";
diff --git a/locales/ru_lat.strings b/locales/ru_lat.strings
new file mode 100644
index 00000000..7f1308b7
--- /dev/null
+++ b/locales/ru_lat.strings
@@ -0,0 +1,1855 @@
+#include
+
+"__locale" = "ru_RU.UTF-8;Rus";
+"__WinEncoding" = "Windows-1251";
+"__transNames" = "[\P{script=Han}]; Russian-Latin/BGN; Any-Latin";
+
+/* Check for https://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html */
+
+/* Main page */
+
+"home" = "Glavnaja";
+"welcome" = "Dobro požalovatí";
+"to_top" = "Vverh";
+
+/* Login */
+
+"log_in" = "Vhod";
+"password" = "Parolí";
+"registration" = "Registracija";
+"forgot_password" = "Zabyli parolí?";
+"checkbox_in_registration" = "Ja soglasen s politikoj konfidencialínosti i pravilami sajta";
+"checkbox_in_registration_unchecked" = "Vy dolžny soglasitísja s politikoj konfidencialínosti i pravilami, čtoby zaregistrirovatísja.";
+"login_failed" = "Ne udalosí vojti";
+"invalid_username_or_password" = "Nevernoe imja polízovatelja ili parolí. Zabyli parolí?";
+"failed_to_register" = "Ne udalosí zaregistrirovatísja";
+"referral_link_invalid" = "Priglasitelínaja ssylka nedejstvitelína.";
+"registration_disabled" = "Registracija otključena sistemnym administratorom.";
+"user_already_exists" = "Polízovatelí s takim email uže susčestvuet.";
+"access_recovery" = "Vosstanovlenie dostupa";
+"page_access_recovery" = "Vosstanovití dostup k stranice";
+"access_recovery_info" = "Zabyli parolí? Ne volnujtesí, vvedite vaši dannye i my otpravim vam email s instrukcijami po vosstanovleniju akkaunta.";
+"access_recovery_info_2" = "Vvedite vaš novyj parolí. Vse tekusčie seansy budut priostanovleny i tokeny dostupa budut annulirovany.";
+"reset_password" = "Sbrosití parolí";
+"2fa_code_2" = "Kod dvuhfaktornoj autentifikacii";
+"password_successfully_reset" = "Vaš parolí byl uspešno sbrošen.";
+"password_reset_email_sent" = "Esli vy zaregistrirovany, vy polučite instrukcii na email.";
+"password_reset_error" = "Nepredvidennaja ošibka pri sbrose parolja.";
+"password_reset_rate_limit_error" = "Nelízja delatí eto tak často, izvinite.";
+"email_sent" = "Pisímo bylo uspešno otpravleno.";
+"email_sent_desc" = "Esli vaš elektronnyj adres susčestvuet, vy polučite pisímo.";
+"email_error" = "Nepredvidennaja ošibka pri otpravke pisíma.";
+"email_rate_limit_error" = "Nelízja delatí eto tak často, izvinite.";
+"email_verify_success" = "Vaš Email byl podtverždjon. Prijatnogo vremjapreprovoždenija!";
+"registration_disabled_info" = "Registracija otključena sistemnym administratorom. Pri vozmožnosti poprosite priglašenie u vašego znakomogo, esli on zaregistrirovan na etom sajte.";
+"registration_closed" = "Registracija zakryta.";
+"invites_you_to" = "$1 priglašaet vas v $2";
+"register_meta_desc" = "Zaregistrirujtesí v $1 prjamo sejčas!";
+"register_referer_meta_title" = "$1 priglašaet vas v $2!";
+"register_referer_meta_desc" = "Prisoedinjajtesí k $1 i množestvu drugih polízovatelej v $2!";
+"registration_welcome_1" = "- universalínoe sredstvo poiska kolleg osnovannoe na strukture VKontakte.";
+"registration_welcome_2" = "My hotim, čtoby druzíja, odnokursniki, odnoklassniki, sosedi i kollegi vsegda mogli bytí v kontakte.";
+"users" = "Polízovateli";
+"other_fields" = "Ostalínoe";
+
+/* Profile information */
+
+"select_language" = "Vybratí jazyk";
+"edit" = "Redaktirovatí";
+"birth_date" = "Dení roždenija";
+"registration_date" = "Data registracii";
+"hometown" = "Rodnoj gorod";
+"this_is_you" = "eto Vy";
+"edit_page" = "Redaktirovatí stranicu";
+"edit_group" = "Redaktirovatí gruppu";
+"change_status" = "izmenití status";
+"name" = "Imja";
+"surname" = "Familija";
+"gender" = "Pol";
+"male" = "mužskoj";
+"female" = "ženskij";
+"description" = "Opisanie";
+"save" = "Sohranití";
+"main_information" = "Osnovnaja informacija";
+"additional_information" = "Dopolnitelínaja informacija";
+"nickname" = "Niknejm";
+"online" = "Onlajn";
+"was_online" = "byl v seti";
+"was_online_m" = "byl v seti";
+"was_online_f" = "byla v seti";
+"all_title" = "Vse";
+"information" = "Informacija";
+"status" = "Status";
+"no_information_provided" = "Informacija otsutstvuet.";
+"deceased_person" = "Stranica pokojnogo čeloveka";
+"none" = "otsutstvuet";
+"desc_none" = "opisanie otsutstvuet";
+"send" = "Otpravití";
+"years_zero" = "0 let";
+"years_one" = "1 god";
+"years_few" = "$1 goda";
+"years_many" = "$1 let";
+"years_other" = "$1 let";
+"show_my_birthday" = "Pokazyvatí datu roždenija";
+"show_only_month_and_day" = "Pokazyvatí tolíko dení i mesjac";
+"relationship" = "Semejnoe položenie";
+"relationship_0" = "Ne vybrano";
+"relationship_1" = "Ne ženat";
+"relationship_2" = "Vstrečajusí";
+"relationship_3" = "Pomolvlen";
+"relationship_4" = "Ženat";
+"relationship_5" = "V graždanskom brake";
+"relationship_6" = "Vljubljon";
+"relationship_7" = "Vsjo složno";
+"relationship_8" = "V aktivnom poiske";
+"relationship_1_fem" = "Ne zamužem";
+"relationship_3_fem" = "Pomolvlena";
+"relationship_4_fem" = "Zamužem";
+"relationship_6_fem" = "Vljublena";
+"politViews" = "Polit. vzgljady";
+"politViews_0" = "Ne vybrany";
+"politViews_1" = "Indifferentnye";
+"politViews_2" = "Kommunističeskie";
+"politViews_3" = "Socialističeskie";
+"politViews_4" = "Umerennye";
+"politViews_5" = "Liberalínye";
+"politViews_6" = "Konservativnye";
+"politViews_7" = "Monarhičeskie";
+"politViews_8" = "Ulítrakonservativnye";
+"politViews_9" = "Libertarianskie";
+"contact_information" = "Kontaktnaja informacija";
+"email" = "Elektronnaja počta";
+"phone" = "Telefon";
+"telegram" = "Telegram";
+"personal_website" = "Ličnyj sajt";
+"city" = "Gorod";
+"address" = "Adres";
+"personal_information" = "Ličnaja informacija";
+"interests" = "Interesy";
+"favorite_music" = "Ljubimaja muzyka";
+"favorite_films" = "Ljubimye filímy";
+"favorite_shows" = "Ljubimye TV-šou";
+"favorite_books" = "Ljubimye knigi";
+"favorite_quotes" = "Ljubimye citaty";
+"information_about" = "O sebe";
+"updated_at" = "Obnovleno $1";
+"user_banned" = "K sožaleniju, nam prišlosí zablokirovatí stranicu polízovatelja $1.";
+"user_banned_comment" = "Kommentarij moderatora:";
+"verified_page" = "Podtverždjonnaja stranica";
+"user_is_blocked" = "Polízovatelí zablokirovan";
+"before" = "do";
+"forever" = "navsegda";
+
+/* Wall */
+
+"feed" = "Novosti";
+"post_writes_m" = "napisal";
+"post_writes_f" = "napisala";
+"post_writes_g" = "opublikovali";
+"post_deact_m" = "udalil stranicu so slovami:";
+"post_deact_f" = "udalila stranicu so slovami:";
+"post_deact_silent_m" = "molča udalil svoju stranicu.";
+"post_deact_silent_f" = "molča udalila svoju stranicu.";
+"post_on_your_wall" = "na vašej stene";
+"post_on_group_wall" = "в $1";
+"post_on_user_wall" = "na stene $1";
+"wall" = "Stena";
+"post" = "Zapisí";
+"write" = "Napisatí";
+"publish" = "Opublikovatí";
+"delete" = "Udalití";
+"comments" = "Kommentarii";
+"share" = "Podelitísja";
+"pin" = "Zakrepití";
+"unpin" = "Otkrepití";
+"pinned" = "zakrepleno";
+"comments_tip" = "Budíte pervym, kto ostavit kommentarij!";
+"your_comment" = "Vaš kommentarij";
+"auditory" = "Auditorija";
+"in_wall" = "na stenu";
+"in_group" = "v gruppu";
+"shown" = "Pokazano";
+"x_out_of" = "$1 iz";
+"wall_zero" = "net zapisej";
+"wall_one" = "edinstvennaja zapisí";
+"wall_few" = "$1 zapisi";
+"wall_many" = "$1 zapisej";
+"wall_other" = "$1 zapisej";
+"publish_post" = "Dobavití zapisí";
+"view_other_comments" = "Posmotretí ostalínye kommentarii";
+"no_comments" = "Kommentarii otsutstvujut";
+"my_news" = "Moi novosti";
+"all_news" = "Vse novosti";
+"posts_per_page" = "Količestvo zapisej na stranice";
+"attachment" = "Vloženie";
+"post_as_group" = "Ot imeni soobsčestva";
+"comment_as_group" = "Ot imeni soobsčestva";
+"add_signature" = "Podpisí avtora";
+"contains_nsfw" = "Soderžit NSFW-kontent";
+"nsfw_warning" = "Dannyj post možet soderžatí 18+ kontent";
+"report" = "Požalovatísja";
+"attach" = "Prikrepití";
+"detach" = "Otkrepití";
+"attach_photo" = "Prikrepití foto";
+"attach_video" = "Prikrepití video";
+"draw_graffiti" = "Narisovatí graffiti";
+"no_posts_abstract" = "Zdesí nikto ničego ne napisal... Poka.";
+"attach_no_longer_available" = "Eto vloženie bolíše nedostupno.";
+"open_post" = "Otkrytí zapisí";
+"version_incompatibility" = "Ne udalosí otobrazití eto vloženie. Vozmožno, baza dannyh nesovmestima s tekusčej versiej OpenVK.";
+"graffiti" = "Graffiti";
+"reply" = "Otvetití";
+"post_is_ad" = "Etot post byl razmesčjon za vzjatku.";
+"edited_short" = "red.";
+
+/* Friends */
+
+"friends" = "Druzíja";
+"followers" = "Podpisčiki";
+"follower" = "Podpisčik";
+"friends_add" = "Dobavití v druzíja";
+"friends_delete" = "Udalití iz druzej";
+"friends_reject" = "Otmenití zajavku";
+"friends_accept" = "Prinjatí zajavku";
+"send_message" = "Otpravití soobsčenie";
+"incoming_req" = "Vhodjasčie";
+"outcoming_req" = "Ishodjasčie";
+"req" = "Zajavki";
+"friends_online" = "Druzíja onlajn";
+"all_friends" = "Vse druzíja";
+"req_zero" = "Ne najdeno ni odnoj zajavki...";
+"req_one" = "Najdena $1 zajavka";
+"req_few" = "Najdeno $1 zajavki";
+"req_many" = "Najdeno $1 zajavki";
+"req_other" = "Najdeno $1 zajavok";
+"friends_zero" = "Ni odnogo druga";
+"friends_one" = "$1 drug";
+"friends_few" = "$1 druga";
+"friends_many" = "$1 druzej";
+"friends_other" = "$1 druzej";
+"friends_online_zero" = "Ni odnogo druga onlajn";
+"friends_online_one" = "$1 drug onlajn";
+"friends_online_few" = "$1 druga onlajn";
+"friends_online_many" = "$1 druzej onlajn";
+"friends_online_other" = "$1 druzej onlajn";
+"friends_list_zero" = "U Vas poka net druzej";
+"friends_list_one" = "U Vas $1 drug";
+"friends_list_few" = "U Vas $1 druga";
+"friends_list_many" = "U Vas $1 druzej";
+"friends_list_other" = "U Vas $1 druzej";
+"followers_zero" = "Ni odnogo podpisčika";
+"followers_one" = "$1 podpisčik";
+"followers_few" = "$1 podpisčika";
+"followers_many" = "$1 podpisčikov";
+"followers_other" = "$1 podpisčikov";
+"subscriptions_zero" = "Ni odnoj podpiski";
+"subscriptions_one" = "$1 podpiska";
+"subscriptions_few" = "$1 podpiski";
+"subscriptions_many" = "$1 podpisok";
+"subscriptions_other" = "$1 podpisok";
+"friends_list_online_zero" = "U Vas poka net druzej onlajn";
+"friends_list_online_one" = "U Vas $1 drug onlajn";
+"friends_list_online_few" = "U Vas $1 druga onlajn";
+"friends_list_online_many" = "U Vas $1 druzej onlajn";
+"friends_list_online_other" = "U Vas $1 druzej onlajn";
+
+/* Group */
+
+"name_group" = "Nazvanie";
+"subscribe" = "Podpisatísja";
+"unsubscribe" = "Otpisatísja";
+"subscriptions" = "Podpiski";
+"join_community" = "Vstupití v gruppu";
+"leave_community" = "Vyjti iz gruppy";
+"check_community" = "Prosmotr gruppy";
+"min_6_community" = "Nazvanie dolžno bytí ne menee 6 simvolov";
+"participants" = "Učastniki";
+"groups" = "Gruppy";
+"meetings" = "Vstreči";
+"create_group" = "Sozdatí gruppu";
+"group_managers" = "Rukovodstvo";
+"group_type" = "Tip gruppy";
+"group_type_open" = "Eto otkrytaja gruppa. V nejo možet vstupití ljuboj želajusčij.";
+"group_type_closed" = "Eto zakrytaja gruppa. Dlja vstuplenija neobhodimo podavatí zajavku.";
+"creator" = "Sozdatelí";
+"administrators" = "Administratory";
+"add_to_left_menu" = "Dobavití v levoe menju";
+"remove_from_left_menu" = "Udalití iz levogo menju";
+"all_followers" = "Vse podpisčiki";
+"only_administrators" = "Tolíko administratory";
+"website" = "Sajt";
+"managed" = "Upravljaemye";
+"size" = "Razmer";
+"administrators_one" = "$1 administrator";
+"administrators_few" = "$1 administratora";
+"administrators_other" = "$1 administratorov";
+"role" = "Rolí";
+"administrator" = "Administrator";
+"promote_to_admin" = "Povysití do administratora";
+"promote_to_owner" = "Naznačití vladelícem";
+"devote" = "Razžalovatí";
+"set_comment" = "Izmenití kommentarij";
+"hidden_yes" = "Skryt: Da";
+"hidden_no" = "Skryt: Net";
+"group_allow_post_for_everyone" = "Razrešití publikovatí zapisi vsem";
+"group_hide_from_global_feed" = "Ne otobražatí publikacii v globalínoj lente";
+"statistics" = "Statistika";
+"group_administrators_list" = "Spisok adminov";
+"group_display_only_creator" = "Otobražatí tolíko sozdatelja gruppy";
+"group_display_all_administrators" = "Otobražatí vseh administratorov";
+"group_dont_display_administrators_list" = "Ničego ne otobražatí";
+"group_changeowner_modal_title" = "Peredača prav vladelíca";
+"group_changeowner_modal_text" = "Vnimanie! Vy peredajote prava vladelíca polízovatelju $1. Eto dejstvie neobratimo. Posle peredači vy ostanetesí adminstratorom, no smožete legko perestatí im bytí.";
+"group_owner_setted" = "Novyj vladelec ($1) uspešno naznačen v soobsčestvo $2. Vam vydany prava administratora v soobsčestve. Esli Vy hotite vernutí rolí vladelíca, obratitesí v tehničeskuju podderžku sajta.";
+"participants_zero" = "Ni odnogo učastnika";
+"participants_one" = "Odin učastnik";
+"participants_few" = "$1 učastnika";
+"participants_many" = "$1 učastnikov";
+"participants_other" = "$1 učastnikov";
+"groups_zero" = "Ni odnoj gruppy";
+"groups_one" = "Odna gruppa";
+"groups_few" = "$1 gruppy";
+"groups_many" = "$1 grupp";
+"groups_other" = "$1 grupp";
+"groups_list_zero" = "Vy ne sostoite ni v odnoj gruppe";
+"groups_list_one" = "Vy sostoite v odnoj gruppe";
+"groups_list_other" = "Vy sostoite v $1 gruppah";
+"meetings_zero" = "Ni odnoj vstreči";
+"meetings_one" = "Odna vstreča";
+"meetings_few" = "$1 vstreči";
+"meetings_many" = "$1 vstreč";
+"meetings_other" = "$1 vstreč";
+"open_new_group" = "Otkrytí novuju gruppu";
+"open_group_desc" = "Ne možete najti nužnuju gruppu? Otkrojte svoju...";
+"search_group" = "Poisk gruppy";
+"search_by_groups" = "Poisk po gruppam";
+"search_group_desc" = "Zdesí Vy možete prosmotretí susčestvujusčie gruppy i vybratí gruppu sebe po vkusu...";
+"group_banned" = "K sožaleniju, nam prišlosí zablokirovatí soobsčestvo $1.";
+
+/* Albums */
+
+"create" = "Sozdatí";
+"album" = "Alíbom";
+"albums" = "Alíbomy";
+"photos" = "fotografij";
+"photo" = "Fotografija";
+"create_album" = "Sozdatí alíbom";
+"edit_album" = "Redaktirovatí alíbom";
+"edit_photo" = "Izmenití fotografiju";
+"creating_album" = "Sozdanie alíboma";
+"delete_photo" = "Udalití fotografiju";
+"sure_deleting_photo" = "Vy uvereny, čto hotite udalití etu fotografiju?";
+"upload_photo" = "Zagruzití fotografiju";
+"photo" = "Fotografija";
+"upload_button" = "Zagruzití";
+"open_original" = "Otkrytí original";
+"avatar_album" = "Fotografii so stranicy";
+"wall_album" = "Fotografii so steny";
+"albums_zero" = "Ni odnogo alíboma";
+"albums_one" = "Odin alíbom";
+"albums_few" = "$1 alíboma";
+"albums_many" = "$1 alíbomov";
+"albums_other" = "$1 alíbomov";
+"albums_list_zero" = "U Vas net ni odnogo alíboma";
+"albums_list_one" = "U Vas odin alíbom";
+"albums_list_few" = "U Vas $1 alíboma";
+"albums_list_many" = "U Vas $1 alíbomov";
+"albums_list_other" = "U Vas $1 alíbomov";
+
+"add_image" = "Postavití izobraženie";
+"add_image_group" = "Zagruzití fotografiju";
+"upload_new_picture" = "Zagruzití novuju fotografiju";
+"uploading_new_image" = "Zagruzka novoj fotografii";
+"friends_avatar" = "Druzíjam budet prosče uznatí Vas, esli vy zagruzite svoju nastojasčuju fotografiju.";
+"groups_avatar" = "Horošee foto sdelaet Vaše soobsčestvo bolee uznavaemym.";
+"formats_avatar" = "Vy možete zagruzití izobraženie v formate JPG, GIF ili PNG.";
+"troubles_avatar" = "Esli voznikajut problemy s zagruzkoj, poprobujte vybratí fotografiju meníšego razmera.";
+"webcam_avatar" = "Esli vaš kompíjuter osnasčjon veb-kameroj, Vy možete sdelatí momentalínuju fotografiju »";
+
+"update_avatar_notification" = "Fotografija profilja obnovlena";
+"update_avatar_description" = "Nažmite sjuda, čtoby perejti k prosmotru";
+
+"deleting_avatar" = "Udalenie fotografii";
+"deleting_avatar_sure" = "Vy dejstvitelíno hotite udalití avatar?";
+
+"deleted_avatar_notification" = "Fotografija uspešno udalena";
+
+"save_changes" = "Sohranití izmenenija";
+
+"upd_m" = "obnovil fotografiju na svoej stranice";
+"upd_f" = "obnovila fotografiju na svoej stranice";
+"upd_g" = "obnovilo fotografiju gruppy";
+
+"add_photos" = "Dobavití fotografii";
+"upload_picts" = "Zagruzití fotografii";
+"end_uploading" = "Zaveršití zagruzku";
+"photos_successfully_uploaded" = "Fotografii uspešno zagruženy";
+"click_to_go_to_album" = "Nažmite, čtoby perejti k alíbomu.";
+"error_uploading_photo" = "Ne udalosí zagruzití fotografiju";
+"too_many_pictures" = "Ne bolíše 10 fotografij";
+
+"drag_files_here" = "Peretasčite fajly sjuda";
+"only_images_accepted" = "Fajl \"$1\" ne javljaetsja izobraženiem";
+"max_filesize" = "Maksimalínyj razmer fajla — $1 megabajt";
+
+"uploading_photos_from_computer" = "Zagruzka fotografij s Vašego kompíjutera";
+"supported_formats" = "Podderživaemye formaty fajlov: JPG, PNG i GIF.";
+"max_load_photos" = "Vy možete zagružatí do 10 fotografij za odin raz.";
+"tip" = "Podskazka";
+"tip_ctrl" = "dlja togo, čtoby vybratí srazu neskolíko fotografij, uderživajte klavišu Ctrl pri vybore fajlov v OS Windows ili klavišu CMD v Mac OS.";
+"album_poster" = "Obložka alíboma";
+"select_photo" = "Vyberite fotografiju";
+"upload_new_photo" = "Zagruzití novuju fotografiju";
+
+"is_x_photos_zero" = "Vsego nolí fotografij.";
+"is_x_photos_one" = "Vsego odna fotografija.";
+"is_x_photos_few" = "Vsego $1 fotografii.";
+"is_x_photos_many" = "Vsego $1 fotografij.";
+"is_x_photos_other" = "Vsego $1 fotografij.";
+
+"all_photos" = "Vse fotografii";
+"error_uploading_photo" = "Ne udalosí zagruzití fotografiju. Tekst ošibki: ";
+"too_many_photos" = "Sliškom mnogo fotografij.";
+
+"photo_x_from_y" = "Fotografija $1 iz $2";
+
+/* Notes */
+
+"notes" = "Zametki";
+"note" = "Zametka";
+"name_note" = "Nazvanie";
+"text_note" = "Soderžanie";
+"create_note" = "Dobavití zapisí";
+"edit_note" = "Redaktirovatí zametku";
+"actions" = "Dejstvija";
+"notes_start_screen" = "S pomosčíju zametok Vy možete delitísja sobytijami iz žizni s druzíjami, a tak že bytí v kurse togo, čto proishodit u nih.";
+"note_preview" = "Predprosmotr";
+"note_preview_warn" = "Eto vsego liší predprosmotr";
+"note_preview_warn_details" = "Posle sohranenija zametki mogut vygljadetí inače. K tomu že, ne vyzyvajte predprosmotr sliškom často.";
+"note_preview_empty_err" = "Začem vam predprosmotr dlja zametki bez imeni ili soderžanija?";
+"edited" = "Otredaktirovano";
+"notes_zero" = "Ni odnoj zametki";
+"notes_one" = "Odna zametka";
+"notes_few" = "$1 zametki";
+"notes_many" = "$1 zametok";
+"notes_other" = "$1 zametok";
+"notes_list_zero" = "Ne najdeno ni odnoj zametki";
+"notes_list_one" = "Najdena odna zametka";
+"notes_list_few" = "Najdeno $1 zametki";
+"notes_list_many" = "Najdeno $1 zametok";
+"notes_list_other" = "Najdeno $1 zametok";
+
+"select_note" = "Vybor zametki";
+"no_notes" = "U vas net ni odnoj zametki";
+
+"error_attaching_note" = "Ne udalosí prikrepití zametku";
+
+"select_or_create_new" = "Vyberite susčestvujusčuju zametku ili sozdajte novuju";
+
+"notes_closed" = "Vy ne možete prikrepití zametku k zapisi, tak kak vaši zametki vidny tolíko vam.
";
+
+"tour_section_12_title_1" = "Fon profilja i gruppy";
+"tour_section_12_text_1" = "Vy možete ustanovití dva izobraženija v kačestve fona vašej stranicy";
+"tour_section_12_text_2" = "Oni budut otobražatísja po bokam u teh, kto zajdjot na vašu stranicu";
+"tour_section_12_text_3" = "Sovet: pered ustanovkoj fona, poeksperimentirujte s razmetkoj: poprobujte otzerkalití budusčuju fonovuju kartinku, ili voobsče prosto sozdajte krasivyj gradient";
+"tour_section_12_title_2" = "Avatary";
+"tour_section_12_text_2_1" = "Vy možete zadatí variant pokaza avatara polízovatelja: standartnoe, zakrugljonnye i kvadratnye (1:1)";
+"tour_section_12_text_2_2" = "Dannye nastrojki budut vidny tolíko vam";
+"tour_section_12_title_3" = "Redaktirovanie levogo menju";
+"tour_section_12_text_3_1" = "Pri neobhodimosti vy možete skrytí nenužnye razdely sajta";
+"tour_section_12_text_3_2" = "Napominanie: Razdely pervoj neobhodimosti (Moja Stranica; Moi Druzíja; Moi Otvety; Moi Nastrojki) skrytí nelízja";
+"tour_section_12_title_4" = "Vid postov";
+"tour_section_12_text_4_1" = "Esli nadoel staryj dizajn steny, kotoryj byl v nekogda populjarnom originalínom VKontakte.ru, to vy vsegda možete izmenití vid postov na Mikroblog";
+"tour_section_12_text_4_2" = "Vid postov možno menjatí meždu dvumja variantami v ljuboe vremja";
+"tour_section_12_text_4_3" = "Obratite vnimanie, čto esli vybran staryj vid otobraženija postov, to poslednie kommentarii podgružatísja ne budut";
+"tour_section_12_bottom_text_1" = "Stranica ustanovki fona";
+"tour_section_12_bottom_text_2" = "Primery stranic s ustanovlennym fonom";
+"tour_section_12_bottom_text_3" = "S pomosčíju etoj vozmožnosti vy možete dobavití svoemu profilju bolíše individualínosti";
+"tour_section_12_bottom_text_4" = "Staryj vid postov";
+"tour_section_12_bottom_text_5" = "Mikroblog";
+
+
+"tour_section_13_title_1" = "Vaučery";
+"tour_section_13_text_1" = "Vaučer v OpenVK eto čto-to vrode promokoda na dobavlenie kakoj-libo valjuty (procenty rejtinga, golosov i tak dalee)";
+"tour_section_13_text_2" = "Podobnye kupony sozdajutsja po kakim-libo značimym sobytijam i prazdnikam. Sledite za Telegram-kanalom OpenVK";
+"tour_section_13_text_3" = "Posle aktivacii kakogo-libo vaučera, zadannaja administratorami valjuta budet perečislena v vašu polízu";
+"tour_section_13_text_4" = "Pomnite: Vse vaučery imejut ograničennyj srok aktivacii";
+"tour_section_13_bottom_text_1" = "Vaučery sostojat iz 24 cifr i bukv";
+"tour_section_13_bottom_text_2" = "Uspešnaja aktivacija (naprimer, nam začislili 100 golosov)";
+"tour_section_13_bottom_text_3" = "Vnimanie: Posle aktivacii vaučera na vašu stranicu, tot že samyj vaučer nelízja budet aktivirovatí povtorno";
+
+"tour_section_14_title_1" = "Mobilínaja versija";
+"tour_section_14_text_1" = "Na dannyj moment mobilínoj veb-versii sajta poka net, no zato estí mobilínoe priloženie dlja Android";
+"tour_section_14_text_2" = "OpenVK Legacy - eto mobilínoe priloženie OpenVK dlja retro-ustrojstv na Android s dizajnom VKontakte 3.0.4 iz 2013 goda";
+"tour_section_14_text_3" = "Minimalíno podderživaemoj versiej javljaetsja Android 2.1 Eclair, to estí apparaty vremjon načala 2010-yh vpolne prigodjatsja";
+
+"tour_section_14_title_2" = "Gde eto možno skačatí?";
+"tour_section_14_text_2_1" = "Reliznye versii skačivajutsja čerez oficialínyj repozitorij F-Droid";
+"tour_section_14_text_2_2" = "Esli vy javljaetesí beta-testirovsčikom priloženija, to novye versii priloženija vykladyvajutsja v otdelínyj kanal obnovlenija";
+"tour_section_14_text_2_3" = "Važno: Priloženie možet imetí različnye bagi i nedočjoty, ob ošibkah soobsčajte v oficialínuju gruppu priloženija";
+
+"tour_section_14_bottom_text_1" = "Skrinšoty priloženija";
+"tour_section_14_bottom_text_2" = "Na etom ekskursija po sajtu zaveršena. Esli vy hotite poprobovatí naše mobilínoe priloženie, sozdatí zdesí svoju gruppu, pozvatí svoih druzej ili najti novyh, ili voobsče prosto kak-nibudí porazvlekatísja, to eto možno sdelatí prjamo sejčas, projdja nebolíšuju registraciju";
+"tour_section_14_bottom_text_3" = "Na etom ekskursija po sajtu zaveršena.";
+
+/* Search */
+
+"s_people" = "Ljudi";
+"s_groups" = "Gruppy";
+"s_events" = "Sobytija";
+"s_apps" = "Priloženija";
+"s_questions" = "Voprosy";
+"s_notes" = "Zametki";
+"s_themes" = "Temy";
+"s_posts" = "Zapisi";
+"s_comments" = "Kommentarii";
+"s_videos" = "Video";
+"s_audios" = "Audio";
+"s_by_people" = "po ljudjam";
+"s_by_groups" = "po gruppam";
+"s_by_posts" = "po zapisjam";
+"s_by_comments" = "po kommentarijam";
+"s_by_videos" = "po video";
+"s_by_apps" = "po priloženijam";
+"s_by_audios" = "po muzyke";
+
+"s_order_by" = "Porjadok";
+
+"s_order_by_id" = "Po id";
+"s_order_by_name" = "Po imeni";
+"s_order_by_random" = "Po slučajnosti";
+"s_order_by_rating" = "Po rejtingu";
+"s_order_invert" = "Invertirovatí";
+
+"s_by_date" = "Po date";
+"s_registered_before" = "Zaregistrirovan do";
+"s_registered_after" = "Zaregistrirovan posle";
+"s_date_before" = "Do";
+"s_date_after" = "Posle";
+
+"s_main" = "Osnovnoe";
+
+"s_now_on_site" = "cejčas na sajte";
+"s_with_photo" = "s foto";
+"s_only_in_names" = "tolíko v imenah";
+
+"s_any" = "ljuboj";
+"reset" = "Sbros";
+
+"closed_group_post" = "Eta zapisí iz zakrytoj gruppy";
+"deleted_target_comment" = "Etot kommentarij prinadležit k udaljonnoj zapisi";
+
+"no_results" = "Rezulítatov net";
+
+/* BadBrowser */
+
+"deprecated_browser" = "Ustarevšij brauzer";
+"deprecated_browser_description" = "Dlja prosmotra etogo kontenta vam ponadobitsja Firefox ESR 52+ ili ekvivalentnyj po funkcionalu navigator po vsemirnoj seti internet. Sožaleem ob etom.";
+
+/* Statistics */
+
+"coverage" = "Ohvat";
+"coverage_this_week" = "Etot grafik otobražaet ohvat za poslednie 7 dnej.";
+"views" = "Prosmotry";
+"views_this_week" = "Etot grafik otobražaet prosmotry postov soobsčestva za poslednie 7 dnej.";
+
+"full_coverage" = "Polnyj ohvat";
+"all_views" = "Vse prosmotry";
+
+"subs_coverage" = "Ohvat podpisčikov";
+"subs_views" = "Prosmotry podpisčikov";
+
+"viral_coverage" = "Viralínyj ohvat";
+"viral_views" = "Viralínye prosmotry";
+
+/* Sudo */
+
+"you_entered_as" = "Vy vošli kak";
+"please_rights" = "požalujsta, uvažajte pravo na tajnu perepiski drugih ljudej i ne zloupotrebljajte podmenoj polízovatelja.";
+"click_on" = "Nažmite";
+"there" = "zdesí";
+"to_leave" = "čtoby vyjti";
+
+/* Phone number */
+
+"verify_phone_number" = "Podtverdití nomer telefona";
+"we_sended_first" = "My otpravili SMS s kodom na nomer";
+"we_sended_end" = "vvedite ego sjuda";
+
+/* Mobile */
+"mobile_friends" = "Druzíja";
+"mobile_photos" = "Fotografii";
+"mobile_videos" = "Videozapisi";
+"mobile_messages" = "Soobsčenija";
+"mobile_notes" = "Zametki";
+"mobile_groups" = "Gruppy";
+"mobile_search" = "Poisk";
+"mobile_settings" = "Nastrojki";
+"mobile_desktop_version" = "Polnaja versija";
+"mobile_log_out" = "Vyjti";
+"mobile_menu" = "Menju";
+"mobile_like" = "Nravitsja";
+"mobile_user_info_hide" = "Skrytí";
+"mobile_user_info_show_details" = "Pokazatí podrobnee";
+
+/* Moderation */
+
+"section" = "Razdel";
+"template_ban" = "Ban po šablonu";
+"active_templates" = "Dejstvujusčie šablony";
+"users_reports" = "Žaloby polízovatelej";
+"substring" = "Podstroka";
+"n_user" = "Polízovatelí";
+"time_before" = "Vremja raníše, čem";
+"time_after" = "Vremja pozže, čem";
+"where_for_search" = "WHERE dlja poiska po razdelu";
+"block_params" = "Parametry blokirovki";
+"only_rollback" = "Tolíko otkat";
+"only_block" = "Tolíko blokirovka";
+"rollback_and_block" = "Otkat i blokirovka";
+"subm" = "Primenití";
+
+"select_section_for_start" = "Vyberite razdel dlja načala raboty";
+"results_will_be_there" = "Zdesí budut otobražatísja rezulítaty poiska";
+"search_results" = "Rezulítaty poiska";
+"cnt" = "št";
+
+"link_to_page" = "Ssylka na stranicu";
+"or_subnet" = "ili podsetí";
+"error_when_searching" = "Ošibka pri vypolnenii zaprosa";
+"no_found" = "Ničego ne najdeno";
+"operation_successfully" = "Operacija zaveršena uspešno";
+
+"unknown_error" = "Neizvestnaja ošibka";
+"templates" = "Šablony";
+"type" = "Tip";
+"count" = "Količestvo";
+"time" = "Vremja";
+
+"roll_back" = "otkatití";
+"roll_backed" = "otkačeno";
diff --git a/locales/uk.strings b/locales/uk.strings
index 7ccfb367..df4cb766 100644
--- a/locales/uk.strings
+++ b/locales/uk.strings
@@ -134,6 +134,10 @@
"updated_at" = "Оновлено $1";
"user_banned" = "На жаль, нам довелося заблокувати сторінку користувача $1.";
"user_banned_comment" = "Коментар модератора:";
+"verified_page" = "Верифікована сторінка";
+"user_is_blocked" = "Користувача заблоковано";
+"before" = "до";
+"forever" = "назавжди";
/* Wall */
@@ -184,6 +188,7 @@
"nsfw_warning" = "Даний запис може містити контент 18+";
"report" = "Поскаржитися";
"attach" = "Прикріпити";
+"detach" = "Відкріпити";
"attach_photo" = "Прикріпити фото";
"attach_photo" = "Прикріпити відео";
"draw_graffiti" = "Намалювати графіті";
@@ -193,6 +198,8 @@
"version_incompatibility" = "Не вдалося показати це вкладення. Можливо, база даних несумісна з поточною версією OpenVK.";
"graffiti" = "Графіті";
"reply" = "Відповісти";
+"post_is_ad" = "Цей пост було проплачено Держдепом США";
+"edited_short" = "ред.";
/* Friends */
@@ -317,14 +324,21 @@
"search_group" = "Пошук групи";
"search_by_groups" = "Пошук за групами";
"search_group_desc" = "Тут Ви можете переглянути існуючи групи та обрати групу до вподоби.";
+"group_banned" = "Спільнота $1 заблокована.";
/* Albums */
"create" = "Створити";
+"album" = "Альбом";
"albums" = "Альбоми";
+"photos" = "фотографій";
+"photo" = "Фотографія";
"create_album" = "Створити альбом";
"edit_album" = "Редагувати альбом";
+"edit_photo" = "Змінити фотографію";
"creating_album" = "Створення альбому";
+"delete_photo" = "Видалити фотографію";
+"sure_deleting_photo" = "Ви впевнені, що бажаєте видалити цю світлину?";
"upload_photo" = "Завантажити фотографію";
"photo" = "Фотографія";
"upload_button" = "Завантажити";
@@ -366,6 +380,39 @@
"upd_f" = "оновила фотографію на своїй сторінці";
"upd_g" = "оновило фотографію групи";
+"add_photos" = "Додати фотографії";
+"upload_picts" = "Завантажити фотографії";
+"end_uploading" = "Завершити завантаження";
+"photos_successfully_uploaded" = "Фотографії було завантажено";
+"click_to_go_to_album" = "Натисніть, щоб перейти до альбому.";
+"error_uploading_photo" = "Помилка завантаження фотографії";
+"too_many_pictures" = "Не більше 10 фотографій";
+
+"drag_files_here" = "Перетягніть файли сюди";
+"only_images_accepted" = "Файл \"$1\" не є зображенням";
+"max_filesize" = "Максимальний розмір файлу — $1 мегабайт";
+
+"uploading_photos_from_computer" = "Завантаження фотографій з Вашого ПК";
+"supported_formats" = "Підтримувані формати зображень: JPG, PNG й GIF.";
+"max_load_photos" = "Ви можете завантажити до 10 фотографій за один раз.";
+"tip" = "Порада";
+"tip_ctrl" = "щоб обрати кілька фотографій одразу, утримуйте клавішу Ctrl під час вибору файлів в OS Windows або клавішу CMD у Mac OS.";
+"album_poster" = "Обкладинка альбому";
+"select_photo" = "Оберіть фотографію";
+"upload_new_photo" = "Завантажте нову світлину";
+
+"is_x_photos_zero" = "Усього 0 фотографій.";
+"is_x_photos_one" = "Всього 1 фотографія.";
+"is_x_photos_few" = "Всього $1 фотографій.";
+"is_x_photos_many" = "Всього $1 фотографій.";
+"is_x_photos_other" = "Всього $1 фотографій.";
+
+"all_photos" = "Всі фотографії";
+"error_uploading_photo" = "Помилка завантаження фотографії. Текст помилки: ";
+"too_many_photos" = "Надто багато фотографій.";
+
+"photo_x_from_y" = "Фотографія $1 з $2";
+
/* Notes */
"notes" = "Нотатки";
@@ -395,6 +442,18 @@
/* Notes: Article Viewer */
"aw_legacy_ui" = "Класичне дієвидло";
+"select_note" = "Вибір нотатки";
+"no_notes" = "Ви не маєте жодної нотатки";
+
+"error_attaching_note" = "Не вдалося закріпити нотатку";
+
+"select_or_create_new" = "Оберіть існуючу нотатку або створіть нову";
+
+"notes_closed" = "Ви не можете прикріпити нотатку до запису, оскільки ваші нотатки видно тільки вам.
+ +
+ ${tr("fully_delete_audio")} +
${tr("report_question_text")} +
${tr("report_reason")}: `, [tr("confirm_m"), tr("cancel")], [(function() { + + res = document.querySelector("#uReportMsgInput").value; + xhr = new XMLHttpRequest(); + xhr.open("GET", "/report/" + e.target.dataset.id + "?reason=" + res + "&type=audio", true); + xhr.onload = (function() { + if(xhr.responseText.indexOf("reason") === -1) + MessageBox(tr("error"), tr("error_sending_report"), ["OK"], [Function.noop]); + else + MessageBox(tr("action_successfully"), tr("will_be_watched"), ["OK"], [Function.noop]); + }); + xhr.send(null) + }), + + Function.noop]) +}) + +$(document).on("click", ".audiosContainer .paginator a", (e) => { + e.preventDefault() + let url = new URL(e.currentTarget.href) + let page = url.searchParams.get("p") + + function searchNode(id) { + let node = document.querySelector(`.audioEmbed[data-realid='${id}'] .audioEntry`) + + if(node != null) { + node.classList.add("nowPlaying") + } + } + + if(window.savedAudiosPages[page] != null) { + history.pushState({}, "", e.currentTarget.href) + document.querySelector(".audiosContainer").innerHTML = window.savedAudiosPages[page].innerHTML + searchNode(window.player["tracks"].currentTrack != null ? window.player["tracks"].currentTrack.id : 0) + + return + } + + e.currentTarget.parentNode.classList.add("lagged") + $.ajax({ + type: "GET", + url: e.currentTarget.href, + success: (response) => { + let domparser = new DOMParser() + let result = domparser.parseFromString(response, "text/html") + + document.querySelector(".audiosContainer").innerHTML = result.querySelector(".audiosContainer").innerHTML + history.pushState({}, "", e.currentTarget.href) + window.savedAudiosPages[page] = result.querySelector(".audiosContainer") + searchNode(window.player["tracks"].currentTrack != null ? window.player["tracks"].currentTrack.id : 0) + + if(!window.player.context["playedPages"].includes(page)) { + $.ajax({ + type: "POST", + url: "/audios/context", + data: { + context: window.player["context"].context_type, + context_entity: window.player["context"].context_id, + hash: u("meta[name=csrf]").attr("value"), + page: page + }, + success: (response_2) => { + window.player.tracks["tracks"] = window.player.tracks["tracks"].concat(response_2["items"]) + window.player.context["playedPages"].push(String(page)) + console.info("Page is switched") + } + }) + } + } + }) +}) + +$(document).on("click", ".addToPlaylist", (e) => { + let audios = document.querySelector("input[name='audios']") + let id = e.currentTarget.dataset.id + + if(!audios.value.includes(id + ",")) { + document.querySelector("input[name='audios']").value += (id + ",") + e.currentTarget.querySelector("span").innerHTML = tr("remove_from_playlist") + } else { + document.querySelector("input[name='audios']").value = document.querySelector("input[name='audios']").value.replace(id + ",", "") + e.currentTarget.querySelector("span").innerHTML = tr("add_to_playlist") + } +}) + +$(document).on("click", "#bookmarkPlaylist, #unbookmarkPlaylist", (e) => { + let target = e.currentTarget + let id = target.id + + $.ajax({ + type: "POST", + url: `/playlist${e.currentTarget.dataset.id}/action?act=${id == "unbookmarkPlaylist" ? "unbookmark" : "bookmark"}`, + data: { + hash: u("meta[name=csrf]").attr("value"), + }, + beforeSend: () => { + e.currentTarget.classList.add("lagged") + }, + success: (response) => { + if(response.success) { + e.currentTarget.setAttribute("id", id == "unbookmarkPlaylist" ? "bookmarkPlaylist" : "unbookmarkPlaylist") + e.currentTarget.innerHTML = id == "unbookmarkPlaylist" ? tr("bookmark") : tr("unbookmark") + e.currentTarget.classList.remove("lagged") + } else + fastError(response.flash.message) + } + }) +}) diff --git a/Web/static/js/al_playlists.js b/Web/static/js/al_playlists.js new file mode 100644 index 00000000..cf61c45d --- /dev/null +++ b/Web/static/js/al_playlists.js @@ -0,0 +1,113 @@ +let context_type = "entity_audios" +let context_id = 0 + +if(document.querySelector("#editPlaylistForm")) { + context_type = "playlist_context" + context_id = document.querySelector("#editPlaylistForm").dataset.id +} + +if(document.querySelector(".showMoreAudiosPlaylist") && document.querySelector(".showMoreAudiosPlaylist").dataset.club != null) { + context_type = "entity_audios" + context_id = Number(document.querySelector(".showMoreAudiosPlaylist").dataset.club) * -1 +} + +let searcher = new playersSearcher(context_type, context_id) + +searcher.successCallback = (response, thisc) => { + let domparser = new DOMParser() + let result = domparser.parseFromString(response, "text/html") + let pagesCount = Number(result.querySelector("input[name='pagesCount']").value) + let count = Number(result.querySelector("input[name='count']").value) + + result.querySelectorAll(".audioEmbed").forEach(el => { + let id = Number(el.dataset.realid) + let isAttached = (document.querySelector("input[name='audios']").value.includes(`${id},`)) + + document.querySelector(".playlistAudiosContainer").insertAdjacentHTML("beforeend", ` +
If you would like to start using the site again, you can restore your account until $1."; "profile_deactivated_status" = "Account deleted"; -"profile_deactivated_info" = "The account has been deleted.
The information is not available."; +"profile_deactivated_info" = "This account has been deleted.
No further information available."; "share_with_friends" = "Share with friends"; "end_all_sessions" = "End all sessions"; -"end_all_sessions_description" = "If you wanna logout from $1 on all devices, click on button below"; +"end_all_sessions_description" = "If you wish to log out of $1 on all devices, click the button below."; -"end_all_sessions_done" = "All sessions was ended, including mobile apps"; +"end_all_sessions_done" = "All sessions have been terminated, including mobile apps."; "backdrop_short" = "Backdrop"; -"backdrop" = "Page backdrop"; -"backdrop_desc" = "You can set two pictures as your profile or group backdrop. They will be displayed on left and right edges of page. With this feature, you can add more personality to your profile."; -"backdrop_warn" = "The images will be arranged as in the layout above. Their height will be automatically increased so that they occupy 100% of the screen height, there will be a blur in the middle. It is not possible to replace the background of the main OpenVK interface or add audio."; -"backdrop_about_adding" = "You can upload only one picture, although depending on design, the ending result may look ugly. You can also change only one pic: if you already have two images set up and you want to change one - upload only one, the other won't be removed. To remove both images press the button below, you can't remove pictures individually."; -"backdrop_save" = "Save backdrop picture(s)"; -"backdrop_remove" = "Remove all backdrop pictures"; -"backdrop_error_title" = "Error saving backdrop settings"; -"backdrop_error_no_media" = "Images are corrupted or haven't been uploaded in their entirety"; -"backdrop_succ" = "Backdrop settings saved"; -"backdrop_succ_rem" = "Backdrop images have been removed"; +"backdrop" = "Page Backdrop"; +"backdrop_desc" = "You can set two pictures as the backdrop for your profile or group. They will be displayed on the left and right edges of the page, adding personality to your profile."; +"backdrop_warn" = "The images will be arranged as shown in the layout above. Their height will be automatically increased to occupy 100% of the screen height, with a blur in the middle. You cannot replace the background of the main OpenVK interface or add audio."; +"backdrop_about_adding" = "You can upload only one picture, although depending on the design, the end result may look imperfect. You can also change only one picture: if you already have two images set up and you want to change one, upload only one, and the other won't be removed. To remove both images, press the button below; you can't remove pictures individually."; +"backdrop_save" = "Save Backdrop Picture(s)"; +"backdrop_remove" = "Remove All Backdrop Pictures"; +"backdrop_error_title" = "Error Saving Backdrop Settings"; +"backdrop_error_no_media" = "Images are corrupted or haven't been uploaded in their entirety."; +"backdrop_succ" = "Backdrop settings saved successfully"; +"backdrop_succ_rem" = "Backdrop images have been successfully removed"; "backdrop_succ_desc" = "Users will start seeing changes in 5 minutes."; "browse" = "Browse"; /* Two-factor authentication */ -"two_factor_authentication" = "Two-factor authentication"; -"two_factor_authentication_disabled" = "Provides reliable protection against hacking: to enter the page, you must enter the code obtained in the 2FA application."; -"two_factor_authentication_enabled" = "Two-factor authentication is enabled. Your page is protected."; -"two_factor_authentication_login" = "You have two-factor authentication enabled. To login, enter the code received in the application."; +"two_factor_authentication" = "Two-Factor Authentication"; +"two_factor_authentication_disabled" = "Enhances security by requiring a code from the 2FA app to access your account, providing reliable protection against hacking."; +"two_factor_authentication_enabled" = "Two-Factor Authentication is active. Your account is securely protected."; +"two_factor_authentication_login" = "Two-Factor Authentication is enabled. Enter the code received in the application to log in."; -"two_factor_authentication_settings_1" = "Two-factor authentication via TOTP can be used even without internet. To do this, you need a code generation app. For example, Google Authenticator for Android and iOS or FOSS Aegis and andOTP for Android. Make sure the date and time is set correctly on your phone."; +"two_factor_authentication_settings_1" = "Two-Factor Authentication via TOTP can be used even without an internet connection. You'll need a code generation app such as Google Authenticator for Android and iOS or FOSS Aegis and andOTP for Android. Ensure your phone's date and time are set correctly."; "two_factor_authentication_settings_2" = "Using the app for two-factor authentication, scan the QR code below:"; -"two_factor_authentication_settings_3" = "or manually enter the given secret key: $1."; -"two_factor_authentication_settings_4" = "Now enter the code that the application gave you and the password for your page so that we can confirm that you really are."; +"two_factor_authentication_settings_3" = "Alternatively, manually enter the provided secret key: $1."; +"two_factor_authentication_settings_4" = "Now, enter the code generated by the app and your account password to confirm your identity."; "connect" = "Connect"; "enable" = "Enable"; @@ -667,20 +669,20 @@ "code" = "Code"; "2fa_code" = "2FA code"; -"incorrect_password" = "Incorrect password"; -"incorrect_code" = "Incorrect code"; -"incorrect_2fa_code" = "Incorrect two-factor authentication code"; -"two_factor_authentication_enabled_message" = "Two-factor authentication enabled"; -"two_factor_authentication_enabled_message_description" = "Your page has become more difficult to hack. We recommend that you download backup codes"; -"two_factor_authentication_disabled_message" = "Two-factor authentication disabled"; +"incorrect_password" = "Password is incorrect"; +"incorrect_code" = "Code is incorrect"; +"incorrect_2fa_code" = "Two-factor authentication code is incorrect"; +"two_factor_authentication_enabled_message" = "Two-factor authentication is now enabled"; +"two_factor_authentication_enabled_message_description" = "Your account is now more secure. We recommend downloading backup codes."; +"two_factor_authentication_disabled_message" = "Two-factor authentication has been disabled"; -"view_backup_codes" = "View backup codes"; -"backup_codes" = "Backup codes for login confirmation"; -"two_factor_authentication_backup_codes_1" = "Backup codes allow you to validate your login when you don't have access to your phone, for example, while traveling."; -"two_factor_authentication_backup_codes_2" = "You have 10 more codes, each code can only be used once. Print them out, put them away in a safe place and use them when you need codes to validate your login."; -"two_factor_authentication_backup_codes_3" = "You can get new codes if they run out. Only the last created backup codes are valid."; +"view_backup_codes" = "See backup codes"; +"backup_codes" = "Backup codes for login verification"; +"two_factor_authentication_backup_codes_1" = "Backup codes enable you to confirm your login when you can't access your phone, such as when traveling."; +"two_factor_authentication_backup_codes_2" = "You have 10 more codes, and each code can only be used once. Print them, store them securely, and use when needed for login verification."; +"two_factor_authentication_backup_codes_3" = "You can request new codes if needed. Only the latest set of backup codes generated is valid."; "viewing_backup_codes" = "View backup codes"; -"disable_2fa" = "Disable 2FA"; +"disable_2fa" = "Turn off 2FA"; "viewing" = "View"; /* Sorting */ @@ -713,12 +715,133 @@ "upload_new_video" = "Upload new video"; "max_attached_videos" = "Max is 10 videos"; "max_attached_photos" = "Max is 10 photos"; +"max_attached_audios" = "Max is 10 audios"; "no_videos" = "You don't have uploaded videos."; "no_videos_results" = "No results."; "change_video" = "Change video"; "unknown_video" = "This video is not supported in your version of OpenVK."; + +/* Audios */ + +"audios" = "Audios"; +"audio" = "Audio"; +"playlist" = "Playlist"; +"upload_audio" = "Upload audio"; +"upload_audio_to_group" = "Upload audio to group"; + +"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 and contain audio stream."; +"audio_requirements_2" = "Audio must not infringe copyright and related rights"; +"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"; +"music_user" = "User's music"; +"music_club" = "Club's 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"; +"searchable" = "Searchable"; + +"select_audio" = "Select audios"; +"no_playlists_thisuser" = "You haven't added any playlists yet."; +"no_playlists_user" = "This user has not added any playlists yet."; +"no_playlists_club" = "This group hasn't added playlists yet."; + +"no_audios_thisuser" = "You haven't added any audios yet."; +"no_audios_user" = "This user has not added any audios yet."; +"no_audios_club" = "This group has not added any audios yet."; + +"new_playlist" = "New playlist"; +"created_playlist" = "created"; +"updated_playlist" = "updated"; +"bookmark" = "Add to collection"; +"unbookmark" = "Remove from collection"; +"empty_playlist" = "There are no audios in this playlist."; +"edit_playlist" = "Edit playlist"; +"unable_to_load_queue" = "Error when loading queue."; + +"fully_delete_audio" = "Fully delete audio"; +"attach_audio" = "Attach audio"; +"detach_audio" = "Detach audio"; + +"show_more_audios" = "Show more audios"; +"add_to_playlist" = "Add to playlist"; +"remove_from_playlist" = "Remove from playlist"; +"delete_playlist" = "Delete playlist"; +"playlist_cover" = "Playlist cover"; + +"playlists_user" = "Users playlists"; +"playlists_club" = "Groups playlists"; +"change_cover" = "Change cover"; +"playlist_cover" = "Playlist's cover"; + +"minutes_count_zero" = "lasts no minutes"; +"minutes_count_one" = "lasts one minute"; +"minutes_count_few" = "lasts $1 minutes"; +"minutes_count_many" = "lasts $1 minutes"; +"minutes_count_other" = "lasts $1 minutes"; + +"listens_count_zero" = "no listens"; +"listens_count_one" = "one listen"; +"listens_count_few" = "$1 listens"; +"listens_count_many" = "$1 listens"; +"listens_count_other" = "$1 listens"; + +"add_audio_to_club" = "Add audio to group"; +"what_club_add" = "Which group do you want to add the song to?"; +"group_has_audio" = "This group already has this song."; +"group_hasnt_audio" = "This group doesn't have this song."; + +"by_name" = "by name"; +"by_performer" = "by performer"; +"no_access_clubs" = "There are no groups where you are an administrator."; +"audio_successfully_uploaded" = "Audio has been successfully uploaded and is currently being processed."; + +"broadcast_audio" = "Broadcast audio to status"; +"sure_delete_playlist" = "Do you sure want to delete this playlist?"; +"edit_audio" = "Edit audio"; +"audios_group" = "Audios from group"; +"playlists_group" = "Playlists from group"; + +"play_tip" = "Play/pause"; +"repeat_tip" = "Repeat"; +"shuffle_tip" = "Shuffle"; +"mute_tip" = "Mute"; + /* Notifications */ "feedback" = "Feedback"; @@ -727,13 +850,13 @@ "notifications_like" = "$1 liked your $2post$3 from $4"; "notifications_repost" = "$1 shared your $2post$3 from $4"; -"notifications_comment_under" = "$1 leaved a comment on $2"; +"notifications_comment_under" = "$1 left a comment on $2"; "notifications_under_note" = "your $3note$4"; "notifications_under_photo" = "your $3photo$4"; "notifications_under_post" = "your $3post$4 from $5"; "notifications_under_video" = "your $3video$4"; "notifications_post" = "$1 published $2a post$3 on your wall: $4"; -"notifications_appoint" = "$1 appointed you as community manager $2"; +"notifications_appoint" = "$1 appointed you as community manager of $2"; "nt_liked_yours" = "liked your"; "nt_shared_yours" = "shared your"; @@ -775,19 +898,19 @@ "you_still_have_x_points" = "You have $1 unused votes."; "vouchers" = "Vouchers"; -"have_voucher" = "Have voucher"; +"have_voucher" = "Have a voucher"; "voucher_token" = "Voucher token"; "voucher_activators" = "Users"; -"voucher_explanation" = "Enter the voucher serial number. It is usually listed on the receipt or in the message."; -"voucher_explanation_ex" = "Note that vouchers can expire and can only be used once."; -"invalid_voucher" = "Voucher is invalid"; -"voucher_bad" = "You may have entered the wrong serial number, already used voucher, or the voucher has simply expired."; +"voucher_explanation" = "Enter the voucher serial number, usually found on the receipt or in the message."; +"voucher_explanation_ex" = "Note that vouchers can expire and are valid for a single use only."; +"invalid_voucher" = "Invalid voucher"; +"voucher_bad" = "You may have entered the wrong serial number, used a voucher already, or the voucher has expired."; "voucher_good" = "Voucher activated"; -"voucher_redeemed" = "The voucher has been successfully activated. You will receive points, but you will no longer be able to activate it with this code."; +"voucher_redeemed" = "The voucher has been successfully activated. You will receive points, but this code can no longer be used."; "redeem" = "Redeem voucher"; "deactivate" = "Deactivate"; -"usages_total" = "Number of uses"; -"usages_left" = "Uses left"; +"usages_total" = "Total uses"; +"usages_left" = "Uses remaining"; "points_transfer_dialog_header_1" = "You can send as a gift or transfer part of the votes to another person."; "points_transfer_dialog_header_2" = "Your current balance:"; @@ -882,13 +1005,13 @@ "app_news" = "A news note"; "app_state" = "Status"; "app_enabled" = "Enabled"; -"app_creation_hint_url" = "Specify in the URL the exact address together with the scheme (https), the port (80) and the required request parameters."; -"app_creation_hint_iframe" = "Your application will be opened in an iframe."; -"app_balance" = "Your application has $1 votes to its credit."; -"app_users" = "Your application is used by $1 people."; -"app_withdrawal_q" = "withdraw?"; -"app_withdrawal" = "Withdrawal"; -"app_withdrawal_empty" = "Couldn't withdraw emptiness, sorry."; +"app_creation_hint_url" = "Specify the exact address in the URL, including the scheme (https), port (80), and required request parameters."; +"app_creation_hint_iframe" = "Your application will open in an iframe."; +"app_balance" = "Your application has $1 votes credited to it."; +"app_users" = "Your application is being used by $1 people."; +"app_withdrawal_q" = "Do you want to withdraw?"; +"app_withdrawal" = "Withdraw"; +"app_withdrawal_empty" = "Sorry, withdrawal of emptiness is not possible."; "app_withdrawal_created" = "A request to withdraw $1 votes has been created. Awaiting crediting."; "appjs_payment" = "Purchase payment"; @@ -983,6 +1106,7 @@ "going_to_report_photo" = "You are about to report this photo."; "going_to_report_user" = "You are about to report this user."; "going_to_report_video" = "You are about to report this video."; +"going_to_report_audio" = "You are about to report this audio."; "going_to_report_post" = "You are about to report this post."; "going_to_report_comment" = "You are about to report this comment."; @@ -991,14 +1115,11 @@ "author" = "Author"; -"you_have_not_entered_text" = "You have not entered any text"; -"you_have_not_entered_name_or_text" = "You did not enter a name or text"; +"ticket_changed" = "Ticket Updated"; +"ticket_changed_comment" = "Changes will be applied in a few seconds."; -"ticket_changed" = "Ticket changed"; -"ticket_changed_comment" = "The changes will take effect in a few seconds."; - -"banned_in_support_1" = "Sorry, $1, but now you can't create tickets."; -"banned_in_support_2" = "And the reason for this is simple: $1. Unfortunately, this time we had to take away this opportunity from you forever."; +"banned_in_support_1" = "Apologies, $1, but you're currently banned from creating tickets."; +"banned_in_support_2" = "The reason is straightforward: $1. Unfortunately, this privilege has been permanently revoked this time."; "you_can_close_this_ticket_1" = "If you have no more questions, you can "; "you_can_close_this_ticket_2" = "close this ticket"; @@ -1018,22 +1139,22 @@ "banned_title" = "You are banned"; "banned_header" = "You are banned"; "banned_alt" = "The user is blocked."; -"banned_1" = "Sorry $1, but you have been banned."; -"banned_2" = "And the reason for this is simple: $1."; -"banned_perm" = "Unfortunately, this time we had to block you forever."; -"banned_until_time" = "This time we had to block you until $1"; -"banned_3" = "You can still write to the support if you think there was an error or logout."; +"banned_1" = "Apologies, $1, but you have been banned."; +"banned_2" = "The reason for this action is simple: $1."; +"banned_perm" = "Regrettably, this time the ban is permanent."; +"banned_until_time" = "This time, the ban extends until $1"; +"banned_3" = "You can still reach out to support if you believe there was an error or logout."; "banned_unban_myself" = "Unban myself"; "banned_unban_title" = "Your account has been unbanned"; -"banned_unban_description" = "Try not to break the rules anymore."; +"banned_unban_description" = "Kindly refrain from violating the rules in the future."; /* Registration confirm */ -"ec_header" = "Registration confirmation"; -"ec_title" = "Thanks!"; -"ec_1" = "$1, your registration is almost done. In a few minutes you should receive an mail with a link to confirm your email address."; -"ec_2" = "If for some reason you don't get the mail, check your spam folder. If you don't find the email there, you can resend it."; -"ec_resend" = "Resend mail"; +"ec_header" = "Confirm Registration"; +"ec_title" = "Thank You!"; +"ec_1" = "$1, your registration is nearly complete. In a few minutes, you should receive an email with a link to verify your email address."; +"ec_2" = "If, for any reason, you don't receive the email, please check your spam folder. If it's not there, you can request a resend."; +"ec_resend" = "Resend Email"; /* Messages */ @@ -1111,6 +1232,7 @@ "created" = "Created"; "everyone_can_create_topics" = "Everyone can create topics"; +"everyone_can_upload_audios" = "Everyone can upload audios"; "display_list_of_topics_above_wall" = "Display a list of topics above the wall"; "topic_changes_saved_comment" = "The updated title and settings will appear on the topic page."; @@ -1129,7 +1251,7 @@ "information_-2" = "Login success"; "no_data" = "No data"; -"no_data_description" = "There is nothing here... yet..."; +"no_data_description" = "There is nothing here... yet."; "error" = "Error"; "error_shorturl" = "This short address is already owned."; @@ -1137,7 +1259,7 @@ "error_upload_failed" = "Failed to upload a photo"; "error_old_password" = "Old password does not match"; "error_new_password" = "New password does not match"; -"error_weak_password" = "Password isn't strong enough. It should has at least 8 symbols, at least one capital letter and at least one digit."; +"error_weak_password" = "Your password isn't strong enough. It should be at least 8 characters long, include at least one capital letter, and contain at least one digit."; "error_shorturl_incorrect" = "The short address has an incorrect format."; "error_repost_fail" = "Failed to share post"; "error_data_too_big" = "Attribute '$1' must be at most $2 $3 long"; @@ -1157,7 +1279,7 @@ "invalid_email_address" = "Invalid Email address"; "invalid_email_address_comment" = "The Email you entered is not correct."; -"invalid_real_name" = "Please, enter your real name. It'll be easier for your friends to find you like this."; +"invalid_real_name" = "Kindly input your actual name. It will make it simpler for your friends to find you in this way."; "invalid_birth_date" = "Invalid date of birth"; "invalid_birth_date_comment" = "The date of birth you entered is not correct."; @@ -1170,8 +1292,8 @@ "profile_changed" = "Profile changed"; "profile_changed_comment" = "Your active profile has been changed."; -"profile_not_found" = "User is not found."; -"profile_not_found_text" = "This profile has either been deleted or not been created yet."; +"profile_not_found" = "User was not found."; +"profile_not_found_text" = "Either this profile has been deleted, or it hasn't been created yet."; "suspicious_registration_attempt" = "Suspicious registration attempt"; "suspicious_registration_attempt_comment" = "You tried to register from a suspicious location."; @@ -1210,10 +1332,10 @@ "error_comment_file_too_big" = "Media file is corrupted or too big."; "comment_is_added" = "Comment has been added"; -"comment_is_added_desc" = "Your comment will appear on page."; +"comment_is_added_desc" = "Your comment will appear on the page."; "error_access_denied_short" = "Access denied"; -"error_access_denied" = "You don't have rights to edit this resource"; +"error_access_denied" = "You don't have permission to edit this resource"; "success" = "Success"; "comment_will_not_appear" = "This comment will no longer appear."; @@ -1238,7 +1360,7 @@ "new_changes_desc" = "New data will appear in your group."; "comment_is_changed" = "Admin comment changed"; "comment_is_deleted" = "Admin comment deleted"; -"comment_is_too_long" = "Comment is too long ($1 symbols instead 36)"; +"comment_is_too_long" = "Comment is too long ($1 characters instead of 36)"; "x_no_more_admin" = "$1 no longer an administrator."; "x_is_admin" = "$1 appointed as an administrator."; @@ -1257,7 +1379,7 @@ "no_photo" = "No photo"; "select_file" = "Select file"; -"new_description_will_appear" = "The updated description will appear on the photo page.."; +"new_description_will_appear" = "The updated description will appear on the photo page."; "photo_is_deleted" = "Photo was deleted"; "photo_is_deleted_desc" = "This photo has been successfully deleted."; @@ -1266,7 +1388,7 @@ "error_occured" = "Error occured"; "error_video_damaged_file" = "The file is corrupted or does not contain video."; "error_video_incorrect_link" = "Perhaps the link is incorrect."; -"error_video_no_title" = "Video can't be published without title."; +"error_video_no_title" = "Video can't be published without a title."; "new_data_video" = "The updated description will appear on the video page."; "error_deleting_video" = "Failed to delete video"; @@ -1275,9 +1397,9 @@ "error_max_pinned_clubs" = "Maximum count of the pinned groups is 10."; "error_viewing_subs" = "You cannot view the full list of subscriptions $1."; -"error_status_too_long" = "Status is too long ($1 instead 255)"; +"error_status_too_long" = "Status is too long ($1 characters instead of 255)"; "death" = "Death..."; -"nehay" = "Live long!"; +"nehay" = "Let 'em live!"; "user_successfully_banned" = "User was successfully banned."; "content_is_deleted" = "The content has been removed and the user has received a warning."; @@ -1287,6 +1409,22 @@ "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."; + +"set_playlist_name" = "Enter the playlist name."; +"playlist_already_bookmarked" = "This playlist is already in your collection."; +"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 */ "login_as" = "Login as $1"; @@ -1316,7 +1454,7 @@ "admin_verification" = "Verification"; "admin_banreason" = "Ban reason"; "admin_banned" = "banned"; -"admin_gender" = "Gender"; +"admin_gender" = "Sex"; "admin_registrationdate" = "Registration date"; "admin_actions" = "Actions"; "admin_image" = "Image"; @@ -1361,7 +1499,7 @@ "admin_voucher_serial_desc" = "The number consists of 24 characters. If the format is incorrect or the field is not filled in, it will be assigned automatically."; "admin_voucher_coins" = "Number of votes"; "admin_voucher_rating_number" = "Number of rating"; -"admin_voucher_usages_desc" = "The number of accounts that can use the voucher. If you type -1, it will be infinity."; +"admin_voucher_usages_desc" = "The number of users that can use the voucher. If you type -1, it will be infinity."; "admin_voucher_status" = "Status"; "admin_voucher_status_opened" = "active"; "admin_voucher_status_closed" = "closed"; @@ -1392,7 +1530,11 @@ "admin_banned_link_not_found" = "Link not found"; "admin_gift_moved_successfully" = "Gift moved successfully"; -"admin_gift_moved_to_recycle" = "This gift will now be in Recycle Bin."; +"admin_gift_moved_to_recycle" = "This gift was moved to Recycle Bin."; +"admin_original_file" = "Original file"; +"admin_audio_length" = "Length"; +"admin_cover_id" = "Cover (photo ID)"; +"admin_music" = "Music"; "logs" = "Logs"; "logs_anything" = "Anything"; @@ -1404,11 +1546,11 @@ "logs_edited" = "edited"; "logs_removed" = "removed"; "logs_restored" = "restored"; -"logs_id_post" = "ID записи"; -"logs_id_object" = "ID объекта"; -"logs_uuid_user" = "UUID пользователя"; -"logs_change_type" = "Тип изменения"; -"logs_change_object" = "Тип объекта"; +"logs_id_post" = "Post ID"; +"logs_id_object" = "Object ID"; +"logs_uuid_user" = "User UUID"; +"logs_change_type" = "Change Type"; +"logs_change_object" = "Object Type"; "logs_user" = "User"; "logs_object" = "Object"; @@ -1476,26 +1618,26 @@ "confirm_m" = "Confirm"; "action_successfully" = "Success"; -/* User alerts */ +/* User Alerts */ -"user_alert_scam" = "This account has been reported a lot for scam. Please be careful, especially if he asked for money."; -"user_may_not_reply" = "This user may not reply to you because of your privacy settings. Open privacy settings"; +"user_alert_scam" = "This account has received numerous reports for scams. Please exercise caution, especially if they request money."; +"user_may_not_reply" = "This user may be unable to reply to you due to your privacy settings. Open privacy settings"; -/* Cookies pop-up */ +/* Cookies Pop-up */ -"cookies_popup_content" = "All kids love cookie, so this website uses Cookies to identify your session and nothing more. Check our privacy policy for more information."; +"cookies_popup_content" = "Just like how kids love cookies, this website uses Cookies to identify your session and nothing more. Check our privacy policy for more information."; "cookies_popup_agree" = "Accept"; /* Away */ "transition_is_blocked" = "Transition is blocked"; "caution" = "Caution"; -"url_is_banned" = "Link is not allowed"; -"url_is_banned_comment" = "The $1 administration recommends not to follow this link."; -"url_is_banned_comment_r" = "The $1 administration recommends not to follow this link.
The reason is: $2"; -"url_is_banned_default_reason" = "The link you are trying to open may lead you to a site that was created for the purpose of deceiving users with the intention of gaining profit."; +"url_is_banned" = "This link is banned"; +"url_is_banned_comment" = "The $1 administration advises against following this link."; +"url_is_banned_comment_r" = "The $1 administration advises against following this link.
Reason: $2"; +"url_is_banned_default_reason" = "The link you are attempting to open may lead to a site designed to deceive users for profit."; "url_is_banned_title" = "Link to a suspicious site"; -"url_is_banned_proceed" = "Follow the link"; +"url_is_banned_proceed" = "Proceed to the link"; "recently" = "Recently"; @@ -1516,17 +1658,17 @@ "c_group_removed" = "The group has been deleted."; "c_groups" = "Chandler Groups"; "c_users" = "Chandler Users"; -"c_group_permissions" = "Permissions"; -"c_group_members" = "Members"; +"c_group_permissions" = "Group Permissions"; +"c_group_members" = "Group Members"; "c_model" = "Model"; "c_permission" = "Permission"; "c_permissions" = "Permissions"; "c_color" = "Color"; "add" = "Add"; "c_edit_groups" = "Edit Groups"; -"c_user_is_not_in_group" = "The relationship between the user and the group was not found."; -"c_permission_not_found" = "The relationship between the permission and the group was not found."; -"c_group_not_found" = "The group was not found."; +"c_user_is_not_in_group" = "No relationship found between the user and the group."; +"c_permission_not_found" = "No relationship found between the permission and the group."; +"c_group_not_found" = "Group not found."; "c_user_is_already_in_group" = "This user is already a member of this group."; "c_add_to_group" = "Add to group"; "c_remove_from_group" = "Remove from group"; @@ -1535,8 +1677,8 @@ "global_maintenance" = "Undergoing maintenance"; "section_maintenance" = "The section is not available"; -"undergoing_global_maintenance" = "Unfortunately, the instance is now closed for technical work. We are already working on troubleshooting. Please try to come back later."; -"undergoing_section_maintenance" = "Unfortunately, the $1 section is temporarily unavailable. We are already working on troubleshooting. Please try to come back later."; +"undergoing_global_maintenance" = "Unfortunately, the instance is now closed for technical work. We are already working on troubleshooting. Please come back later."; +"undergoing_section_maintenance" = "Unfortunately, the $1 section is temporarily unavailable. We are already working on troubleshooting. Please come back later."; "topics" = "Topics"; @@ -1580,48 +1722,56 @@ "tour_section_2_title_1" = "Your Profile"; -"tour_section_2_text_1_1" = "After registering, you will automatically be redirected to your profile."; -"tour_section_2_text_1_2" = "You can edit it anywhere and anytime you want."; -"tour_section_2_text_1_3" = "Hint: To make your profile look nice and presentable, you can fill it with information or upload a photo that highlights, for example, your deep inner world."; -"tour_section_2_bottom_text_1" = "You are the one who decides how much information your friends need to know about you."; -"tour_section_2_title_2" = "Set your own privacy settings"; -"tour_section_2_text_2_1" = "You can define exactly who can access certain types of information and sections on your page."; -"tour_section_2_text_2_2" = "You have the right to block access to your page from search engines and unregistered users."; -"tour_section_2_text_2_3" = "Remember: privacy settings will be expanded in the future."; +"tour_section_2_text_1_1" = "Upon registration, you'll be redirected to your profile automatically."; +"tour_section_2_text_1_2" = "You have the flexibility to edit it at any time and from anywhere."; +"tour_section_2_text_1_3" = "Hint: To enhance your profile's appearance, consider filling it with information or uploading a photo that reflects your inner world."; +"tour_section_2_bottom_text_1" = "You decide how much information you want to share with your friends."; +"tour_section_2_title_2" = "Set Your Privacy Settings"; +"tour_section_2_text_2_1" = "Define who can access specific types of information and sections on your page."; +"tour_section_2_text_2_2" = "You can block access to your page from search engines and unregistered users."; +"tour_section_2_text_2_3" = "Remember: Privacy settings will be expanded in the future."; "tour_section_2_title_3" = "Profile URL"; -"tour_section_2_text_3_1" = "After registering your page, you get a personal ID like @id12345"; -"tour_section_2_text_3_2" = "The default ID, which was obtained after registration, cannot be changed"; -"tour_section_2_text_3_3" = "But in the settings of your page you can bind your personal address and this address can be changed at any time"; -"tour_section_2_text_3_4" = "Hint: You can take any address that is at least 5 characters long. Try to get a cool URL :)"; -"tour_section_2_bottom_text_2" = "Any short address in Latin small letters is supported; the address may contain numbers (not at the beginning), periods, and underscores (not at the beginning or end)"; +"tour_section_2_text_3_1" = "After registering, your page is assigned a personal ID like @id12345."; +"tour_section_2_text_3_2" = "The default ID obtained at registration cannot be changed."; +"tour_section_2_text_3_3" = "However, in your page settings, you can link a personal address that can be changed at any time."; +"tour_section_2_text_3_4" = "Hint: Choose an address that's at least 5 characters long for a cool URL :)"; +"tour_section_2_bottom_text_2" = "Supports any short address in lowercase Latin letters; the address may contain numbers (not at the beginning), periods, and underscores (not at the beginning or end)"; "tour_section_2_title_4" = "Wall"; -"tour_section_3_title_1" = "Share your photo moments"; -"tour_section_3_text_1" = "The "Photos" section is available in your profile as soon as you sign up"; -"tour_section_3_text_2" = "You can browse user photo albums and create your own"; -"tour_section_3_text_3" = "Access to all your photo albums for other users is controlled in the page privacy settings"; -"tour_section_3_bottom_text_1" = "You can create an unlimited number of photo albums from your travels or events, or just to store memes"; +"tour_section_3_title_1" = "Share Your Photo Moments"; +"tour_section_3_text_1" = "The \"Photos\" section becomes available in your profile as soon as you sign up."; +"tour_section_3_text_2" = "Explore user photo albums and create your own."; +"tour_section_3_text_3" = "Control access to all your photo albums for other users in the page privacy settings."; +"tour_section_3_bottom_text_1" = "Create an unlimited number of photo albums for your travels, events, or just to store memes."; "tour_section_4_title_1" = "Search"; -"tour_section_4_text_1" = "The "Search" section allows you to search for users and groups."; -"tour_section_4_text_2" = "This section of the site will be improved over time."; -"tour_section_4_text_3" = "To start a search, you need to know the user's first (or last) name; and if you're looking for a group, you need to know its name."; +"tour_section_4_text_1" = "The \"Search\" section allows you to search for users and groups."; +"tour_section_4_text_2" = "This section of the site will continue to improve over time."; +"tour_section_4_text_3" = "To start a search, you need to know the user's first (or last) name. For groups, you need to know the group's name."; "tour_section_4_title_2" = "Quick Search"; -"tour_section_4_text_4" = "If you want to save time in any way, the search bar is also available in the header of the site"; +"tour_section_4_text_4" = "To save time, use the search bar in the site header."; -"tour_section_5_title_1" = "Upload and share videos with your friends!"; -"tour_section_5_text_1" = "You can upload an unlimited number of videos and clips"; -"tour_section_5_text_2" = "The "Videos" section is controlled by the privacy settings"; -"tour_section_5_bottom_text_1" = "Videos can be uploaded bypassing the "Videos" section by simply attaching them to a new entry on the wall:"; -"tour_section_5_title_2" = "Importing videos from YouTube"; -"tour_section_5_text_3" = "In addition to uploading videos directly, the site also supports embedding videos from YouTube"; +"tour_section_5_title_1" = "Upload and Share Videos with Your Friends!"; +"tour_section_5_text_1" = "You can upload an unlimited number of videos and clips."; +"tour_section_5_text_2" = "Control the \"Videos\" section through privacy settings."; +"tour_section_5_bottom_text_1" = "Upload videos directly to the wall, bypassing the \"Videos\" section:"; +"tour_section_5_title_2" = "Importing Videos from YouTube"; +"tour_section_5_text_3" = "In addition to direct uploads, 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 music"; +"tour_section_6_text_1" = "You can listen to music in \"My Audios\""; +"tour_section_6_text_2" = "This section is also controlled by the privacy settings."; +"tour_section_6_text_3" = "The most listened songs are in \"Popular\", and recently uploaded songs are in \"New\""; +"tour_section_6_text_4" = "To add a song to your collection, hover over it and click on the \"plus\". You can search for the song you want."; +"tour_section_6_text_5" = "If you can't find the song you want, you can upload it yourself"; +"tour_section_6_bottom_text_1" = "Important: the song must not infringe copyright"; +"tour_section_6_title_2" = "Create playlists"; +"tour_section_6_text_6" = "You can create playlists in the \"My Playlists\" tab"; +"tour_section_6_text_7" = "You can also add another's playlists to your collection"; "tour_section_7_title_1" = "Follow what your friends write"; @@ -1630,80 +1780,84 @@ "tour_section_7_bottom_text_1" = "No recommendation system. It's up to you to create your own news feed"; -"tour_section_8_title_1" = "Follow what topics are discussed on the site"; -"tour_section_8_text_1" = "The global newsfeed will show entries of all site users and groups"; -"tour_section_8_text_2" = "Viewing this section may not be recommended for sensitive or fragile people"; -"tour_section_8_bottom_text_1" = "The design of the global feed does not differ in any way from the local feed"; -"tour_section_8_bottom_text_2" = "The feed has many types of content, from regular photos and videos to anonymous posts and polls"; +"tour_section_7_title_1" = "Stay Updated with Your Friends' Updates"; +"tour_section_7_text_1" = "The 'My News' section is divided into two types: local feed and global feed."; +"tour_section_7_text_2" = "The local feed exclusively displays updates from your friends and groups."; +"tour_section_7_bottom_text_1" = "No recommendation system. You have the power to curate your own news feed."; + +"tour_section_8_title_1" = "Explore Site-wide Discussions"; +"tour_section_8_text_1" = "The global newsfeed showcases posts from all site users and groups."; +"tour_section_8_text_2" = "Note: Viewer discretion is advised, as this section may contain content not suitable for sensitive individuals."; +"tour_section_8_bottom_text_1" = "The design of the global feed is identical to the local feed."; +"tour_section_8_bottom_text_2" = "The feed encompasses various content types, including regular photos, videos, anonymous posts, and polls."; -"tour_section_9_title_1" = "Create groups!"; -"tour_section_9_text_1" = "The site already has thousands of groups devoted to various topics and fan associations"; -"tour_section_9_text_2" = "You can join any group you want. And if you haven't found a matching one, you can create your own"; -"tour_section_9_text_3" = "Each group has its own section of wiki pages, photo albums, block of links and discussions"; -"tour_section_9_title_2" = "Manage your group with a friend"; -"tour_section_9_text_2_1" = "Manage the group in the "Edit Group" section under the community avatar"; -"tour_section_9_text_2_2" = "Create a team of administrators from the usual participants or those whom you trust"; -"tour_section_9_text_2_3" = "You can hide the administrator you want, so he will not appear anywhere within your group"; -"tour_section_9_bottom_text_1" = "The "My Groups" section is on the left menu of the site"; -"tour_section_9_bottom_text_2" = "Group example"; -"tour_section_9_bottom_text_3" = "Groups are often real organizations whose members want to stay in touch with their audiences"; - +"tour_section_9_title_1" = "Create Groups!"; +"tour_section_9_text_1" = "Explore thousands of groups on the site covering diverse topics and fan communities."; +"tour_section_9_text_2" = "Join any group that interests you, and if you can't find one, feel free to create your own."; +"tour_section_9_text_3" = "Each group comes with its dedicated section of wiki pages, photo albums, a block of links, and discussion forums."; +"tour_section_9_title_2" = "Manage Your Group with a Friend"; +"tour_section_9_text_2_1" = "Navigate to the "Edit Group" section under the community avatar to manage your group."; +"tour_section_9_text_2_2" = "Build a team of administrators from regular participants or individuals you trust."; +"tour_section_9_text_2_3" = "You have the option to hide specific administrators, ensuring they remain discreet within your group."; +"tour_section_9_bottom_text_1" = "Find the "My Groups" section in the left menu of the site."; +"tour_section_9_bottom_text_2" = "Example Group"; +"tour_section_9_bottom_text_3" = "Groups often represent real organizations, with members wanting to stay connected with their audiences."; "tour_section_10_title_1" = "Oops"; "tour_section_10_text_1" = "I would be very happy to do a tutorial on this section, but it's still under development. For now, let's skip it and move on..."; "tour_section_11_title_1" = "Themes"; -"tour_section_11_text_1" = "After registering, you will have the default theme installed as your appearance"; -"tour_section_11_text_2" = "Some new users may be a bit intimidated by the current default theme, which reeks of quite antiquity"; -"tour_section_11_text_3" = "But no worries: You can create your own theme for the site by reading the documentation or choose an existing one from the catalog"; -"tour_section_11_bottom_text_1" = "A catalog of themes is available under "My Settings", in the "Interface" tab;"; +"tour_section_11_text_1" = "Upon registration, the default theme will be set as your appearance."; +"tour_section_11_text_2" = "Some new users might find the current default theme a bit dated."; +"tour_section_11_text_3" = "No worries: You can create your own theme by reading the documentation or choose one from the catalog."; +"tour_section_11_bottom_text_1" = "A catalog of themes is available under \"My Settings\" in the \"Interface\" tab."; "tour_section_11_wordart" = "
Vy možete pomenjatí eto v nastrojkah."; +"do_not_attach_note" = "Ne prikrepljatí zametku"; +"something" = "Koe-čto"; +"supports_xhtml" = "iz (X)HTML podderživaetsja."; + +/* Notes: Article Viewer */ +"aw_legacy_ui" = "Staryj interfejs"; + +/* Menus */ + +"edit_button" = "red."; + +"my_page" = "Moja Stranica"; +"my_friends" = "Moi Druzíja"; +"my_photos" = "Moi Fotografii"; +"my_videos" = "Moi Videozapisi"; +"my_messages" = "Moi Soobsčenija"; +"my_notes" = "Moi Zametki"; +"my_audios" = "Moi Audiozapisi"; +"my_groups" = "Moi Gruppy"; +"my_feed" = "Moi Novosti"; +"my_feedback" = "Moi Otvety"; +"my_settings" = "Moi Nastrojki"; +"bug_tracker" = "Bag-treker"; + +"menu_settings" = "Nastrojki"; +"menu_login" = "Vhod"; +"menu_registration" = "Registracija"; +"menu_help" = "Pomosčí"; +"menu_logout" = "Vyjti"; +"menu_support" = "Podderžka"; + +"header_home" = "glavnaja"; +"header_groups" = "gruppy"; +"header_people" = "ljudi"; +"header_invite" = "priglasití"; +"header_help" = "pomosčí"; +"header_log_out" = "vyjti"; +"header_search" = "Poisk"; +"header_login" = "vhod"; +"header_registration" = "registracija"; + +"left_menu_donate" = "Podderžatí"; + +"footer_about_instance" = "ob instancii"; +"footer_rules" = "pravila"; +"footer_blog" = "blog"; +"footer_help" = "pomosčí"; +"footer_developers" = "razrabotčikam"; +"footer_choose_language" = "vybratí jazyk"; +"footer_privacy" = "privatností"; + +/* Settings */ + +"main" = "Osnovnoe"; +"contacts" = "Kontakty"; +"avatar" = "Avatar"; +"privacy" = "Privatností"; +"interface" = "Vnešnij vid"; +"security" = "Bezopasností"; +"profile_picture" = "Izobraženie stranicy"; +"picture" = "Izobraženie"; +"change_password" = "Izmenití parolí"; +"old_password" = "Staryj parolí"; +"new_password" = "Novyj parolí"; +"repeat_password" = "Povtorite parolí"; +"avatars_style" = "Otobraženie avatarov"; +"style" = "Stilí"; +"default" = "po umolčaniju"; +"arbitrary_avatars" = "Proizvolínye"; +"cut" = "Kvadratnye"; +"round_avatars" = "Kruglye"; +"apply_style_for_this_device" = "Primenití stilí tolíko dlja etogo ustrojstva"; +"search_for_groups" = "Poisk grupp"; +"search_for_users" = "Poisk ljudej"; +"search_for_posts" = "Poisk zapisej"; +"search_for_comments" = "Poisk kommentariev"; +"search_for_videos" = "Poisk video"; +"search_for_apps" = "Poisk priloženij"; +"search_for_notes" = "Poisk zapisok"; +"search_for_audios" = "Poisk muzyki"; +"search_button" = "Najti"; +"search_placeholder" = "Načnite vvodití ljuboe imja, nazvanie ili slovo"; +"results_zero" = "Ni odnogo rezulítata"; +"results_one" = "Odin rezulítat"; +"results_few" = "$1 rezulítata"; +"results_many" = "$1 rezulítatov"; +"results_other" = "$1 rezulítatov"; +"privacy_setting_access_page" = "Komu v internete vidno moju stranicu"; +"privacy_setting_read_info" = "Komu vidno osnovnuju informaciju moej stranicy"; +"privacy_setting_see_groups" = "Komu vidno moi gruppy i vstreči"; +"privacy_setting_see_photos" = "Komu vidno moi fotografii"; +"privacy_setting_see_videos" = "Komu vidno moi videozapisi"; +"privacy_setting_see_notes" = "Komu vidno moi zametki"; +"privacy_setting_see_friends" = "Komu vidno moih druzej"; +"privacy_setting_add_to_friends" = "Kto možet nazyvatí menja drugom"; +"privacy_setting_write_wall" = "Kto možet pisatí u menja na stene"; +"privacy_setting_write_messages" = "Kto možet pisatí mne soobsčenija"; +"privacy_setting_view_audio" = "Komu vidno moi audiozapisi"; +"privacy_value_anybody" = "Vse želajusčie"; +"privacy_value_anybody_dative" = "Vsem želajusčim"; +"privacy_value_users" = "Polízovateljam OpenVK"; +"privacy_value_friends" = "Druzíja"; +"privacy_value_friends_dative" = "Druzíjam"; +"privacy_value_only_me" = "Tolíko ja"; +"privacy_value_only_me_dative" = "Tolíko mne"; +"privacy_value_nobody" = "Nikto"; +"your_email_address" = "Adres Vašej elektronnoj počty"; +"your_page_address" = "Adres Vašej stranicy"; +"page_address" = "Adres stranicy"; +"current_email_address" = "Tekusčij adres"; +"new_email_address" = "Novyj adres"; +"save_email_address" = "Sohranití adres"; +"page_id" = "ID stranicy"; +"you_can_also" = "Vy takže možete"; +"delete_your_page" = "udalití svoju stranicu"; +"delete_album" = "udalití alíbom"; +"ui_settings_interface" = "Interfejs"; +"ui_settings_sidebar" = "Levoe menju"; +"ui_settings_rating" = "Rejting"; +"ui_settings_rating_show" = "Pokazyvatí"; +"ui_settings_rating_hide" = "Skryvatí"; +"ui_settings_nsfw_content" = "NSFW-kontent"; +"ui_settings_nsfw_content_dont_show" = "Ne pokazyvatí v globalínoj lente"; +"ui_settings_nsfw_content_blur" = "Tolíko zamazyvatí"; +"ui_settings_nsfw_content_show" = "Pokazyvatí"; +"ui_settings_view_of_posts" = "Vid postov"; +"ui_settings_view_of_posts_old" = "Staryj"; +"ui_settings_view_of_posts_microblog" = "Mikroblog"; +"ui_settings_main_page" = "Glavnaja stranica"; +"ui_settings_sessions" = "Sessii"; +"additional_links" = "Dopolnitelínye ssylki"; +"ad_poster" = "Reklamnyj plakat"; +"email_change_confirm_message" = "Čtoby izmenenie vstupilo v silu, podtverdite vaš novyj adres elektronnoj počty. My otpravili instrukcii na nego."; +"profile_deactivate" = "Udalenie stranicy"; +"profile_deactivate_button" = "Udalití stranicu"; +"profile_deactivate_header" = "My sožaleem, čto Vy hotite udalití svoju stranicu. Poetomu, Vy možete ukazatí pričinu udalenija i Vaše soobsčenie po etomu povodu. My čitaem Vaši otzyvy i pytaemsja sdelatí sajt lučše!"; +"profile_deactivate_reason_header" = "Požalujsta, ukažite pričinu udalenija Vašej stranicy"; +"profile_deactivate_reason_1" = "U menja estí drugaja stranica na sajte"; +"profile_deactivate_reason_1_text" = "Ja sozdal novuju stranicu i teperí hoču podteretí svojo prošloe."; +"profile_deactivate_reason_2" = "Sajt otnimaet u menja sliškom mnogo vremeni"; +"profile_deactivate_reason_2_text" = "Pustí etot sajt horoš i prekrasen, no on otnimaet u menja vremja, kotoroe mne nužno dlja raboty i žizni."; +"profile_deactivate_reason_3" = "Sajt soderžit sliškom mnogo nepriemlemyh materialov"; +"profile_deactivate_reason_3_text" = "Ja našjol dostatočno pornografii i piratskogo kontenta - hvatit na vsju žizní. Teperí ja uhožu."; +"profile_deactivate_reason_4" = "Menja bespokoit bezopasností moih dannyh"; +"profile_deactivate_reason_4_text" = "Za mnoj sledjat i mne strašno zdesí nahoditsja. Izvinite, ja vynužden ujti."; +"profile_deactivate_reason_5" = "Moju stranicu ne kommentirujut"; +"profile_deactivate_reason_5_text" = "Menja nikto ne smotrit zdesí i eto grustno. Vy požaleete o tom, čto ja ušjol."; +"profile_deactivate_reason_6" = "Drugaja pričina"; +"profile_deactivated_msg" = "Vaša stranica udalena.
Esli Vy zahotite snova načatí polízovatísja sajtom, Vy možete vosstanovití svoju stranicu do $1."; +"profile_deactivated_status" = "Stranica udalena"; +"profile_deactivated_info" = "Stranica polízovatelja udalena.
Informacija nedostupna."; +"share_with_friends" = "Rasskazatí druzíjam"; +"end_all_sessions" = "Sbrosití vse sessii"; +"end_all_sessions_description" = "Esli vy hotite vyjti iz $1 so vseh ustrojstv, nažmite na knopku niže"; +"end_all_sessions_done" = "Vse sessii sbrošeny, vključaja mobilínye priloženija"; +"backdrop_short" = "Fon"; +"backdrop" = "Fon stranicy"; +"backdrop_desc" = "Vy možete ustanovití dva izobraženija v kačestve fona vašej stranicy. Oni budut otobražatísja po bokam u teh, kto zajdjot na vašu stranicu. S pomosčíju etoj vozmožnosti vy možete dobavití svoemu profilju bolíše individualínosti."; +"backdrop_warn" = "Izobraženija budut raspoloženy tak, kak na sheme vyše. Ih vysota budet avtomatičeski uveličena, čtoby oni zanimali 100% vysoty ekrana, poseredine budet razmytie. Zamenití fon osnovnogo interfejsa OpenVK ili dobavití audiozapisí nelízja."; +"backdrop_about_adding" = "Vy možete ustanovití tolíko 1 izobraženie (no budet nekrasivo), a tak že zamenití tolíko odno: esli u vas uže stoit dva, a vy hotite zamenití vtoroe - to zagružajte tolíko vtoroe, pervoe sohranitsja, a čtoby udalití nado nažatí na sootvetstvujusčuju knopku vnizu, udaljatí po odnoj nelízja."; +"backdrop_save" = "Sohranití fon"; +"backdrop_remove" = "Udalití fon"; +"backdrop_error_title" = "Ne udalosí sohranití fon"; +"backdrop_error_no_media" = "Izobraženija povreždeny ili zagruženy ne polnostíju"; +"backdrop_succ" = "Fon sohranjon"; +"backdrop_succ_rem" = "Fon udaljon"; +"backdrop_succ_desc" = "Izmenenija budut zametny drugim polízovateljam čerez 5 minut."; +"browse" = "Obzor"; + +/* Two-factor authentication */ + +"two_factor_authentication" = "Dvuhfaktornaja autentifikacija"; +"two_factor_authentication_disabled" = "Obespečivaet nadežnuju zasčitu ot vzloma: dlja vhoda na stranicu neobhodimo vvesti kod, polučennyj v priloženii 2FA."; +"two_factor_authentication_enabled" = "Dvuhfaktornaja autentifikacija vključena. Vaša stranica zasčisčena."; +"two_factor_authentication_login" = "U vas vključena dvuhfaktornaja autentifikacija. Dlja vhoda vvedite kod polučennyj v priloženii."; +"two_factor_authentication_settings_1" = "Dvuhfaktornuju autentifikaciju čerez TOTP možno ispolízovatí daže bez interneta. Dlja etogo vam ponadobitsja priloženie dlja generacii kodov. Naprimer, Google Authenticator dlja Android i iOS ili svobodnye Aegis i andOTP dlja Android. Ubeditesí, čto na telefone točno ustanovlena data i vremja."; +"two_factor_authentication_settings_2" = "Ispolízuja priloženie dlja dvuhfaktornoj autentifikacii, otskanirujte privedennyj niže QR-kod:"; +"two_factor_authentication_settings_3" = "ili vručnuju vvedite sekretnyj ključ: $1."; +"two_factor_authentication_settings_4" = "Teperí vvedite kod, kotoryj vam predostavilo priloženie, i parolí ot vašej stranicy, čtoby my mogli podtverdití, čto vy dejstvitelíno vy."; +"connect" = "Podključití"; +"enable" = "Vključití"; +"disable" = "Otključití"; +"code" = "Kod"; +"2fa_code" = "Kod 2FA"; +"incorrect_password" = "Nevernyj parolí"; +"incorrect_code" = "Nevernyj kod"; +"incorrect_2fa_code" = "Nevernyj kod dvuhfaktornoj autentifikacii"; +"two_factor_authentication_enabled_message" = "Dvuhfaktornaja autentifikacija vključena"; +"two_factor_authentication_enabled_message_description" = "Vašu stranicu stalo trudnee vzlomatí. Rekomenduem vam skačatí rezervnye kody"; +"two_factor_authentication_disabled_message" = "Dvuhfaktornaja autentifikacija otključena"; +"view_backup_codes" = "Posmotretí rezervnye kody"; +"backup_codes" = "Rezervnye kody dlja podtverždenija vhoda"; +"two_factor_authentication_backup_codes_1" = "Rezervnye kody pozvoljajut podtverždatí vhod, kogda u vas net dostupa k telefonu, naprimer, v putešestvii."; +"two_factor_authentication_backup_codes_2" = "U vas estí esčjo 10 kodov, každym kodom možno vospolízovatísja tolíko odin raz. Raspečatajte ih, uberite v nadežnoe mesto i ispolízujte, kogda potrebujutsja kody dlja podtverždenija vhoda."; +"two_factor_authentication_backup_codes_3" = "Vy možete polučití novye kody, esli oni zakančivajutsja. Dejstvitelíny tolíko poslednie sozdannye rezervnye kody."; +"viewing_backup_codes" = "Prosmotr rezervnyh kodov"; +"disable_2fa" = "Otključití 2FA"; +"viewing" = "Prosmotretí"; + +/* Sorting */ + +"sort_randomly" = "Sortirovatí slučajno"; +"sort_up" = "Sortirovatí po date sozdanija vverh"; +"sort_down" = "Sortirovatí po date sozdanija vniz"; + +/* Videos */ + +"videos" = "Videozapisi"; +"video" = "Videozapisí"; +"upload_video" = "Zagruzití video"; +"video_uploaded" = "Zagruženo"; +"video_updated" = "Obnovleno"; +"video_link_to_yt" = "Ssylka na YouTube"; +"info_name" = "Nazvanie"; +"info_description" = "Opisanie"; +"info_uploaded_by" = "Zagruzil"; +"info_upload_date" = "Data zagruzki"; +"videos_zero" = "Net video"; +"videos_one" = "Odna videozapisí"; +"videos_few" = "$1 videozapisi"; +"videos_many" = "$1 videozapisej"; +"videos_other" = "$1 videozapisej"; +"view_video" = "Prosmotr"; +"change_video" = "Izmenití videozapisí"; +"unknown_video" = "Eta videozapisí ne podderživaetsja v vašej versii OpenVK."; + +"selecting_video" = "Vybor videozapisej"; +"upload_new_video" = "Zagruzití novoe video"; +"max_attached_videos" = "Maksimum 10 videozapisej"; +"max_attached_photos" = "Maksimum 10 fotografij"; +"max_attached_audios" = "Maksimum 10 audiozapisej"; +"no_videos" = "U vas net video."; +"no_videos_results" = "Net rezulítatov."; + +/* Audios */ + +"audios" = "Audiozapisi"; +"audio" = "Audiozapisí"; +"playlist" = "Plejlist"; +"upload_audio" = "Zagruzití audio"; +"upload_audio_to_group" = "Zagruzití audio v gruppu"; + +"performer" = "Ispolnitelí"; +"audio_name" = "Nazvanie"; +"genre" = "Žanr"; +"lyrics" = "Tekst"; + +"select_another_file" = "Vybratí drugoj fajl"; + +"limits" = "Ograničenija"; +"select_audio" = "Vyberite audiozapisí na Vašem kompíjutere"; +"audio_requirements" = "Audiozapisí dolžna bytí dlinnoj ot $1c do $2 minut, vesití do $3mb i soderžatí audiopotok."; +"audio_requirements_2" = "Audiozapisí ne dolžna narušatí avtorskie i smežnye prava."; +"you_can_also_add_audio_using" = "Vy takže možete dobavití audiozapisí iz čisla uže zagružennyh fajlov, vospolízovavšisí"; +"search_audio_inst" = "poiskom po audio"; + +"audio_embed_not_found" = "Audiozapisí ne najdena"; +"audio_embed_deleted" = "Audiozapisí byla udalena"; +"audio_embed_withdrawn" = "Audiozapisí byla izjjata po obrasčeniju pravoobladatelja."; +"audio_embed_forbidden" = "Nastrojki privatnosti polízovatelja ne pozvoljajut vstraivatí etu kompoziciju"; +"audio_embed_processing" = "Audio esčjo obrabatyvaetsja, libo obrabotalosí nepravilíno."; + +"audios_count_zero" = "Net audiozapisej"; +"audios_count_one" = "Odna audiozapisí"; +"audios_count_few" = "$1 audiozapisi"; +"audios_count_many" = "$1 audiozapisej"; +"audios_count_other" = "$1 audiozapisej"; + +"track_unknown" = "Neizvesten"; +"track_noname" = "Bez nazvanija"; + +"my_music" = "Moja muzyka"; +"music_user" = "Muzyka polízovatelja"; +"music_club" = "Muzyka gruppy"; +"audio_new" = "Novoe"; +"audio_popular" = "Populjarnoe"; +"audio_search" = "Poisk"; + +"my_audios_small" = "Moi audiozapisi"; +"my_playlists" = "Moi plejlisty"; +"playlists" = "Plejlisty"; +"audios_explicit" = "Soderžit necenzurnuju leksiku"; +"withdrawn" = "Izjjato"; +"deleted" = "Udaleno"; +"owner" = "Vladelec"; +"searchable" = "Dostupno v poiske"; + +"select_audio" = "Vybratí audiozapisi"; +"no_playlists_thisuser" = "Vy esčjo ne dobavljali plejlistov."; +"no_playlists_user" = "Etot polízovatelí esčjo ne dobavljal plejlistov."; +"no_playlists_club" = "Eta gruppa esčjo ne dobavljala plejlistov."; + +"no_audios_thisuser" = "Vy esčjo ne dobavljali audiozapisej."; +"no_audios_user" = "Etot polízovatelí esčjo ne dobavljal audiozapisej."; +"no_audios_club" = "Eta gruppa esčjo ne dobavljala audiozapisej."; + +"new_playlist" = "Novyj plejlist"; +"created_playlist" = "sozdan"; +"updated_playlist" = "obnovljon"; +"bookmark" = "Dobavití v kollekciju"; +"unbookmark" = "Ubratí iz kollekcii"; +"empty_playlist" = "V etom plejliste net audiozapisej."; +"edit_playlist" = "Redaktirovatí plejlist"; +"unable_to_load_queue" = "Ne udalosí zagruzití očeredí."; + +"fully_delete_audio" = "Polnostíju udalití audiozapisí"; +"attach_audio" = "Prikrepití audiozapisí"; +"detach_audio" = "Otkrepití audiozapisí"; + +"show_more_audios" = "Pokazatí bolíše audiozapisej"; +"add_to_playlist" = "Dobavití v plejlist"; +"remove_from_playlist" = "Udalití iz plejlista"; +"delete_playlist" = "Udalití plejlist"; +"playlist_cover" = "Obložka plejlista"; +"playlists_user" = "Plejlisty políz."; +"playlists_club" = "Plejlisty gruppy"; +"change_cover" = "Smenití obložku"; +"playlist_cover" = "Obložka plejlista"; + +"minutes_count_zero" = "dlitsja nolí minut"; +"minutes_count_one" = "dlitsja odnu minutu"; +"minutes_count_few" = "dlitsja $1 minuty"; +"minutes_count_many" = "dlitsja $1 minut"; +"minutes_count_other" = "dlitsja $1 minut"; + +"listens_count_zero" = "net proslušivanij"; +"listens_count_one" = "odno proslušivanie"; +"listens_count_few" = "$1 proslušivanija"; +"listens_count_many" = "$1 proslušivanij"; +"listens_count_other" = "$1 proslušivanij"; + +"add_audio_to_club" = "Dobavití audio v gruppu"; +"what_club_add" = "V kakuju gruppu vy hotite dobavití pesnju?"; +"group_has_audio" = "U gruppy uže estí eta pesnja."; +"group_hasnt_audio" = "U gruppy net etoj pesni."; + +"by_name" = "po kompozicijam"; +"by_performer" = "po ispolnitelju"; +"no_access_clubs" = "Net grupp, gde vy javljaetesí administratorom."; +"audio_successfully_uploaded" = "Audio uspešno zagruženo i na dannyj moment obrabatyvaetsja."; + +"broadcast_audio" = "Translirovatí audio v status"; +"sure_delete_playlist" = "Vy dejstvitelíno hotite udalití etot plejlist?"; +"edit_audio" = "Redaktirovatí audiozapisí"; +"audios_group" = "Audiozapisi gruppy"; +"playlists_group" = "Plejlisty gruppy"; + +"play_tip" = "Proigryvanie/pauza"; +"repeat_tip" = "Povtorenie"; +"shuffle_tip" = "Peremešatí"; +"mute_tip" = "Zaglušití"; + +/* Notifications */ + +"feedback" = "Otvety"; +"unread" = "Nepročitannoe"; +"archive" = "Arhiv"; +"notifications_like" = "$1 ocenil vašu $2zapisí$3 ot $4"; +"notifications_repost" = "$1 podelilsja(-lasí) vašej $2zapisíju$3 ot $4"; +"notifications_comment_under" = "$1 ostavil(-la) kommentarij pod $2"; +"notifications_under_note" = "vašej $3zametkoj$4"; +"notifications_under_photo" = "vašim $3foto$4"; +"notifications_under_post" = "vašej $3zapisíju$4 ot $5"; +"notifications_under_video" = "vašim $3video$4"; +"notifications_post" = "$1 napisal(-la) $2zapisí$3 na vašej stene: $4"; +"notifications_appoint" = "$1 naznačil vas rukovoditelem soobsčestva $2"; +"nt_liked_yours" = "ponravilsja vaš"; +"nt_shared_yours" = "podelilsja(-así) vašim"; +"nt_commented_yours" = "ostavil(a) kommentarij pod"; +"nt_written_on_your_wall" = "napisal(a) na vašej stene"; +"nt_made_you_admin" = "naznačil(a) vas rukovoditelem soobsčestva"; +"nt_from" = "ot"; +"nt_yours_adjective" = "vašim"; +"nt_yours_feminitive_adjective" = "vašej"; +"nt_post_nominative" = "post"; +"nt_post_instrumental" = "postom"; +"nt_note_instrumental" = "zametkoj"; +"nt_photo_instrumental" = "fotografiej"; +"nt_topic_instrumental" = "temoj"; + +"nt_you_were_mentioned_u" = "Vas upomjanul polízovatelí"; +"nt_you_were_mentioned_g" = "Vas upomjanulo soobsčestvo"; +"nt_mention_in_post_or_comms" = "v poste ili v odnoj iz vetok ego obsuždenija"; +"nt_mention_in_photo" = "v obsuždenii fotografii"; +"nt_mention_in_video" = "v obsuždenii videozapisi"; +"nt_mention_in_note" = "v obsuždenii zametki"; +"nt_mention_in_topic" = "v obsuždenii"; +"nt_sent_gift" = "otpravil vam podarok"; + +/* Time */ + +"time_at_sp" = " v "; +"time_just_now" = "tolíko čto"; +"time_exactly_five_minutes_ago" = "rovno 5 minut nazad"; +"time_minutes_ago" = "$1 minut nazad"; +"time_today" = "segodnja"; +"time_yesterday" = "včera"; +"points" = "Golosa"; +"points_count" = "golosov"; +"on_your_account" = "na vašem sčetu"; +"top_up_your_account" = "Popolnití balans"; +"you_still_have_x_points" = "U Vas $1 neispolízovannyh golosov."; +"vouchers" = "Vaučery"; +"have_voucher" = "Estí vaučer"; +"voucher_token" = "Kod vaučera"; +"voucher_activators" = "Vospolízovavšiesja"; +"voucher_explanation" = "Vvedite serijnyj nomer vaučera. Obyčno on ukazan v čeke ili v soobsčenii."; +"voucher_explanation_ex" = "Obratite vnimanie, čto vaučery mogut istekatí i vospolízovatísja imi možno tolíko odin raz."; +"invalid_voucher" = "Vaučer nedejstvitelínyj"; +"voucher_bad" = "Vozmožno, vy vveli nevernyj serijnyj nomer, uže ispolízovali dannyj vaučer ili že on prosto istjok."; +"voucher_good" = "Vaučer aktivirovan"; +"voucher_redeemed" = "Vaučer byl uspešno aktivirovan. Vam budut načisleny golosa, no etim kodom vy bolíše ne smožete aktivirovatí ego."; +"redeem" = "Aktivirovatí vaučer"; +"deactivate" = "Deaktivirovatí"; +"usages_total" = "Količestvo ispolízovanij"; +"usages_left" = "Ostalosí ispolízovanij"; +"points_transfer_dialog_header_1" = "Vy možete otpravití v podarok ili peredatí častí golosov drugomu čeloveku."; +"points_transfer_dialog_header_2" = "Vaš tekusčij balans:"; +"points_amount_one" = "1 golos"; +"points_amount_few" = "$1 golosa"; +"points_amount_many" = "$1 golosov"; +"points_amount_other" = "$1 golosov"; +"transfer_poins" = "Peredača golosov"; +"transfer_poins_button" = "Peredatí golosa"; +"also_you_can_transfer_points" = "Takže vy možete peredatí golosa drugomu čeloveku."; +"transferred_to_you" = "peredal vam"; +"transfer_trough_ton" = "Popolnití s pomosčíju TON"; +"transfer_ton_contents" = "Vy možete popolnití vaš balans s pomosčíju kriptovaljuty TON. Dostatočno otskanirovatí QR-code priloženiem Tonkeeper, ili vručnuju otpravití TON po rekvizitam. V tečenie neskolíkih minut vam pridut opredelennoe količestvo golosov."; +"transfer_ton_address" = "Adres košelíka: $1
Soderžanie soobsčenija: $2"; +"transfer_ton_currency_per_ton" = "$1 TON"; +"receiver_address" = "Adres polučatelja"; +"coins_count" = "Količestvo golosov"; +"message" = "Soobsčenie"; +"failed_to_tranfer_points" = "Ne udalosí peredatí golosa"; +"points_transfer_successful" = "Vy uspešno peredali $1 $3."; +"not_all_information_has_been_entered" = "Vvedena ne vsja informacija."; +"negative_transfer_value" = "My ne možem ukrastí golosa u drugogo čeloveka, izvinite."; +"message_is_too_long" = "Soobsčenie sliškom dlinnoe."; +"receiver_not_found" = "Polučatelí ne najden."; +"you_dont_have_enough_points" = "U vas nedostatočno golosov."; +"increase_rating" = "Povysití rejting"; +"increase_rating_button" = "Povysití"; +"to_whom" = "Komu"; +"increase_by" = "Povysití na"; +"price" = "Stoimostí"; +"you_have_unused_votes" = "U Vas $1 neispolízovannyh golosa na balanse."; +"apply_voucher" = "Primenití vaučer"; +"failed_to_increase_rating" = "Ne udalosí povysití rejting"; +"rating_increase_successful" = "Vy uspešno povysyli rejting $2 na $3%."; +"negative_rating_value" = "My ne možem ukrastí rejting u drugogo čeloveka, izvinite."; +"increased_your_rating_by" = "povysil vaš rejting na"; + +/* Gifts */ + +"gift" = "Podarok"; +"gifts" = "Podarki"; +"gifts_zero" = "Net podarkov"; +"gifts_one" = "Odin podarok"; +"gifts_few" = "$1 podarka"; +"gifts_many" = "$1 podarkov"; +"gifts_other" = "$1 podarkov"; +"gifts_left" = "Podarkov ostalosí: $1"; +"gifts_left_one" = "Odin podarok ostalsja"; +"gifts_left_few" = "$1 podarka ostalosí"; +"gifts_left_many" = "$1 podarkov ostalosí"; +"gifts_left_other" = "$1 podarkov ostalosí"; +"send_gift" = "Otpravití podarok"; +"gift_select" = "Vybratí podarok"; +"collections" = "Kollekcii"; +"confirm" = "Podtverždenie"; +"as_anonymous" = "Anonimno"; +"gift_your_message" = "Vaše soobsčenie"; +"free_gift" = "Besplatno"; +"coins" = "Golosa"; +"coins_zero" = "0 golosov"; +"coins_one" = "Odin golos"; +"coins_few" = "$1 golosa"; +"coins_many" = "$1 golosov"; +"coins_other" = "$1 golosov"; +"users_gifts" = "Podarki"; + +/* Apps */ +"app" = "Priloženie"; +"apps" = "Priloženija"; +"my_apps" = "Moi Priloženija"; +"all_apps" = "Vse priloženija"; +"installed_apps" = "Moi priloženija"; +"own_apps" = "Upravlenie"; +"own_apps_alternate" = "Moi priloženija"; +"app_play" = "zapustití"; +"app_uninstall" = "otključití"; +"app_edit" = "redaktirovatí"; +"app_dev" = "Razrabotčik"; +"create_app" = "Sozdatí priloženie"; +"edit_app" = "Redaktirovatí priloženie"; +"new_app" = "Novoe priloženie"; +"app_news" = "Zametka s novostjami"; +"app_state" = "Sostojanie"; +"app_enabled" = "Vključeno"; +"app_creation_hint_url" = "Ukažite v URL točnyj adres vmeste so shemoj (https), portom (80) i nužnymi parametrami zaprosa."; +"app_creation_hint_iframe" = "Vaše priloženie budet otkryto v iframe."; +"app_balance" = "Na sčetu vašego priloženija $1 golosov."; +"app_users" = "Vašim priloženiem polízujutsja $1 čelovek."; +"app_withdrawal_q" = "vyvesti?"; +"app_withdrawal" = "Vyvod sredstv"; +"app_withdrawal_empty" = "Ne udalosí vyvesti pustotu, izvinite."; +"app_withdrawal_created" = "Zajavka na vyvod $1 golosov byla sozdana. Ožidajte začislenija."; +"appjs_payment" = "Oplata pokupki"; +"appjs_payment_intro" = "Vy sobiraetesí oplatití zakaz v priloženii"; +"appjs_order_items" = "Sostav zakaza"; +"appjs_payment_total" = "Itogovaja summa k oplate"; +"appjs_payment_confirm" = "Oplatití"; +"appjs_err_funds" = "Ne udalosí oplatití pokupku: nedostatočno sredstv."; +"appjs_wall_post" = "Opublikovatí zapisí"; +"appjs_wall_post_desc" = "hočet opublikovatí na Vašej stene zapisí"; +"appjs_act_friends" = "vašim Druzíjam"; +"appjs_act_friends_desc" = "dobavljatí polízovatelej v druzíja i čitatí Vaš spisok druzej"; +"appjs_act_wall" = "vašej Stene"; +"appjs_act_wall_desc" = "smotretí Vaši novosti, Vašu stenu i sozdavatí na nej zapisi"; +"appjs_act_messages" = "vašim Soobsčenijam"; +"appjs_act_messages_desc" = "čitatí i pisatí ot Vašego imeni soobsčenija"; +"appjs_act_groups" = "vašim Soobsčestvam"; +"appjs_act_groups_desc" = "smotretí spisok Vaših grupp i podpisyvatí vas na drugie"; +"appjs_act_likes" = "funkcionalu Lajkov"; +"appjs_act_likes_desc" = "stavití i ubiratí otmetki \"Mne nravitsja\" s zapisej"; +"appjs_act_request" = "Zapros dostupa"; +"appjs_act_requests" = "zaprašivaet dostup k"; +"appjs_act_can" = "Priloženie smožet"; +"appjs_act_allow" = "Razrešití"; +"appjs_act_disallow" = "Ne razrešatí"; +"app_uninstalled" = "Priloženie otključeno"; +"app_uninstalled_desc" = "Ono bolíše ne smožet vypolnjatí dejstvija ot Vašego imeni."; +"app_err_not_found" = "Priloženie ne najdeno"; +"app_err_not_found_desc" = "Nekorrektnyj identifikator ili ono bylo otključeno."; +"app_err_forbidden_desc" = "Eto priloženie ne Vaše."; +"app_err_url" = "Nepravilínyj adres"; +"app_err_url_desc" = "Adres priloženija ne prošjol proverku, ubeditesí čto on ukazan pravilíno."; +"app_err_ava" = "Ne udalosí zagruzití avatarku"; +"app_err_ava_desc" = "Avatarka sliškom bolíšaja ili krivaja: ošibka obsčego haraktera №$res."; +"app_err_note" = "Ne udalosí prikrepití novostnuju zametku"; +"app_err_note_desc" = "Ubeditesí, čto ssylka pravilínaja i zametka prinadležit Vam."; + +"learn_more" = "Podrobnee"; + +/* Support */ + +"support_opened" = "Otkrytye"; +"support_answered" = "S otvetom"; +"support_closed" = "Zakrytye"; +"support_ticket" = "Obrasčenie"; +"support_tickets" = "Obrasčenija"; +"support_status_0" = "Vopros na rassmotrenii"; +"support_status_1" = "Estí otvet"; +"support_status_2" = "Zakryto"; +"support_greeting_hi" = "Zdravstvujte, $1!"; +"support_greeting_regards" = "S uvaženiem,
komanda podderžki $1."; +"support_faq" = "Často zadavaemye voprosy"; +"support_list" = "Spisok obrasčenij"; +"support_new" = "Novoe obrasčenie"; +"support_new_title" = "Vvedite temu vašego obrasčenija"; +"support_new_content" = "Opišite problemu ili predloženie"; + + +"support_rate_good_answer" = "Eto horošij otvet"; +"support_rate_bad_answer" = "Eto plohoj otvet"; +"support_good_answer_user" = "Vy ostavili položitelínyj otzyv."; +"support_bad_answer_user" = "Vy ostavili negativnyj otzyv."; +"support_good_answer_agent" = "Polízovatelí ostavil položitelínyj otzyv."; +"support_bad_answer_agent" = "Polízovatelí ostavil negativnyj otzyv"; +"support_rated_good" = "Vy ostavili položitelínyj otzyv ob otvete."; +"support_rated_bad" = "Vy ostavili negativnyj otzyv ob otvete."; +"wrong_parameters" = "Nevernye parametry zaprosa."; +"fast_answers" = "Bystrye otvety"; + +"reports" = "Žaloby"; +"ignore_report" = "Ignorirovatí žalobu"; +"report_number" = "Žaloba №"; +"list_of_reports" = "Spisok žalob"; +"text_of_the_post" = "Tekst zapisi"; +"today" = "segodnja"; + +"will_be_watched" = "Skoro ejo rassmotrjat moderatory"; + +"report_question" = "Požalovatísja?"; +"report_question_text" = "Čto imenno vam kažetsja nedopustimym v etom materiale?"; +"report_reason" = "Pričina žaloby"; +"reason" = "Pričina"; +"going_to_report_app" = "Vy sobiraetesí požalovatísja na dannoe priloženie."; +"going_to_report_club" = "Vy sobiraetesí požalovatísja na dannoe soobsčestvo."; +"going_to_report_photo" = "Vy sobiraetesí požalovatísja na dannuju fotografiju."; +"going_to_report_user" = "Vy sobiraetesí požalovatísja na dannogo polízovatelja."; +"going_to_report_video" = "Vy sobiraetesí požalovatísja na dannuju videozapisí."; +"going_to_report_post" = "Vy sobiraetesí požalovatísja na dannuju zapisí."; +"going_to_report_comment" = "Vy sobiraetesí požalovatísja na dannyj kommentarij."; + +"comment" = "Kommentarij"; +"sender" = "Otpravitelí"; +"author" = "Avtor"; +"you_have_not_entered_text" = "Vy ne vveli tekst"; +"you_have_not_entered_name_or_text" = "Vy ne vveli imja ili tekst"; +"ticket_changed" = "Tiket izmenjon"; +"ticket_changed_comment" = "Izmenenija vstupjat silu čerez neskolíko sekund."; +"banned_in_support_1" = "Izvinite, $1, no teperí vam nelízja sozdavatí obrasčenija."; +"banned_in_support_2" = "A pričina etomu prosta: $1. K sožaleniju, na etot raz nam prišlosí otobratí u vas etu vozmožností navsegda."; +"you_can_close_this_ticket_1" = "Esli u Vas bolíše net voprosov, Vy možete "; +"you_can_close_this_ticket_2" = "zakrytí etot tiket"; +"agent_profile_created_1" = "Profilí sozdan"; +"agent_profile_created_2" = "Teperí polízovateli vidjat Vaši psevdonim i avatarku vmesto standartnyh avatarki i nomera."; +"agent_profile_edited" = "Profilí otredaktirovan"; +"agent_profile" = "Kartočka agenta"; + +/* Invite */ + +"invite" = "Priglasití"; +"you_can_invite" = "Vy možete priglasití svoih druzej ili znakomyh v setí s pomosčíju individualínoj ssylki:"; +"you_can_invite_2" = "Priložite etu ssylku k vašemu soobsčeniju. Polízovatelí zaregistriruetsja, i on srazu pojavitsja u vas v druzíjah."; + +/* Banned */ + +"banned_title" = "Vam ban"; +"banned_header" = "Vy byli veriskoknuty"; +"banned_alt" = "Polízovatelí zablokirovan."; +"banned_1" = "Izvinite, $1, no vy byli veriskoknuty."; +"banned_2" = "A pričina etomu prosta: $1."; +"banned_perm" = "K sožaleniju, na etot raz nam prišlosí zablokirovatí vas navsegda."; +"banned_until_time" = "Na etot raz nam prišlosí zablokirovatí vas do $1"; +"banned_3" = "Vy vsjo esčjo možete napisatí v službu podderžki, esli sčitaete čto proizošla ošibka ili vyjti."; +"banned_unban_myself" = "Razmorozití stranicu"; +"banned_unban_title" = "Vaš akkaunt razblokirovan"; +"banned_unban_description" = "Postarajtesí bolíše ne narušatí pravila."; + +/* Registration confirm */ + +"ec_header" = "Podtverždenie registracii"; +"ec_title" = "Spasibo!"; +"ec_1" = "$1, vaša registracija počti zakončena. V tečenie neskolíkih minut na vaš adres E-mail dolžno prijti pisímo so ssylkoj dlja podtverždenija vašego adresa počty."; +"ec_2" = "Esli po kakim-to pričinam vam ne prišlo pisímo, to proveríte papku Spam. Esli pisíma ne okažetsja i tam, to vy možete pereotpravití pisímo."; +"ec_resend" = "Pereotpravití pisímo"; + +/* Messages */ + +"all_messages" = "Vse soobsčenija"; +"search_messages" = "Poisk soobsčenij"; +"no_messages" = "Nikto Vam ne pisal, poka čto."; +"messages_blocked" = "Vy ne možete otpravití soobsčenie etomu polízovatelju, poskolíku on ograničil krug lic, kotorye mogut prisylatí emu soobsčenija."; +"enter_message" = "Vvedite soobsčenie"; +"messages_error_1" = "Soobsčenie ne dostavleno"; +"messages_error_1_description" = "Pri otpravke etogo soobsčenija proizošla ošibka obsčego haraktera..."; + +/* Polls */ +"poll" = "Opros"; +"create_poll" = "Novyj opros"; +"poll_title" = "Tema oprosa"; +"poll_add_option" = "Dobavití variant otveta..."; +"poll_anonymous" = "Anonimnyj opros"; +"poll_multiple" = "Množestvennyj vybor"; +"poll_locked" = "Zapretití otmenjatí svoj golos"; +"poll_edit_expires" = "Golosovanie istekaet čerez: "; +"poll_edit_expires_days" = "dnej"; +"poll_editor_tips" = "Nažatie Backspace v pustom variante privodit k ego udaleniju. Tab/Enter v poslednem dobavljaet novyj."; +"poll_embed" = "Polučití kod"; +"poll_voter_count_zero" = "Budíte pervym, kto progolosuet!"; +"poll_voter_count_one" = "V oprose progolosoval odin čelovek."; +"poll_voter_count_few" = "V oprose progolosovalo $1 čeloveka."; +"poll_voter_count_many" = "V oprose progolosovalo $1 čelovek."; +"poll_voter_count_other" = "V oprose progolosovalo $1 čelovek."; +"poll_voters_list" = "Spisok progolosovavših"; +"poll_anon" = "Anonimnoe golosovanie"; +"poll_public" = "Publičnoe golosovanie"; +"poll_multi" = "mnogo variantov"; +"poll_lock" = "nelízja peregolosovatí"; +"poll_until" = "do $1"; +"poll_err_to_much_options" = "Sliškom mnogo variantov v oprose."; +"poll_err_anonymous" = "Nevozmožno prosmotretí spisok progolosovavših v anonimnom golosovanii."; +"cast_vote" = "Progolosovatí!"; +"retract_vote" = "Otmenití golos"; + +/* Discussions */ + +"discussions" = "Obsuždenija"; +"messages_one" = "Odno soobsčenie"; +"messages_few" = "$1 soobsčenija"; +"messages_many" = "$1 soobsčenij"; +"messages_other" = "$1 soobsčenij"; +"topic_messages_count_zero" = "V teme net soobsčenij"; +"topic_messages_count_one" = "V teme odno soobsčenie"; +"topic_messages_count_few" = "V teme $1 soobsčenija"; +"topic_messages_count_many" = "V teme $1 soobsčenij"; +"topic_messages_count_other" = "V teme $1 soobsčenij"; +"replied" = "otvetil"; +"create_topic" = "Sozdatí temu"; +"new_topic" = "Novaja tema"; +"title" = "Zagolovok"; +"text" = "Tekst"; +"view_topic" = "Prosmotr temy"; +"edit_topic_action" = "Redaktirovatí temu"; +"edit_topic" = "Redaktirovanie temy"; +"topic_settings" = "Nastrojki temy"; +"pin_topic" = "Zakrepití temu"; +"close_topic" = "Zakrytí temu"; +"delete_topic" = "Udalití temu"; +"topics_one" = "Odna tema"; +"topics_few" = "$1 temy"; +"topics_many" = "$1 tema"; +"topics_other" = "$1 tem"; +"created" = "Sozdano"; +"everyone_can_create_topics" = "Vse mogut sozdavatí temy"; +"display_list_of_topics_above_wall" = "Otobražatí spisok tem nad stenoj"; +"topic_changes_saved_comment" = "Obnovljonnyj zagolovok i nastrojki pojavjatsja na stranice s temoj."; +"failed_to_create_topic" = "Ne udalosí sozdatí temu"; +"failed_to_change_topic" = "Ne udalosí izmenití temu"; +"no_title_specified" = "Zagolovok ne ukazan."; + +/* Errors */ + +"error_1" = "Nekorrektnyj zapros"; +"error_2" = "Nevernyj login ili parolí"; +"error_3" = "Ne avtorizovan"; +"error_4" = "Polízovatelí ne susčestvuet"; +"information_-1" = "Operacija vypolnena uspešno"; +"information_-2" = "Vhod vypolnen uspešno"; +"no_data" = "Net dannyh"; +"no_data_description" = "Tut ničego net... Poka..."; +"error" = "Ošibka"; +"error_shorturl" = "Dannyj korotkij adres uže zanjat."; +"error_segmentation" = "Ošibka segmentacii"; +"error_upload_failed" = "Ne udalosí zagruzití foto"; +"error_old_password" = "Staryj parolí ne sovpadaet"; +"error_new_password" = "Novye paroli ne sovpadaet"; +"error_weak_password" = "Nenadjožnyj parolí. Parolí dolžen soderžatí ne menee 8 simvolov, cifry, propisnye i stročnye bukvy"; +"error_shorturl_incorrect" = "Korotkij adres imeet nekorrektnyj format."; +"error_repost_fail" = "Ne udalosí podelitísja zapisíju"; +"error_data_too_big" = "Attribut '$1' ne možet bytí dlinnee $2 $3"; +"forbidden" = "Ošibka dostupa"; +"unknown_error" = "Neizvestnaja ošibka"; +"forbidden_comment" = "Nastrojki privatnosti etogo polízovatelja ne razrešajut vam smotretí na ego stranicu."; +"changes_saved" = "Izmenenija sohraneny"; +"changes_saved_comment" = "Novye dannye pojavjatsja na vašej stranice"; +"photo_saved" = "Fotografija sohranena"; +"photo_saved_comment" = "Novoe izobraženie profilja pojavitsja u vas na stranice"; +"shared_succ" = "Zapisí pojavitsja na vašej stene. Nažmite na uvedomlenie, čtoby perejti k svoej stene."; +"invalid_email_address" = "Nevernyj Email adres"; +"invalid_email_address_comment" = "Email, kotoryj vy vveli, ne javljaetsja korrektnym."; +"invalid_real_name" = "Požalujsta, ispolízujte realínye imena. Tak vašim druzíjam budet legče najti vas."; +"invalid_telegram_name" = "Nevernoe imja Telegram akkaunta"; +"invalid_telegram_name_comment" = "Vy vveli nevernoe imja akkaunta Telegram."; +"invalid_birth_date" = "Nevernaja data roždenija"; +"invalid_birth_date_comment" = "Data roždenija, kotoruju vy vveli, ne javljaetsja korrektnoj."; +"token_manipulation_error" = "Ošibka manipulirovanija tokenom"; +"token_manipulation_error_comment" = "Token nedejstvitelen ili istjok"; +"profile_changed" = "Profilí izmenjon"; +"profile_changed_comment" = "Vaš aktivnyj profilí byl izmenjon."; +"profile_not_found" = "Polízovatelí ne najden."; +"profile_not_found_text" = "Stranica udalena libo esčjo ne sozdana."; +"suspicious_registration_attempt" = "Podozritelínaja popytka registracii"; +"suspicious_registration_attempt_comment" = "Vy pytalisí zaregistrirovatísja iz podozritelínogo mesta."; +"rate_limit_error" = "Ej, sbaví oboroty!"; +"rate_limit_error_comment" = "Daniil Myslivec vam očení nedovolen. V $1 nelízja delatí zaprosy tak často. Kod isključenija: $2."; +"not_enough_permissions" = "Nedostatočno prav"; +"not_enough_permissions_comment" = "U vas nedostatočno prav čtoby vypolnjatí eto dejstvie."; +"login_required_error" = "Nedostatočno prav"; +"login_required_error_comment" = "Čtoby prosmatrivatí etu stranicu, nužno zajti na sajt."; +"captcha_error" = "Nepravilíno vvedeny simvoly"; +"captcha_error_comment" = "Požalujsta, ubeditesí, čto vy pravilíno zapolnili pole s kapčej."; +"failed_to_publish_post" = "Ne udalosí opublikovatí post"; +"failed_to_delete_post" = "Ne udalosí udalití post"; +"media_file_corrupted" = "Fajl mediakontenta povreždjon."; +"media_file_corrupted_or_too_large" = "Fajl mediakontenta povreždjon ili sliškom velik."; +"post_is_empty_or_too_big" = "Post pustoj ili sliškom bolíšoj."; +"post_is_too_big" = "Post sliškom bolíšoj."; +"error_sending_report" = "Ne udalosí podatí žalobu..."; +"error_when_saving_gift" = "Ne udalosí sohranití podarok"; +"error_when_saving_gift_bad_image" = "Izobraženie podarka krivoe."; +"error_when_saving_gift_no_image" = "Požalujsta, zagruzite izobraženie podarka."; +"video_uploads_disabled" = "Zagruzki video otključeny administratorom."; + +"error_when_publishing_comment" = "Ne udalosí opublikovatí kommentarij"; +"error_when_publishing_comment_description" = "Fajl izobraženija povreždjon, sliškom velik ili odna storona izobraženija v razy bolíše drugoj."; +"error_comment_empty" = "Kommentarij pustoj ili sliškom bolíšoj."; +"error_comment_too_big" = "Kommentarij sliškom bolíšoj."; +"error_comment_file_too_big" = "Fajl mediakontenta povreždjon ili sliškom velik."; + +"comment_is_added" = "Kommentarij dobavlen"; +"comment_is_added_desc" = "Vaš kommentarij pojavitsja na stranice."; + +"error_access_denied_short" = "Ošibka dostupa"; +"error_access_denied" = "U vas nedostatočno prav, čtoby redaktirovatí etot resurs"; +"success" = "Uspešno"; +"comment_will_not_appear" = "Etot kommentarij bolíše ne budet pokazyvatsja."; + +"error_when_gifting" = "Ne udalosí podarití"; +"error_user_not_exists" = "Polízovatelí ili nabor ne susčestvujut."; +"error_no_rights_gifts" = "Ne udalosí podtverdití prava na podarok."; +"error_no_more_gifts" = "U vas bolíše ne ostalosí takih podarkov."; +"error_no_money" = "Oru nisč ne puk."; + +"gift_sent" = "Podarok otpravlen"; +"gift_sent_desc" = "Vy otpravili podarok $1 za $2 golosov"; + +"error_on_server_side" = "Proizošla ošibka na storone servera. Obratitesí k sistemnomu administratoru."; +"error_no_group_name" = "Vy ne vveli nazvanie gruppy."; + +"success_action" = "Operacija uspešna"; +"connection_error" = "Ošibka podključenija"; +"connection_error_desc" = "Ne udalosí podključitsja k službe telemetrii."; + +"error_when_uploading_photo" = "Ne udalosí sohranití fotografiju."; + +"new_changes_desc" = "Novye dannye pojavjatsja v vašej gruppe."; +"comment_is_changed" = "Kommentarij k administratoru izmenjon"; +"comment_is_deleted" = "Kommentarij k administratoru udalen"; +"comment_is_too_long" = "Kommentarij sliškom dlinnyj ($1 simvolov vmesto 36 simvolov)"; +"x_no_more_admin" = "$1 bolíše ne administrator."; +"x_is_admin" = "$1 naznačen(a) administratorom."; + +"x_is_now_hidden" = "Teperí $1 budet pokazyvatísja kak obyčnyj podpisčik vsem, krome drugih administratorov"; +"x_is_now_showed" = "Teperí vse budut znatí, čto $1 — administrator."; + +"note_is_deleted" = "Zametka udalena"; +"note_x_is_now_deleted" = "Zametka \"$1\" byla uspešno udalena."; +"new_data_accepted" = "Novye dannye prinjaty."; + +"album_is_deleted" = "Alíbom udaljon"; +"album_x_is_deleted" = "Alíbom $1 byl uspešno udaljon."; + +"error_adding_to_deleted" = "Ne udalosí sohranití fotografiju v DELETED."; +"error_adding_to_x" = "Ne udalosí sohranití fotografiju v $1."; +"no_photo" = "Netu fotografii"; + +"select_file" = "Vyberite fajl"; +"new_description_will_appear" = "Obnovljonnoe opisanie pojavitsja na stranice s fotkoj."; +"photo_is_deleted" = "Fotografija udalena"; +"photo_is_deleted_desc" = "Eta fotografija byla uspešno udalena."; + +"no_video" = "Net videozapisi"; +"no_video_desc" = "Vyberite fajl ili ukažite ssylku."; +"error_occured" = "Proizošla ošibka"; +"error_video_damaged_file" = "Fajl povreždjon ili ne soderžit video."; +"error_video_incorrect_link" = "Vozmožno, ssylka nekorrektna."; +"error_video_no_title" = "Video ne možet bytí opublikovano bez nazvanija."; + +"new_data_video" = "Obnovljonnoe opisanie pojavitsja na stranice s video."; +"error_deleting_video" = "Ne udalosí udalití video"; +"login_please" = "Vy ne vošli v akkaunt."; +"invalid_code" = "Ne udalosí podtverdití nomer telefona: nevernyj kod."; + +"error_max_pinned_clubs" = "Nahoditsja v levom menju mogut maksimum 10 grupp"; +"error_viewing_subs" = "Vy ne možete prosmatrivatí polnyj spisok podpisok $1."; +"error_status_too_long" = "Status sliškom dlinnyj ($1 simvolov vmesto 255 simvolov)"; +"death" = "Smertí..."; +"nehay" = "Nehaj žive!"; +"user_successfully_banned" = "Polízovatelí uspešno zabanen."; + +"content_is_deleted" = "Kontent udaljon, a polízovatelju priletelo predupreždenie."; +"report_is_ignored" = "Žaloba proignorirovana."; +"group_owner_is_banned" = "Sozdatelí soobsčestva uspešno zabanen."; +"group_is_banned" = "Soobsčestvo uspešno zabaneno"; +"description_too_long" = "Opisanie sliškom dlinnoe."; + +/* Admin actions */ + +"login_as" = "Vojti kak $1"; +"manage_user_action" = "Upravlenie polízovatelem"; +"manage_group_action" = "Upravlenie gruppoj"; +"ban_user_action" = "Zablokirovatí polízovatelja"; +"blocks" = "Blokirovki"; +"last_actions" = "Poslednie dejstvija"; +"unban_user_action" = "Razblokirovatí polízovatelja"; +"warn_user_action" = "Predupredití polízovatelja"; +"ban_in_support_user_action" = "Zablokirovatí v podderžke"; +"unban_in_support_user_action" = "Razblokirovatí v podderžke"; +"changes_history" = "Istorija redaktirovanija"; + +/* Admin panel */ + +"admin" = "Admin-panelí"; +"sandbox_for_developers" = "Sandbox dlja razrabotčikov"; +"admin_ownerid" = "ID vladelíca"; +"admin_author" = "Avtor"; +"admin_name" = "Imja"; +"admin_title" = "Nazvanie"; +"admin_description" = "Opisanie"; +"admin_first_known_ip" = "Pervyj IP"; +"admin_shortcode" = "Korotkij adres"; +"admin_verification" = "Verifikacija"; +"admin_banreason" = "Pričina blokirovki"; +"admin_banned" = "zablokirovan"; +"admin_actions" = "Dejstvija"; +"admin_image" = "Izobraženie"; +"admin_image_replace" = "Zamenití izobraženie?"; +"admin_uses" = "Ispolízovanij"; +"admin_uses_reset" = "Sbrosití količestvo ispolízovanij?"; +"admin_limits" = "Ograničenija"; +"admin_limits_reset" = "Sbrosití količestvo ograničenij"; +"admin_open" = "Otkrytí"; +"admin_loginas" = "Vojti kak..."; +"admin_commonsettings" = "Obsčie nastrojki"; +"admin_langsettings" = "Jazyko-zavisimye nastrojki"; +"admin_tab_main" = "Glavnoe"; +"admin_tab_ban" = "Blokirovka"; +"admin_tab_followers" = "Učastniki"; +"admin_overview" = "Obzor"; +"admin_overview_summary" = "Svodka"; +"admin_content" = "Polízovatelískij kontent"; +"admin_user_search" = "Poisk polízovatelej"; +"admin_user_online" = "Onlajn status"; +"admin_user_online_default" = "Po-umolčaniju"; +"admin_user_online_incognito" = "Inkognito"; +"admin_user_online_deceased" = "Pokojnik"; +"admin_club_search" = "Poisk grupp"; +"admin_club_excludeglobalfeed" = "Ne otobražatí zapisi v globalínoj lente"; +"admin_services" = "Platnye uslugi"; +"admin_newgift" = "Novyj podarok"; +"admin_price" = "Cena"; +"admin_giftset" = "Nabor podarkov"; +"admin_giftsets" = "Nabory podarkov"; +"admin_giftsets_none" = "Net naborov podarkov. Sozdajte nabor, čtoby sozdatí podarok."; +"admin_giftsets_create" = "Sozdatí nabor podarkov"; +"admin_giftsets_title" = "Vnutrennee nazvanie nabora, kotoroe budet ispolízovatísja, esli ne udajotsja najti nazvanie na jazyke polízovatelja."; +"admin_giftsets_description" = "Vnutrennee opisanie nabora, kotoroe budet ispolízovatísja, esli ne udajotsja najti nazvanie na jazyke polízovatelja."; +"admin_price_free" = "besplatnyj"; +"admin_voucher_rating" = "Rejting"; +"admin_voucher_serial" = "Serijnyj nomer"; +"admin_voucher_serial_desc" = "Nomer sostoit iz 24 simvolov. Esli format nepravilínyj ili pole ne zapolneno, budet naznačen avtomatičeski."; +"admin_voucher_coins" = "Količestvo golosov"; +"admin_voucher_rating_number" = "Količestvo rejtinga"; +"admin_voucher_usages_desc" = "Količestvo akkauntov, kotorye mogut ispolízovatí vaučer. Esli napisatí -1, budet beskonečností."; +"admin_voucher_status" = "Sostojanie"; +"admin_voucher_status_opened" = "aktiven"; +"admin_voucher_status_closed" = "zakončilsja"; +"admin_settings" = "Nastrojki"; +"admin_settings_tuning" = "Obsčee"; +"admin_settings_appearance" = "Vnešnij vid"; +"admin_settings_security" = "Bezopasností"; +"admin_settings_integrations" = "Integracii"; +"admin_settings_system" = "Sistema"; +"admin_about" = "Ob OpenVK"; +"admin_about_version" = "Versija"; +"admin_about_instance" = "Instancija"; +"admin_commerce_disabled" = "Kommercija otključena sistemnym administratorom"; +"admin_commerce_disabled_desc" = "Nastrojki vaučerov i podarkov budut sohraneny, no ne budut okazyvatí nikakogo vlijanija."; +"admin_banned_links" = "Zablokirovannye ssylki"; +"admin_banned_link" = "Ssylka"; +"admin_banned_domain" = "Domen"; +"admin_banned_link_description" = "S protokolom (https://example.com/)"; +"admin_banned_link_regexp" = "Reguljarnoe vyraženie"; +"admin_banned_link_regexp_description" = "Podstavljaetsja posle domena, ukazannogo vyše. Ne zapolnjajte, esli hotite zablokirovatí vesí domen"; +"admin_banned_link_reason" = "Pričina"; +"admin_banned_link_initiator" = "Iniciator"; +"admin_banned_link_not_specified" = "Ssylka ne ukazana"; +"admin_banned_link_not_found" = "Ssylka ne najdena"; + +"admin_gift_moved_successfully" = "Podarok uspešno peremesčjon"; +"admin_gift_moved_to_recycle" = "Teperí podarok nahoditsja v korzine."; + +"logs" = "Logi"; +"logs_anything" = "Ljuboe"; +"logs_adding" = "Sozdanie"; +"logs_editing" = "Redaktirovanie"; +"logs_removing" = "Udalenie"; +"logs_restoring" = "Vosstanovlenie"; +"logs_added" = "dobavil"; +"logs_edited" = "otredaktiroval"; +"logs_removed" = "udalil"; +"logs_restored" = "vosstanovil"; +"logs_id_post" = "ID zapisi"; +"logs_id_object" = "ID objekta"; +"logs_uuid_user" = "UUID polízovatelja"; +"logs_change_type" = "Tip izmenenija"; +"logs_change_object" = "Tip objekta"; + +"logs_user" = "Polízovatelí"; +"logs_object" = "Objekt"; +"logs_type" = "Tip"; +"logs_changes" = "Izmenenija"; +"logs_time" = "Vremja"; + +"bans_history" = "Istorija blokirovok"; +"bans_history_blocked" = "Zabanennyj"; +"bans_history_initiator" = "Iniciator"; +"bans_history_start" = "Načalo"; +"bans_history_end" = "Konec"; +"bans_history_time" = "Vremja"; +"bans_history_reason" = "Pričina"; +"bans_history_start" = "Načalo"; +"bans_history_removed" = "Snjata"; +"bans_history_active" = "Aktivnaja blokirovka"; + +/* Paginator (deprecated) */ + +"paginator_back" = "Nazad"; +"paginator_page" = "Stranica $1"; +"paginator_next" = "Dalíše"; + +/* About */ + +"about_openvk" = "Ob OpenVK"; +"about_this_instance" = "Ob etoj instancii"; +"rules" = "Pravila"; +"most_popular_groups" = "Samye populjarnye gruppy"; +"on_this_instance_are" = "Na etoj instancii:"; +"about_links" = "Ssylki"; +"instance_links" = "Ssylki instancii:"; +"about_users_one" = "1 polízovatelí"; +"about_users_few" = "$1 polízovatelja"; +"about_users_many" = "$1 polízovatelej"; +"about_users_other" = "$1 polízovatelej"; +"about_online_users_one" = "1 polízovatelí v seti"; +"about_online_users_few" = "$1 polízovatelja v seti"; +"about_online_users_many" = "$1 polízovatelej v seti"; +"about_online_users_other" = "$1 polízovatelej v seti"; +"about_active_users_one" = "1 aktivnyj polízovatelí"; +"about_active_users_few" = "$1 aktivnyh polízovatelja"; +"about_active_users_many" = "$1 aktivnyh polízovatelej"; +"about_active_users_other" = "$1 aktivnyh polízovatelej"; +"about_groups_one" = "1 gruppa"; +"about_groups_few" = "$1 gruppy"; +"about_groups_many" = "$1 grupp"; +"about_groups_other" = "$1 grupp"; +"about_wall_posts_one" = "1 zapisí na stenah"; +"about_wall_posts_few" = "$1 zapisi na stenah"; +"about_wall_posts_many" = "$1 zapisej na stenah"; +"about_wall_posts_other" = "$1 zapisej na stenah"; +"about_watch_rules" = "Smotrite zdesí."; + +/* Dialogs */ + +"ok" = "OK"; +"yes" = "Da"; +"no" = "Net"; +"cancel" = "Otmena"; +"edit_action" = "Izmenití"; +"transfer" = "Peredatí"; +"close" = "Zakrytí"; +"warning" = "Vnimanie"; +"question_confirm" = "Eto dejstvie nelízja otmenití. Vy dejstvitelíno uvereny v tom čto hotite sdelatí?"; +"confirm_m" = "Podtverdití"; +"action_successfully" = "Operacija uspešna"; + +/* User alerts */ + +"user_alert_scam" = "Na etot akkaunt mnogo žalovalisí v svjazi s mošenničestvom. Požalujsta, budíte ostorožny, osobenno esli u vas poprosjat deneg."; +"user_may_not_reply" = "Etot polízovatelí, vozmožno, vam ne smožet otvetití iz-za vaših nastroek privatnosti. Otkrytí nastrojki privatnosti"; + +/* Cookies pop-up */ + +"cookies_popup_content" = "Vse deti ljubjat pečeníe, poetomu etot veb-sajt ispolízuet Cookies dlja togo, čtoby identificirovatí vašu sessiju i ničego bolee. Oznakomítesí s našej politikoj konfidencialínosti dlja polučenija dopolnitelínoj informacii."; +"cookies_popup_agree" = "Soglasen"; + +/* Away */ + +"transition_is_blocked" = "Perehod po ssylke zablokirovan"; +"caution" = "Predupreždenie"; +"url_is_banned" = "Perehod nevozmožen"; +"url_is_banned_comment" = "Administracija $1 ne rekomenduet perehodití po etoj ssylke."; +"url_is_banned_comment_r" = "Administracija $1 ne rekomenduet perehodití po etoj ssylke.
Pričina: $2"; +"url_is_banned_default_reason" = "Ssylka, po kotoroj vy popytalisí perejti, možet vesti na sajt, kotoryj byl sozdan s celíju obmana polízovatelej i polučenija za sčjot etogo pribyli."; +"url_is_banned_title" = "Ssylka na podozritelínyj sajt"; +"url_is_banned_proceed" = "Perejti po ssylke"; + +"recently" = "Nedavno"; + +/* Helpdesk */ +"helpdesk" = "Podderžka"; +"helpdesk_agent" = "Agent Podderžki"; +"helpdesk_agent_card" = "Kartočka agenta"; +"helpdesk_positive_answers" = "položitelínyh otvetov"; +"helpdesk_negative_answers" = "otricatelínyh otvetov"; +"helpdesk_all_answers" = "vsego otvetov"; +"helpdesk_showing_name" = "Otobražaemoe imja"; +"helpdesk_show_number" = "Pokazyvatí nomer"; +"helpdesk_avatar_url" = "Ssylka na avatarku"; +/* Chandler */ + +"c_user_removed_from_group" = "Polízovatelí byl udaljon iz gruppy"; +"c_permission_removed_from_group" = "Pravo bylo udaleno iz gruppy"; +"c_group_removed" = "Gruppa byla udalena."; +"c_groups" = "Gruppy Chandler"; +"c_users" = "Polízovateli Chandler"; +"c_group_permissions" = "Prava"; +"c_group_members" = "Učastniki"; +"c_model" = "Modelí"; +"c_permission" = "Pravo"; +"c_permissions" = "Prava"; +"c_color" = "Cvet"; +"add" = "Dobavití"; +"c_edit_groups" = "Redaktirovatí gruppy"; +"c_user_is_not_in_group" = "Svjazí polízovatelja i gruppy ne najdena."; +"c_permission_not_found" = "Svjazí prava i gruppy ne najdena."; +"c_group_not_found" = "Gruppa ne najdena."; +"c_user_is_already_in_group" = "Etot polízovatelí uže vključjon v etu gruppu."; +"c_add_to_group" = "Dobavití v gruppu"; +"c_remove_from_group" = "Isključití iz gruppy"; + +/* Maintenance */ + +"global_maintenance" = "Tehničeskie raboty"; +"section_maintenance" = "Razdel nedostupen"; +"undergoing_global_maintenance" = "K sožaleniju, sejčas instans zakryt na tehničeskie raboty. My uže rabotaem nad ustraneniem neispravnostej. Požalujsta, poprobujte zajti pozže."; +"undergoing_section_maintenance" = "K sožaleniju, razdel $1 vremenno nedostupen. My uže rabotaem nad ustraneniem neispravnostej. Požalujsta, poprobujte zajti pozže."; +"topics" = "Temy"; +"__transNames" = ""; +"admin_registrationdate" = "Data registracii"; +"gifts_left_zero" = "Ostalosí nolí podarkov"; +"admin_gender" = "Pol"; + +/* Tutorial */ + +"tour_title" = "Ekskursija po sajtu"; +"reg_title" = "Registracija"; +"ifnotlike_title" = " "A esli mne zdesí ne ponravitsja?.." "; +"tour_promo" = "O tom, čto Vas ždet posle registracii"; + +"reg_text" = "Registracija akkaunta absoljutno besplatna i zajmjot ne bolee dvuh minut"; +"ifnotlike_text" = "Vy vsegda možete udalití svoj akkaunt"; + + +"tour_next" = "Dalee →"; +"tour_reg" = "Registracija →"; + + +"tour_section_1" = "Načalo"; +"tour_section_2" = "Profilí"; +"tour_section_3" = "Fotografii"; +"tour_section_4" = "Poisk"; +"tour_section_5" = "Videozapisi"; +"tour_section_6" = "Audiozapisi"; +"tour_section_7" = "Novostnaja lenta"; +"tour_section_8" = "Globalínaja lenta"; +"tour_section_9" = "Gruppy"; +"tour_section_10" = "Sobytija"; +"tour_section_11" = "Temy i dizajn"; +"tour_section_12" = "Kastomizacija"; +"tour_section_13" = "Vaučery"; +"tour_section_14" = "Mobilínaja versija"; + + +"tour_section_1_title_1" = "S čego načatí?"; +"tour_section_1_text_1" = "Registracija akkaunta javljaetsja samym pervym i osnovnym etapom v načale vašego puti na dannom sajte."; +"tour_section_1_text_2" = "Dlja registracii vam potrebuetsja vvesti imja, E-mail i parolí."; +"tour_section_1_text_3" = "Pomnite: Vaš E-mail budet ispolízovatísja v kačestve logina dlja vhoda na sajt. Takže vy imeete polnoe pravo ne ukazyvatí familiju pri registracii. V slučae uteri parolja dlja vhoda na sajt, vospolízujtesí razdelom vosstanovlenija"; +"tour_section_1_bottom_text_1" = "Registrirujasí na sajte, vy soglašaetesí s pravilami sajta i politikoj konfidencialínosti"; + + +"tour_section_2_title_1" = "Vaš profilí"; +"tour_section_2_text_1_1" = "Posle registracii na sajte, vy avtomatičeski popadjote v svoj profilí"; +"tour_section_2_text_1_2" = "Vy možete redaktirovatí ego gde ugodno i v ljuboe vremja, kogda vy sami etogo poželaete."; +"tour_section_2_text_1_3" = "Sovet: Čtoby vaš profilí vygljadel krasivo i prezentabelíno, vy možete ego zapolnití kakoj-libo informaciej ili zagruzití fotografiju, kotoraja podčerknjot, naprimer, vaš glubokij vnutrennij mir."; +"tour_section_2_bottom_text_1" = "Vy edinstvennyj, kto rešaet, skolíko informacii vaši druzíja dolžny uznatí o vas."; +"tour_section_2_title_2" = "Zadajte svoi nastrojki svoej privatnosti"; +"tour_section_2_text_2_1" = "Vy možete opredelití, kto imenno možet imetí dostup k opredelennym tipam informacii, razdelam i vozmožnostjam svjazatísja na vašej stranice."; +"tour_section_2_text_2_2" = "Vy imeete polnoe pravo zakrytí dostup k svoej stranice ot poiskovyh sistem i nezaregistrirovannyh polízovatelej."; +"tour_section_2_text_2_3" = "Pomnite: v budusčem nastrojki privatnosti budut rasširjatísja."; +"tour_section_2_title_3" = "Personalínyj adres stranicy"; +"tour_section_2_text_3_1" = "Posle registracii stranicy, vam vydajotsja personalínyj ID vida @id12345"; +"tour_section_2_text_3_2" = "Standartnyj ID, kotoryj byl polučen posle registracii, izmenití nelízja"; +"tour_section_2_text_3_3" = "No v nastrojkah svoej stranicy vy možete privjazatí svoj personalínyj adres i etot adres možno budet izmenití v ljuboe vremja"; +"tour_section_2_text_3_4" = "Sovet: Možno zanimatí ljuboj svobodnyj adres, dlina kotorogo ne meníše 5 simvolov. Avosí kakoj-nibudí krutoj zajmjote :)"; +"tour_section_2_bottom_text_2" = "Podderživaetsja ustanovka ljubogo korotkogo adresa iz latinskih maleníkih bukv; adres možet soderžatí cifry (ne v načale), točki i nižnie podčjorkivanija (ne v načale ili konce)"; +"tour_section_2_title_4" = "Stena"; + + +"tour_section_3_title_1" = "Delitesí svoimi fotomomentami"; +"tour_section_3_text_1" = "Razdel "Fotografii" dostupen v vašem profile srazu že s momenta registracii."; +"tour_section_3_text_2" = "Vy možete prosmatrivatí fotoalíbomy polízovatelej i sozdavatí svoi sobstvennye."; +"tour_section_3_text_3" = "Dostup ko vsem vašim fotoalíbomam dlja drugih polízovatelej reguliruetsja v nastrojkah privatnosti stranicy."; +"tour_section_3_bottom_text_1" = "Vy možete sozdavatí neograničennoe količestvo fotoalíbomov s vaših putešestvij ili kakih-libo sobytij, ili prosto hranití memy"; + + +"tour_section_4_title_1" = "Poisk"; +"tour_section_4_text_1" = "Razdel "Poisk" pozvoljaet iskatí polízovatelej i gruppy."; +"tour_section_4_text_2" = "Dannyj razdel sajta so vremenem budet ulučšatísja."; +"tour_section_4_text_3" = "Dlja načala poiska nužno znatí imja (ili familiju) polízovatelja; a esli isčete gruppu, to nužno znatí ejo nazvanie."; +"tour_section_4_title_2" = "Bystryj poisk"; +"tour_section_4_text_4" = "Esli vy hotite kak-libo sekonomití vremja, to stroka poiska dostupna i v šapke sajta"; + + +"tour_section_5_title_1" = "Zagružajte i delitesí video so svoimi druzíjami!"; +"tour_section_5_text_1" = "Vy možete zagružatí neograničennoe količestvo videozapisej i klipov"; +"tour_section_5_text_2" = "Razdel "Videozapisi" reguliruetsja nastrojkami privatnosti"; +"tour_section_5_bottom_text_1" = "Video možno zagružatí minuja razdel "Videozapisi" čerez obyčnoe prikreplenie k novoj zapisi na stene:"; +"tour_section_5_title_2" = "Importirovanie video s YouTube"; +"tour_section_5_text_3" = "Krome zagruzki video naprjamuju, sajt podderživaet i vstraivanie video iz YouTube"; + + +"tour_section_6_title_1" = "Audiozapisi, kotoryh poka čto net XD"; +"tour_section_6_text_1" = "Ja byl by očení rad sdelatí tutorial po etomu razdelu, no solnyško Vriska ne sdelala muzyku"; + + +"tour_section_7_title_1" = "Sledite za tem, čto pišut vaši druzíja"; +"tour_section_7_text_1" = "Razdel "Moi Novosti" razdeljaetsja na dva tipa: lokalínaja lenta i globalínaja lenta"; +"tour_section_7_text_2" = "V lokalínoj lente budut pokazyvatísja novosti tolíko vaših druzej i grupp"; +"tour_section_7_bottom_text_1" = "Nikakoj sistemy rekomendacij. Svoju lentu novostej formiruete tolíko vy."; + + +"tour_section_8_title_1" = "Sledite za tem, kakie temy obsuždajut na sajte"; +"tour_section_8_text_1" = "V globalínoj lente novostej budut pokazyvatísja zapisi vseh polízovatelej sajta i grupp"; +"tour_section_8_text_2" = "Prosmotr dannogo razdela možet ne rekomendovatísja dlja čuvstvitelínyh i ranimyh ljudej"; +"tour_section_8_bottom_text_1" = "Dizajn globalínoj lenty po dizajnu nikak ne otličaetsja ot lokalínoj"; +"tour_section_8_bottom_text_2" = "V lente estí množestvo tipov kontenta: načinaja ot obyčnyh foto i video, i zakančivaja anonimnymi postami i oprosami"; + + +"tour_section_9_title_1" = "Sozdavajte gruppy!"; +"tour_section_9_text_1" = "Na sajte uže imejutsja tysjači grupp, posvjasčjonnye različnym temam i kakim-libo fanatskim objedinenijam"; +"tour_section_9_text_2" = "Vy možete prisoedinjatísja k ljuboj gruppe. A esli ne našli podhodjasčuju, to možno sozdavatí i svoju"; +"tour_section_9_text_3" = "Každaja gruppa imeet svoj razdel viki-stranic, fotoalíbomov, blok ssylok i obsuždenij"; +"tour_section_9_title_2" = "Upravljajte svoej gruppoj vmeste s drugom"; +"tour_section_9_text_2_1" = "Upravlenie gruppoj osusčestvljaetsja v razdele "Redaktirovatí gruppu" pod avatarom soobsčestva"; +"tour_section_9_text_2_2" = "Sozdajte komandu administratorov iz obyčnyh učastnikov ili teh, komu vy doverjaete"; +"tour_section_9_text_2_3" = "Vy možete skrytí nužnogo Vam administratora, čtoby on nigde ne pokazyvalsja v predelah vašej gruppy"; +"tour_section_9_bottom_text_1" = "Razdel "Moi Gruppy" nahoditsja v levom menju sajta"; +"tour_section_9_bottom_text_2" = "Primer soobsčestva"; +"tour_section_9_bottom_text_3" = "Gruppy často predstavljajut soboj realínye organizacii, členy kotoryh hotjat ostavatísja na svjazi so svoej auditoriej"; + + +"tour_section_10_title_1" = "Ups"; +"tour_section_10_text_1" = "Ja byl by očení rad sdelatí tutorial po etomu razdelu, no razdel nahoditsja na etape razrabotki. A sejčas my poka etot razdel tutoriala propustim i pojdjom dalíše..."; + + +"tour_section_11_title_1" = "Temy oformlenija"; +"tour_section_11_text_1" = "Posle registracii, v kačestve oformlenija u vas budet ustanovlena standartnaja tema"; +"tour_section_11_text_2" = "Nekotoryh novyh polízovatelej možet slegka otpugnutí nynešnjaja stokovaja tema, kotoraja veet sovsem už drevnostíju"; +"tour_section_11_text_3" = "No ne beda: Vy možete sozdatí svoju temu dlja sajta, oznakomivšisí s dokumentaciej ili vybratí uže susčestvujusčuju iz kataloga"; +"tour_section_11_bottom_text_1" = "Katalog tem dostupen v razdele "Moi Nastrojki" vo vkladke "Interfejs" "; +"tour_section_11_wordart" = "
Ви можете змінити це в налаштуваннях."; +"do_not_attach_note" = "Не прикріплювати нотатку"; +"something" = "Щось"; +"supports_xhtml" = "з (X)HTML підтримується."; + /* Menus */ "edit_button" = "ред."; @@ -402,11 +461,13 @@ "my_friends" = "Мої Друзі"; "my_photos" = "Мої Фотографії"; "my_videos" = "Мої Відеозаписи"; +"audios" = "Мої Аудіозаписи"; "my_messages" = "Мої Повідомлення"; "my_notes" = "Мої Нотатки"; "my_groups" = "Мої Групи"; "my_feed" = "Мої Новини"; "my_feedback" = "Мої Відповіді"; +"my_apps" = "Застосунки"; "my_settings" = "Мої Налаштування"; "bug_tracker" = "Баг-трекер"; @@ -483,13 +544,14 @@ "privacy_setting_add_to_friends" = "Хто може називати мене другом"; "privacy_setting_write_wall" = "Хто може писати у мене на стіні"; "privacy_setting_write_messages" = "Хто може писати мені повідомлення"; +"privacy_setting_view_audio" = "Хто може переглядати мої аудіо"; "privacy_value_anybody" = "Всі охочий"; "privacy_value_anybody_dative" = "Усім охочим"; "privacy_value_users" = "Користувачам OpenVK"; "privacy_value_friends" = "Друзі"; "privacy_value_friends_dative" = "Друзям"; "privacy_value_only_me" = "Тільки я"; -"privacy_value_only_me_dative" = "Тільки мені"; +"privacy_value_only_me_dative" = "Тільки мені та Кирилу Буданову"; "privacy_value_nobody" = "Ніхто"; "your_email_address" = "Адрес Вашої електронної пошти"; "your_page_address" = "Адрес Вашої сторінки"; @@ -581,6 +643,9 @@ "two_factor_authentication_backup_codes_1" = "Резервні коди дозволяють підтверджувати вхід, коли у вас немає доступу до телефону, наприклад, у подорожі."; "two_factor_authentication_backup_codes_2" = "У вас є ще 10 кодів, кожним кодом можна скористатися тільки один раз. Надрукуйте їх, приберіть в надійне місце і використовуйте, коли будуть потрібні коди для підтвердження входу."; "two_factor_authentication_backup_codes_3" = "Ви можете отримати нові коди, якщо вони закінчуються. Дійсні тільки останні створені резервні коди."; +"viewing_backup_codes" = "Перегляд резервних кодів"; +"disable_2fa" = "Вимкнути 2FA"; +"viewing" = "Переглянути"; /* Sorting */ @@ -606,6 +671,134 @@ "videos_many" = "$1 відеозаписів"; "videos_other" = "$1 відеозаписів"; "view_video" = "Перегляд"; +"change_video" = "Редагувати відеозапис"; +"unknown_video" = "ЦЕЙ ВІДЕОЗАПИС НЕ ПІДТРИМУЄТЬСЯ В ЦІЙ ВЕРСІЇ OPENVK."; +"selecting_video" = "Вибір відеозаписів"; +"upload_new_video" = "Завантажити нове відео"; +"max_attached_videos" = "Максимум 10 відеозаписів"; +"max_attached_photos" = "Максимум 10 фотографій"; +"max_attached_audios" = "Максимум 10 аудіо"; +"no_videos" = "Ви не маєте відео."; +"no_videos_results" = "Немає результатів."; + +/* Audios */ + +"audios" = "Аудіозаписи"; +"audio" = "Аудіо"; +"playlist" = "Плейлист"; +"upload_audio" = "Завантажити аудіо"; +"upload_audio_to_group" = "Завантажити аудіо у групу"; + +"performer" = "Виконавець"; +"audio_name" = "Назва"; +"genre" = "Жанр"; +"lyrics" = "Текст"; + +"select_another_file" = "Оберіть інший файл"; + +"limits" = "Ліміти"; +"select_audio" = "Оберіть аудіо з вашого пристрою"; +"audio_requirements" = "Аудіо має тривати від $1s до $2 minutes, важити до $3MB та мати аудіопотік."; +"audio_requirements_2" = "Аудіо не має порушувати авторські права"; +"you_can_also_add_audio_using" = "Ви також можете додати аудіо з числа вже завантажених файлів за допомогою"; +"search_audio_inst" = "пошуку аудіо"; + +"audio_embed_not_found" = "Аудіозапис не знайдено"; +"audio_embed_deleted" = "Аудіозапис видалено"; +"audio_embed_withdrawn" = "Аудіо було вилучено на вимогу правовласника"; +"audio_embed_forbidden" = "Налаштування конфіденційності користувача не дозволяють вбудовувати це аудіо"; +"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" = "Моя музика"; +"music_user" = "Музика користувача"; +"music_club" = "Аудіозапис спільноти"; +"audio_new" = "Новий"; +"audio_popular" = "Популярні"; +"audio_search" = "Пошук"; + +"my_audios_small" = "Мої аудіозаписи"; +"my_playlists" = "Мій плейлист"; +"playlists" = "Плейлист"; +"audios_explicit" = "Має нецензурну лексику"; +"withdrawn" = "Вилучено"; +"deleted" = "Видалено"; +"owner" = "Власник"; +"searchable" = "Доступно у пошуку"; + +"select_audio" = "Оберіть аудіо"; +"no_playlists_thisuser" = "Ви ще не додали жодного плейлиста."; +"no_playlists_user" = "Цей користувач ще не додав жодного плейлиста."; +"no_playlists_club" = "Ця група не додала жодного плейлиста."; + +"no_audios_thisuser" = "Ви ще не додали жодного аудіо."; +"no_audios_user" = "Цей користувач не додав жодного аудіо."; +"no_audios_club" = "Ця група не додала жодного аудіо."; + +"new_playlist" = "Новий плейлист"; +"created_playlist" = "створений"; +"updated_playlist" = "оновлений"; +"bookmark" = "Додати до колекції"; +"unbookmark" = "Видалити з колекції"; +"empty_playlist" = "У цьому списку відтворення немає аудіозаписів."; +"edit_playlist" = "Редагувати список відтворення"; +"unable_to_load_queue" = "Помилка при завантаженні черги."; + +"fully_delete_audio" = "Повністю видалити аудіо"; +"attach_audio" = "Прикріпити аудіо"; +"detach_audio" = "Відкріпити аудіо"; + +"show_more_audios" = "Показати більше аудіо"; +"add_to_playlist" = "Додати до списку відтворення"; +"remove_from_playlist" = "Видалити зі списку відтворення"; +"delete_playlist" = "Видалити плейлист"; +"playlist_cover" = "Обкладинка плейлиста"; + +"playlists_user" = "Плейлисти користувачів"; +"playlists_club" = "Списки відтворення груп"; +"change_cover" = "Змінити обкладинку"; +"playlist_cover" = "Обкладинка плейлиста"; + +"minutes_count_zero" = "триває менше ніж хвилину"; +"minutes_count_one" = "триває одну хвилину"; +"minutes_count_few" = "триває 1 хвилину"; +"minutes_count_many" = "триває $1 хвилину"; +"minutes_count_other" = "триває $1 хвилину"; + +"listens_count_zero" = "жодного прослуховування"; +"listens_count_one" = "одне прослуховування"; +"listens_count_few" = "1 прослуховування"; +"listens_count_many" = "$1 прослуховувань"; +"listens_count_other" = "$1 слухає"; + +"add_audio_to_club" = "Додати аудіо до групи"; +"what_club_add" = "До якої групи ви хочете додати пісню?"; +"group_has_audio" = "Ця група вже має цю пісню."; +"group_hasnt_audio" = "Ця група не має цієї пісні."; + +"by_name" = "за назвою"; +"by_performer" = "за виконавцем"; +"no_access_clubs" = "Немає груп, де ви є адміністратором."; +"audio_successfully_uploaded" = "Аудіо було успішно завантажено і наразі обробляється."; + +"broadcast_audio" = "Трансляція аудіо до статусу"; +"sure_delete_playlist" = "Ви дійсно хочете видалити цей список відтворення?"; +"edit_audio" = "Редагувати аудіо"; +"audios_group" = "Аудіо з групи"; +"playlists_group" = "Списки відтворення з групи"; + +"play_tip" = "Відтворення/пауза"; +"repeat_tip" = "Повтор"; +"shuffle_tip" = "Перемішати"; +"mute_tip" = "Вимкнути звук"; /* Notifications */ @@ -642,6 +835,7 @@ "nt_mention_in_video" = "в обговоренні відеозапису"; "nt_mention_in_note" = "в обговоренні під"; "nt_mention_in_topic" = "в обговоренні"; +"nt_sent_gift" = "відправив вам подарунок"; /* Time */ @@ -738,7 +932,6 @@ /* Apps */ "app" = "Застосунок"; "apps" = "Застосунки"; -"my_apps" = "Мої Застосунки"; "all_apps" = "Всі застосунки"; "installed_apps" = "Мої застосунки"; "own_apps" = "Керування"; @@ -815,6 +1008,7 @@ "support_new" = "Нове звернення"; "support_new_title" = "Введіть тему вашого звернення"; "support_new_content" = "Опишіть проблему чи пропозицію"; +"reports" = "Скарги"; "support_rate_good_answer" = "Це хороша відповідь"; "support_rate_bad_answer" = "Це погана відповідь"; "support_good_answer_user" = "Ви залишили позитивний відгук."; @@ -825,6 +1019,26 @@ "support_rated_bad" = "Ви залишили негативний відгук про відповідь."; "wrong_parameters" = "Неправильні параметри запиту."; "fast_answers" = "Швидкі відповіді"; +"ignore_report" = "Ігнорувати скаргу"; +"report_number" = "Скарга №"; +"list_of_reports" = "Лист скарг"; +"text_of_the_post" = "Текст допису"; +"today" = "сьогодні"; + +"will_be_watched" = "Скоро її розглянуть модератори"; + +"report_question" = "Поскаржитись?"; +"report_question_text" = "Що саме Ви вважаєте неприпустимим у цьому матеріалі?"; +"report_reason" = "Причина скарги"; +"reason" = "Причина"; +"going_to_report_app" = "Ви збираєтеся поскаржитися на цей додаток."; +"going_to_report_club" = "Ви збираєтеся поскаржитися на цю спільноту."; +"going_to_report_photo" = "Ви збираєтеся поскаржитися на цю фотографію."; +"going_to_report_user" = "Ви збираєтеся поскаржитися на цього користувача."; +"going_to_report_video" = "Ви збираєтеся поскаржитися на цей відеозапис."; +"going_to_report_post" = "Ви збираєтеся поскаржитися на цей запис."; +"going_to_report_comment" = "Ви збираєтеся поскаржитися на цей коментар."; + "comment" = "Коментар"; "sender" = "Відправник"; "author" = "Автор"; @@ -834,6 +1048,12 @@ "ticket_changed_comment" = "Зміни набудуть чинності через кілька секунд."; "banned_in_support_1" = "Вибачте, $1, але тепер вам не можна створювати звернення."; "banned_in_support_2" = "Підстава: $1. Цього разу нам довелося забрати у вас цю можливість назавжди."; +"you_can_close_this_ticket_1" = "Якщо ви не маєте запитань, Ви можете "; +"you_can_close_this_ticket_2" = "закрити це звернення"; +"agent_profile_created_1" = "Профіль створено"; +"agent_profile_created_2" = "Тепер користувачі бачать Ваш псевдонім і аватар замість стандартного поличчя та ID."; +"agent_profile_edited" = "Профіль відредагований"; +"agent_profile" = "Картка агента"; /* Invite */ @@ -958,6 +1178,7 @@ "error_repost_fail" = "Не вдалося поділитися записом"; "error_data_too_big" = "Атрибут '$1' не може бути довше $2 $3"; "forbidden" = "Помилка доступу"; +"unknown_error" = "Невідома помилка"; "forbidden_comment" = "Налаштування приватності цього користувача не дозволяють дивитися на його сторінку."; "changes_saved" = "Зміни збережені"; "changes_saved_comment" = "Нові дані з'являться на вашій сторінці"; @@ -993,6 +1214,112 @@ "media_file_corrupted_or_too_large" = "Файл медіаконтенту пошкоджений або файл занадто великий."; "post_is_empty_or_too_big" = "Пост порожній чи надто великий."; "post_is_too_big" = "Пост надто великий."; +"error_sending_report" = "Не вдалося подати скаргу..."; +"error_when_saving_gift" = "Не вдалося зберегти подарунок"; +"error_when_saving_gift_bad_image" = "Зображення подарунка пошкоджене."; +"error_when_saving_gift_no_image" = "Будь ласка, завантажте зображення подарунка."; +"video_uploads_disabled" = "Завантаження відео вимкнено адміністратором."; + +"error_when_publishing_comment" = "Не вдалося опублікувати коментар"; +"error_when_publishing_comment_description" = "Файл зображення пошкоджено, він занадто великий або один бік зображення в рази більший за інший."; +"error_comment_empty" = "Коментар порожній або занадто великий."; +"error_comment_too_big" = "Коментар занадто великий."; +"error_comment_file_too_big" = "Файл медіаконтенту пошкоджений або занадто великий."; + +"comment_is_added" = "Коментар додано"; +"comment_is_added_desc" = "Ваш коментар з'явиться на сторінці."; + +"error_access_denied_short" = "Помилка доступу"; +"error_access_denied" = "Ви не маєте права на редагування цього ресурсу"; +"success" = "Успіх"; +"comment_will_not_appear" = "Цей коментар більше не буде відображатися."; + +"error_when_gifting" = "Не вдалося подарувати"; +"error_user_not_exists" = "Користувач або набір не існують."; +"error_no_rights_gifts" = "Не вдалося підтвердити права на подарунок."; +"error_no_more_gifts" = "У вас більше не залишилось цих подарунків."; +"error_no_money" = "АХАХАХА ЛОШАРА ПІЗДУЙ НА ЗАРОБІТКИ У ПОЛЬЩУ"; +/* трррр шкібіді доп доп доп доп єс єс єс) */ + +"description_too_long" = "Опис надто довгий."; + +"invalid_audio" = "Пошкоджене аудіо."; +"do_not_have_audio" = "У вас немає цього аудіо"; +"do_have_audio" = "Ви вже маєте цей аудіозапис."; + +"set_playlist_name" = "Введіть назву плейліста."; +"playlist_already_bookmarked" = "Цей плейліст вже є у вашій збірці."; +"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 не встановлено на цьому сервері."; + +"gift_sent" = "Подарунок відправлено"; +"gift_sent_desc" = "Ви відправили $1 за $2 голосів"; + +"error_on_server_side" = "Виникла помилка на боці сервера. Зверніться до системного адміністратора."; +"error_no_group_name" = "Ви не ввели назву групи."; + +"success_action" = "Операція пройшла успішно"; +"connection_error" = "Помилка з'єднання"; +"connection_error_desc" = "Не вдалося з'єднатися до служби телеметрії"; + +"error_when_uploading_photo" = "Не вдалося зберегти фотографію."; + +"new_changes_desc" = "Нові дані з'являться у вашій групі."; +"comment_is_changed" = "Коментар до адміністратора змінено"; +"comment_is_deleted" = "Коментар до адміністратора видалено"; +"comment_is_too_long" = "Коментар надто довгий ($1 символів замість 36 символів)"; +"x_no_more_admin" = "$1 більше не є адміністратором."; +"x_is_admin" = "$1 призначено адміністратором."; + +"x_is_now_hidden" = "Тепер $1 буде відображатися як звичайний підписник усім, окрім інших адміністраторів"; +"x_is_now_showed" = "Тепер $1 буде відображатися як звичайний адміністратор."; + +"note_is_deleted" = "Нотатка видалена"; +"note_x_is_now_deleted" = "Нотатка \"$1\" була успішно видалена."; +"new_data_accepted" = "Нові дані прийняті."; + +"album_is_deleted" = "Альбом видалено"; +"album_x_is_deleted" = "Альбом $1 було видалено."; + +"error_adding_to_deleted" = "Не вдалося зберегти фотографію у DELETED."; +"error_adding_to_x" = "Не вдалося зберегти фотографію в $1."; +"no_photo" = "Нема фотографій"; + +"select_file" = "Оберіть файл"; +"new_description_will_appear" = "Оновлений опис з'явиться на сторінці з фото."; +"photo_is_deleted" = "Фотографія видалена"; +"photo_is_deleted_desc" = "Ця світлина була успішно видалена."; + +"no_video" = "Немає відеозапису"; +"no_video_desc" = "Оберіть файл або вкажіть URL."; +"error_occured" = "Виникла помилка"; +"error_video_damaged_file" = "Файл пошкоджений або не має відеозапису."; +"error_video_incorrect_link" = "Вірогідно, посилання некоректне."; +"error_video_no_title" = "Відео не може бути опубліковано без назви."; + +"new_data_video" = "Оновлений опис з'явиться на сторінці з відео."; +"error_deleting_video" = "Не вдалося видалити відео"; +"login_please" = "Ви не увійшли в аккаунт."; +"invalid_code" = "Не вдалося підтвердити номер телефону: неправильний код."; + +"error_max_pinned_clubs" = "Знаходитись у лівому меню можуть максимум 10 спільнот"; +"error_viewing_subs" = "Ви не можете переглядати лист підписок $1."; +"error_status_too_long" = "Статус надто довгий ($1 символів замість 255 символів)"; +"death" = "Сміерць..."; +"nehay" = "Няхай жыве!"; +"user_successfully_banned" = "Користувача успішно заблоковано."; + +"content_is_deleted" = "Коментар видалено, а користувач отримав попередження."; +"report_is_ignored" = "Скаргу проігноровано."; +"group_owner_is_banned" = "Творець спільноти успішно заблоковано."; +"group_is_banned" = "Спільноту успішно заблоковано"; /* Admin actions */ @@ -1000,14 +1327,18 @@ "manage_user_action" = "Керування користувачем"; "manage_group_action" = "Керування групою"; "ban_user_action" = "Заблокувати користувача"; +"blocks" = "Блокування"; +"last_actions" = "Останні дії"; "unban_user_action" = "Розблокувати користувача"; "warn_user_action" = "Попередити користувача"; "ban_in_support_user_action" = "Заблокувати у тех.підтримці"; "unban_in_support_user_action" = "Розблокувати у тех.підтримці"; +"changes_history" = "Історія редагування"; /* Admin panel */ "admin" = "Адмін панель"; +"sandbox_for_developers" = "Sandbox для розробників"; "admin_ownerid" = "ID власника"; "admin_author" = "Автор"; "admin_name" = "Ім'я"; @@ -1082,6 +1413,44 @@ "admin_banned_link_initiator" = "Ініціатор"; "admin_banned_link_not_specified" = "Посилання не зазначено"; "admin_banned_link_not_found" = "Посилання не знайдено"; +"admin_gift_moved_successfully" = "Подарунок успішно переміщено"; +"admin_gift_moved_to_recycle" = "Тепер подарунок у кошику."; +"admin_original_file" = "Оригінальний файл"; +"admin_audio_length" = "Довжина"; +"admin_cover_id" = "Обкладинка (ідентифікатор фото)"; +"admin_music" = "Музика"; +"logs" = "Логи"; +"logs_anything" = "Будь-яке"; +"logs_adding" = "Створення"; +"logs_editing" = "Редагування"; +"logs_removing" = "Видалення"; +"logs_restoring" = "Відновлення"; +"logs_added" = "додав"; +"logs_edited" = "відредагував"; +"logs_removed" = "видалив"; +"logs_restored" = "відновив"; +"logs_id_post" = "ID допису"; +"logs_id_object" = "ID об'єкту"; +"logs_uuid_user" = "UUID користувача"; +"logs_change_type" = "Тип зміни"; +"logs_change_object" = "Тип об'єкта"; + +"logs_user" = "Користувач"; +"logs_object" = "Об'єкт"; +"logs_type" = "Тип"; +"logs_changes" = "Зміни"; +"logs_time" = "Час"; + +"bans_history" = "Історія блокувань"; +"bans_history_blocked" = "Заблоковано"; +"bans_history_initiator" = "Ініціатор"; +"bans_history_start" = "Початок"; +"bans_history_end" = "Кінець"; +"bans_history_time" = "Час"; +"bans_history_reason" = "Причина"; +"bans_history_start" = "Початок"; +"bans_history_removed" = "Знята"; +"bans_history_active" = "Активне блокування"; /* Paginator (deprecated) */ @@ -1130,7 +1499,9 @@ "transfer" = "Передати"; "close" = "Закрити"; "warning" = "Увага"; -"question_confirm" = "Цю дію не можна скасувати. Ви дійсно впевнені, що хочете зробити?"; +"question_confirm" = "Цю дію не можна скасувати. Ви переконані що хочете це зробити?"; +"confirm_m" = "Підтвердити"; +"action_successfully" = "Операція виконана успішно"; /* User alerts */ @@ -1144,6 +1515,8 @@ /* Away */ +"transition_is_blocked" = "Перехід за посиланням заборонений"; +"caution" = "Попередження"; "url_is_banned" = "Перехід неможливий"; "url_is_banned_comment" = "Адміністрація $1 не рекомендує переходити за цим посиланням."; "url_is_banned_comment_r" = "Адміністрація $1 не рекомендує переходити за цим посиланням.
Підстава: $2"; @@ -1151,6 +1524,8 @@ "url_is_banned_title" = "Посилання на підозрілий сайт"; "url_is_banned_proceed" = "Перейти за посиланням"; +"recently" = "Нещодавно"; + /* Chandler */ "c_user_removed_from_group" = "Користувача було видалено з групи"; @@ -1402,6 +1777,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" = "За датою"; @@ -1422,11 +1799,49 @@ "closed_group_post" = "Цей допис з приватної групи"; "deleted_target_comment" = "Цей коментар належить до видаленого допису"; +"no_results" = "Немає результатів"; + +/* BadBrowser */ + +"deprecated_browser" = "Застарілий браузер"; +"deprecated_browser_description" = "Для перегляду цього контенту вам необхідний >Firefox ESR 52 або еквівалент по функціоналу навігатор по всесвітньою мережею інтернет. Співчуваємо про це."; + +/* Statistics */ + +"coverage" = "Обхват"; +"coverage_this_week" = "Цей графік відображає обхват за останні 7 днів."; +"views" = "Перегляди"; +"views_this_week" = "Цей графік відображає перегляди дописів спільноти за остані 7 днів."; + +"full_coverage" = "Повний обхват"; +"all_views" = "Усі перегляди"; + +"subs_coverage" = "обхват підписників"; +"subs_views" = "Перегляди підписників"; + +"viral_coverage" = "Віральний обхват"; +"viral_views" = "Віральні перегляди"; + +/* Sudo */ + +"you_entered_as" = "Ви увійшли як"; +"please_rights" = "наполегливо просимо, шануйте право на таємницю листування інших людей та не зловживайте підміною користувача."; +"click_on" = "Натисніть"; +"there" = "тут"; +"to_leave" = "щоб вийти"; + +/* Phone number */ + +"verify_phone_number" = "Підтвердити номер телефону"; +"we_sended_first" = "Ми надіслали SMS з кодом на номер"; +"we_sended_end" = "уведіть його сюди"; + /* Mobile */ "mobile_friends" = "Друзі"; "mobile_photos" = "Фотографії"; "mobile_videos" = "Відеозаписи"; +"mobile_audios" = "Аудіо"; "mobile_messages" = "Повідомлення"; "mobile_notes" = "Нотатки"; "mobile_groups" = "Групи"; @@ -1438,3 +1853,42 @@ "mobile_like" = "Подобається"; "mobile_user_info_hide" = "Приховувати"; "mobile_user_info_show_details" = "Показати докладніше"; +"my" = "Мої"; +"введіть_назву_або_виконавця" = "Введіть назву або виконавця..."; + +/* Moderation */ + +"section" = "Розділ"; +"template_ban" = "Блокування за шаблоном"; +"active_templates" = "Чинні шаблони"; +"users_reports" = "Скарги користувачів"; +"substring" = "Підрядок"; +"n_user" = "Користувач"; +"time_before" = "Годину раніше, ніж"; +"time_after" = "Годиною пізніше, ніж"; +"where_for_search" = "WHERE для пошуку по розділу"; +"block_params" = "Параметри блокувань"; +"only_rollback" = "Тільки відкат"; +"only_block" = "Тільки блокування"; +"rollback_and_block" = "Відкат та блокування"; +"subm" = "Застосувати"; + +"select_section_for_start" = "Для початку роботи, оберіть розділ"; +"results_will_be_there" = "Тут будуть відображатися результати пошуку"; +"search_results" = "Результати пошуку"; +"cnt" = "шт."; + +"link_to_page" = "Посилання на сторінку"; +"or_subnet" = "або підмережа"; +"error_when_searching" = "Помилка при виконанні запиту"; +"no_found" = "Нічого не знайдено"; +"operation_successfully" = "Операцію успішно виконано"; + +"unknown_error" = "Невідома помилка"; +"templates" = "Шаблони"; +"type" = "Тип"; +"count" = "Кількість"; +"time" = "Час"; + +"roll_back" = "відкотити"; +"roll_backed" = "відкачано"; diff --git a/openvk-example.yml b/openvk-example.yml index 4b45e7ba..308610e2 100644 --- a/openvk-example.yml +++ b/openvk-example.yml @@ -50,6 +50,8 @@ openvk: - "Good luck filling! If you are a regular support agent, inform the administrator that he forgot to fill the config" messages: strict: false + music: + exposeOriginalURLs: true wall: christian: false anonymousPosting: diff --git a/themepacks/midnight/stylesheet.css b/themepacks/midnight/stylesheet.css index 634917c6..73030b40 100644 --- a/themepacks/midnight/stylesheet.css +++ b/themepacks/midnight/stylesheet.css @@ -235,10 +235,133 @@ input[type="radio"] { } .searchList #used { - background: linear-gradient(#453e5e,#473f61); + background: #463f60 !important; } #backdropEditor { background-image: url("/themepack/midnight/0.0.2.8/resource/backdrop-editor.gif") !important; border-color: #473e66 !important; -} \ No newline at end of file +} + +.bigPlayer { + background-color: rgb(30, 26, 43) !important; +} + +.bigPlayer .selectableTrack, .audioEmbed .track > .selectableTrack { + border-top: #b9b9b9 1px solid !important; +} + +.bigPlayer .paddingLayer .slider, .audioEmbed .track .slider { + background: #b9b9b9 !important; +} + +.musicIcon { + filter: invert(81%) !important; +} + +.audioEntry.nowPlaying { + background: #463f60 !important; + border: 1px solid #645a86 !important; +} + +.preformer { + color: #b7b7b7 !important; +} + +.bigPlayer .paddingLayer .trackPanel .track .timeTip { + background: #b9b9b9 !important; + color: black !important; +} + +.audioEntry.nowPlaying:hover { + background: #50486f !important; +} + +.audioEntry:hover { + background: #19142D !important; +} + +.audioEntry .performer a { + color: #a2a1a1 !important; +} + +.musicIcon.lagged { + opacity: 49%; +} + +.bigPlayer .paddingLayer .bigPlayerTip { + color: black !important; +} + +.searchList a { + color: #bbb !important; +} + +.searchList a:hover { + color: #eeeeee !important; + background: #332d46 !important; +} + +.friendsAudiosList .elem:hover { + background: #332d46 !important; +} + +.audioEntry .playerButton .playIcon { + filter: invert(81%); +} + +img[src$='/assets/packages/static/openvk/img/camera_200.png'], img[src$='/assets/packages/static/openvk/img/song.jpg'] { + filter: invert(100%); +} + +.audioStatus { + color: #8E8E8E !important; +} + +.audioEntry .withLyrics { + color: #6f6497 !important; +} + +#listensCount { + color: unset !important; +} + +#upload_container, .whiteBox { + background: #1d1928 !important; + border: 1px solid #383052 !important; +} + +ul { + color: #8b9ab5 !important; +} + +#audio_upload { + border: 2px solid #383052 !important; + background-color: #262133 !important; +} + +/* вот бы css в овк был бы написан на var()'ах( */ +#upload_container.uploading { + background: #121017 url('/assets/packages/static/openvk/img/progressbar.gif') !important; +} + +.musicIcon.pressed { + opacity: 41% !important; +} + +.ovk-diag-body .searchBox { + background: #1e1a2b !important; +} + +.audioEntry.nowPlaying .title { + color: #fff !important; +} + +.attachAudio:hover { + background: #19142D !important; + cursor: pointer; +} + +.showMore, .showMoreAudiosPlaylist { + background: #181826 !important; +} diff --git a/themepacks/openvk_modern/stylesheet.css b/themepacks/openvk_modern/stylesheet.css index 178e8094..841e92ec 100644 --- a/themepacks/openvk_modern/stylesheet.css +++ b/themepacks/openvk_modern/stylesheet.css @@ -279,18 +279,9 @@ input[type=checkbox] { box-shadow: none; } -.searchList #used -{ - margin-left:0px; - color: white; - padding: 2px; - padding-top: 5px; - padding-bottom: 5px; - border: none; - background: #a4a4a4; - margin-bottom: 2px; - padding-left: 5px; - width: 90%; +.searchList #used { + background: #3c3c3c !important; + border: unset !important; } .searchList #used a @@ -337,3 +328,35 @@ input[type=checkbox] { { border-top: 1px solid #2f2f2f; } + +.musicIcon { + filter: contrast(202%) !important; +} + +.audioEntry .playerButton .playIcon { + filter: contrast(7) !important; +} + +.audioEmbed .track > .selectableTrack, .bigPlayer .selectableTrack { + border-top: #404040 1px solid !important; +} + +.bigPlayer .paddingLayer .slider, .audioEmbed .track .slider { + background: #3c3c3c !important; +} + +.audioEntry.nowPlaying { + background: #4b4b4b !important; +} + +.audioEntry.nowPlaying:hover { + background: #373737 !important; +} + +.musicIcon.pressed { + filter: brightness(150%) !important; +} + +.musicIcon.lagged { + opacity: 50%; +}