Videos: add window player (#951)

* De#910fy

* Fiksez

* newlines

---------

Co-authored-by: Dmitry Tretyakov <76806170+tretdm@users.noreply.github.com>
This commit is contained in:
lalka2018 2023-11-15 11:41:18 +03:00 committed by GitHub
parent 9fbf7f5bf5
commit 0f0d3ee950
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1208 additions and 125 deletions

156
ServiceAPI/Video.php Normal file
View file

@ -0,0 +1,156 @@
<?php declare(strict_types=1);
namespace openvk\ServiceAPI;
use openvk\Web\Models\Entities\{User, Post};
use openvk\Web\Models\Repositories\{Videos, Comments, Clubs};
use Chandler\MVC\Routing\Router;
class Video implements Handler
{
protected $user;
protected $videos;
protected $comments;
protected $groups;
function __construct(?User $user)
{
$this->user = $user;
$this->videos = new Videos;
$this->comments = new Comments;
$this->groups = new Clubs;
}
function getVideo(int $id, callable $resolve, callable $reject)
{
$video = $this->videos->get($id);
if(!$video || $video->isDeleted()) {
$reject(2, "Video does not exists");
}
if(method_exists($video, "canBeViewedBy") && !$video->canBeViewedBy($this->user)) {
$reject(4, "Access to video denied");
}
if(!$video->getOwner()->getPrivacyPermission('videos.read', $this->user)) {
$reject(8, "Access to video denied: this user chose to hide his videos");
}
$prevVideo = NULL;
$nextVideo = NULL;
$lastVideo = $this->videos->getLastVideo($video->getOwner());
if($video->getVirtualId() - 1 != 0) {
for($i = $video->getVirtualId(); $i != 0; $i--) {
$maybeVideo = (new Videos)->getByOwnerAndVID($video->getOwner()->getId(), $i);
if(!is_null($maybeVideo) && !$maybeVideo->isDeleted() && $maybeVideo->getId() != $video->getId()) {
if(method_exists($maybeVideo, "canBeViewedBy") && !$maybeVideo->canBeViewedBy($this->user)) {
continue;
}
$prevVideo = $maybeVideo;
break;
}
}
}
if(is_null($lastVideo) || $lastVideo->getId() == $video->getId()) {
$nextVideo = NULL;
} else {
for($i = $video->getVirtualId(); $i <= $lastVideo->getVirtualId(); $i++) {
$maybeVideo = (new Videos)->getByOwnerAndVID($video->getOwner()->getId(), $i);
if(!is_null($maybeVideo) && !$maybeVideo->isDeleted() && $maybeVideo->getId() != $video->getId()) {
if(method_exists($maybeVideo, "canBeViewedBy") && !$maybeVideo->canBeViewedBy($this->user)) {
continue;
}
$nextVideo = $maybeVideo;
break;
}
}
}
$res = [
"id" => $video->getId(),
"title" => $video->getName(),
"owner" => $video->getOwner()->getId(),
"commentsCount" => $video->getCommentsCount(),
"description" => $video->getDescription(),
"type" => $video->getType(),
"name" => $video->getOwner()->getCanonicalName(),
"pretty_id" => $video->getPrettyId(),
"virtual_id" => $video->getVirtualId(),
"published" => (string)$video->getPublicationTime(),
"likes" => $video->getLikesCount(),
"has_like" => $video->hasLikeFrom($this->user),
"author" => $video->getOwner()->getCanonicalName(),
"canBeEdited" => $video->getOwner()->getId() == $this->user->getId(),
"isProcessing" => $video->getType() == 0 && $video->getURL() == "/assets/packages/static/openvk/video/rendering.mp4",
"prevVideo" => !is_null($prevVideo) ? $prevVideo->getId() : null,
"nextVideo" => !is_null($nextVideo) ? $nextVideo->getId() : null,
];
if($video->getType() == 1) {
$res["embed"] = $video->getVideoDriver()->getEmbed();
} else {
$res["url"] = $video->getURL();
}
$resolve($res);
}
function shareVideo(int $owner, int $vid, int $type, string $message, int $club, bool $signed, bool $asGroup, callable $resolve, callable $reject)
{
$video = $this->videos->getByOwnerAndVID($owner, $vid);
if(!$video || $video->isDeleted()) {
$reject(16, "Video does not exists");
}
if(method_exists($video, "canBeViewedBy") && !$video->canBeViewedBy($this->user)) {
$reject(32, "Access to video denied");
}
if(!$video->getOwner()->getPrivacyPermission('videos.read', $this->user)) {
$reject(8, "Access to video denied: this user chose to hide his videos");
}
$flags = 0;
$nPost = new Post;
$nPost->setOwner($this->user->getId());
if($type == 0) {
$nPost->setWall($this->user->getId());
} else {
$club = $this->groups->get($club);
if(!$club || $club->isDeleted() || !$club->canBeModifiedBy($this->user)) {
$reject(64, "Can't do repost to this club");
}
if($asGroup)
$flags |= 0b10000000;
if($signed)
$flags |= 0b01000000;
$nPost->setWall($club->getId() * -1);
}
$nPost->setContent($message);
$nPost->setFlags($flags);
$nPost->save();
$nPost->attach($video);
$res = [
"id" => $nPost->getId(),
"pretty_id" => $nPost->getPrettyId(),
];
$resolve($res);
}
}

View file

@ -1,72 +1,157 @@
<?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers;
use openvk\Web\Models\Repositories\Users as UsersRepo;
use openvk\Web\Models\Repositories\{Posts as PostsRepo, Comments as CommentsRepo, Photos as PhotosRepo, Videos as VideosRepo};
use openvk\Web\Models\Repositories\Posts as PostsRepo;
use openvk\Web\Models\Repositories\Comments as CommentsRepo;
use openvk\Web\Models\Repositories\Videos as VideosRepo;
use openvk\Web\Models\Repositories\Photos as PhotosRepo;
use openvk\Web\Models\Repositories\Notes as NotesRepo;
final class Likes extends VKAPIRequestHandler
{
function add(string $type, int $owner_id, int $item_id): object
{
$this->requireUser();
function add(string $type, int $owner_id, int $item_id): object
{
$this->requireUser();
$this->willExecuteWriteAction();
$postable = NULL;
switch($type) {
case "post":
$post = (new PostsRepo)->getPostById($owner_id, $item_id);
if(is_null($post))
$this->fail(100, "One of the parameters specified was missing or invalid: object not found");
$post->setLike(true, $this->getUser());
return (object) [
"likes" => $post->getLikesCount()
];
$postable = $post;
break;
case "comment":
$comment = (new CommentsRepo)->get($item_id);
$postable = $comment;
break;
case "video":
$video = (new VideosRepo)->getByOwnerAndVID($owner_id, $item_id);
$postable = $video;
break;
case "photo":
$photo = (new PhotosRepo)->getByOwnerAndVID($owner_id, $item_id);
$postable = $photo;
break;
case "note":
$note = (new NotesRepo)->getNoteById($owner_id, $item_id);
$postable = $note;
break;
default:
$this->fail(100, "One of the parameters specified was missing or invalid: incorrect type");
}
}
function delete(string $type, int $owner_id, int $item_id): object
{
$this->requireUser();
if(is_null($postable) || $postable->isDeleted())
$this->fail(100, "One of the parameters specified was missing or invalid: object not found");
if(method_exists($postable, "canBeViewedBy") && !$postable->canBeViewedBy($this->getUser() ?? NULL)) {
$this->fail(2, "Access to postable denied");
}
$postable->setLike(true, $this->getUser());
return (object) [
"likes" => $postable->getLikesCount()
];
}
function delete(string $type, int $owner_id, int $item_id): object
{
$this->requireUser();
$this->willExecuteWriteAction();
$postable = NULL;
switch($type) {
case "post":
$post = (new PostsRepo)->getPostById($owner_id, $item_id);
if (is_null($post))
$this->fail(100, "One of the parameters specified was missing or invalid: object not found");
$post->setLike(false, $this->getUser());
return (object) [
"likes" => $post->getLikesCount()
];
$postable = $post;
break;
case "comment":
$comment = (new CommentsRepo)->get($item_id);
$postable = $comment;
break;
case "video":
$video = (new VideosRepo)->getByOwnerAndVID($owner_id, $item_id);
$postable = $video;
break;
case "photo":
$photo = (new PhotosRepo)->getByOwnerAndVID($owner_id, $item_id);
$postable = $photo;
break;
case "note":
$note = (new NotesRepo)->getNoteById($owner_id, $item_id);
$postable = $note;
break;
default:
$this->fail(100, "One of the parameters specified was missing or invalid: incorrect type");
}
}
if(is_null($postable) || $postable->isDeleted())
$this->fail(100, "One of the parameters specified was missing or invalid: object not found");
if(method_exists($postable, "canBeViewedBy") && !$postable->canBeViewedBy($this->getUser() ?? NULL)) {
$this->fail(2, "Access to postable denied");
}
if(!is_null($postable)) {
$postable->setLike(false, $this->getUser());
return (object) [
"likes" => $postable->getLikesCount()
];
}
}
function isLiked(int $user_id, string $type, int $owner_id, int $item_id): object
{
$this->requireUser();
{
$this->requireUser();
$user = (new UsersRepo)->get($user_id);
if(is_null($user) || $user->isDeleted())
$this->fail(100, "One of the parameters specified was missing or invalid: user not found");
if(method_exists($user, "canBeViewedBy") && !$user->canBeViewedBy($this->getUser())) {
$this->fail(1984, "Access denied: you can't see this user");
}
$postable = NULL;
switch($type) {
case "post":
$user = (new UsersRepo)->get($user_id);
if (is_null($user))
$this->fail(100, "One of the parameters specified was missing or invalid: user not found");
$post = (new PostsRepo)->getPostById($owner_id, $item_id);
if (is_null($post))
$this->fail(100, "One of the parameters specified was missing or invalid: object not found");
return (object) [
"liked" => (int) $post->hasLikeFrom($user),
"copied" => 0 # TODO: handle this
];
$postable = $post;
break;
case "comment":
$comment = (new CommentsRepo)->get($item_id);
$postable = $comment;
break;
case "video":
$video = (new VideosRepo)->getByOwnerAndVID($owner_id, $item_id);
$postable = $video;
break;
case "photo":
$photo = (new PhotosRepo)->getByOwnerAndVID($owner_id, $item_id);
$postable = $photo;
break;
case "note":
$note = (new NotesRepo)->getNoteById($owner_id, $item_id);
$postable = $note;
break;
default:
$this->fail(100, "One of the parameters specified was missing or invalid: incorrect type");
}
if(is_null($postable) || $postable->isDeleted())
$this->fail(100, "One of the parameters specified was missing or invalid: object not found");
if(!$postable->canBeViewedBy($this->getUser())) {
$this->fail(665, "Access to postable denied");
}
return (object) [
"liked" => (int) $postable->hasLikeFrom($user),
"copied" => 0
];
}
function getList(string $type, int $owner_id, int $item_id, bool $extended = false, int $offset = 0, int $count = 10, bool $skip_own = false)

View file

@ -26,7 +26,7 @@ final class Video extends VKAPIRequestHandler
$video = (new VideosRepo)->getByOwnerAndVID(intval($id[0]), intval($id[1]));
if($video) {
$items[] = $video->getApiStructure();
$items[] = $video->getApiStructure($this->getUser());
}
}
@ -38,14 +38,18 @@ final class Video extends VKAPIRequestHandler
if ($owner_id > 0)
$user = (new UsersRepo)->get($owner_id);
else
$this->fail(1, "Not implemented");
$this->fail(1, "Not implemented");
if(!$user->getPrivacyPermission('videos.read', $this->getUser())) {
$this->fail(20, "Access denied: this user chose to hide his videos");
}
$videos = (new VideosRepo)->getByUser($user, $offset + 1, $count);
$videosCount = (new VideosRepo)->getUserVideosCount($user);
$items = [];
foreach ($videos as $video) {
$items[] = $video->getApiStructure();
$items[] = $video->getApiStructure($this->getUser());
}
return (object) [

View file

@ -57,7 +57,7 @@ final class Wall extends VKAPIRequestHandler
} else if($attachment instanceof \openvk\Web\Models\Entities\Poll) {
$attachments[] = $this->getApiPoll($attachment, $this->getUser());
} else if ($attachment instanceof \openvk\Web\Models\Entities\Video) {
$attachments[] = $attachment->getApiStructure();
$attachments[] = $attachment->getApiStructure($this->getUser());
} else if ($attachment instanceof \openvk\Web\Models\Entities\Note) {
$attachments[] = $attachment->toVkApiStruct();
} else if ($attachment instanceof \openvk\Web\Models\Entities\Audio) {
@ -237,7 +237,7 @@ final class Wall extends VKAPIRequestHandler
} else if($attachment instanceof \openvk\Web\Models\Entities\Poll) {
$attachments[] = $this->getApiPoll($attachment, $user);
} else if ($attachment instanceof \openvk\Web\Models\Entities\Video) {
$attachments[] = $attachment->getApiStructure();
$attachments[] = $attachment->getApiStructure($this->getUser());
} else if ($attachment instanceof \openvk\Web\Models\Entities\Note) {
$attachments[] = $attachment->toVkApiStruct();
} else if ($attachment instanceof \openvk\Web\Models\Entities\Audio) {

View file

@ -75,7 +75,11 @@ class Comment extends Post
if($attachment->isDeleted())
continue;
$res->attachments[] = $attachment->toVkApiStruct();
if($attachment instanceof \openvk\Web\Models\Entities\Photo) {
$res->attachments[] = $attachment->toVkApiStruct();
} else if($attachment instanceof \openvk\Web\Models\Entities\Video) {
$res->attachments[] = $attachment->toVkApiStruct($this->getUser());
}
}
if($need_likes) {

View file

@ -131,10 +131,15 @@ abstract class Postable extends Attachable
"target" => $this->getRecord()->id,
];
if($liked)
DB::i()->getContext()->table("likes")->insert($searchData);
else
DB::i()->getContext()->table("likes")->where($searchData)->delete();
if($liked) {
if(!$this->hasLikeFrom($user)) {
DB::i()->getContext()->table("likes")->insert($searchData);
}
} else {
if($this->hasLikeFrom($user)) {
DB::i()->getContext()->table("likes")->where($searchData)->delete();
}
}
}
function hasLikeFrom(User $user): bool

View file

@ -115,15 +115,15 @@ class Video extends Media
return $this->getRecord()->owner;
}
function getApiStructure(): object
function getApiStructure(?User $user = NULL): object
{
$fromYoutube = $this->getType() == Video::TYPE_EMBED;
return (object)[
$res = (object)[
"type" => "video",
"video" => [
"can_comment" => 1,
"can_like" => 0, // we don't h-have wikes in videos
"can_repost" => 0,
"can_like" => 1, // we don't h-have wikes in videos
"can_repost" => 1,
"can_subscribe" => 1,
"can_add_to_faves" => 0,
"can_add" => 0,
@ -155,21 +155,26 @@ class Video extends Media
"repeat" => 0,
"type" => "video",
"views" => 0,
"likes" => [
"count" => 0,
"user_likes" => 0
],
"reposts" => [
"count" => 0,
"user_reposted" => 0
]
]
];
if(!is_null($user)) {
$res->video["likes"] = [
"count" => $this->getLikesCount(),
"user_likes" => $this->hasLikeFrom($user)
];
}
return $res;
}
function toVkApiStruct(): object
function toVkApiStruct(?User $user): object
{
return $this->getApiStructure();
return $this->getApiStructure($user);
}
function setLink(string $link): string

View file

@ -77,4 +77,11 @@ class Videos
return new Util\EntityStream("Video", $result->order("$sort"));
}
function getLastVideo(User $user)
{
$video = $this->videos->where("owner", $user->getId())->where(["deleted" => 0, "unlisted" => 0])->order("id DESC")->fetch();
return new Video($video);
}
}

View file

@ -74,18 +74,18 @@ final class VideosPresenter extends OpenVKPresenter
else if(!empty($this->postParam("link")))
$video->setLink($this->postParam("link"));
else
$this->flashFail("err", tr("no_video"), tr("no_video_desc"));
$this->flashFail("err", tr("no_video_error"), tr("no_video_description"));
} catch(\DomainException $ex) {
$this->flashFail("err", tr("error_occured"), tr("error_video_damaged_file"));
$this->flashFail("err", tr("error_video"), tr("file_corrupted"));
} catch(ISE $ex) {
$this->flashFail("err", tr("error_occured"), tr("error_video_incorrect_link"));
$this->flashFail("err", tr("error_video"), tr("link_incorrect"));
}
$video->save();
$this->redirect("/video" . $video->getPrettyId());
} else {
$this->flashFail("err", tr("error_occured"), tr("error_video_no_title"));
$this->flashFail("err", tr("error_video"), tr("no_name_error"));
}
}
}
@ -99,14 +99,14 @@ final class VideosPresenter extends OpenVKPresenter
if(!$video)
$this->notFound();
if(is_null($this->user) || $this->user->id !== $owner)
$this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"));
$this->flashFail("err", tr("access_denied_error"), tr("access_denied_error_description"));
if($_SERVER["REQUEST_METHOD"] === "POST") {
$video->setName(empty($this->postParam("name")) ? NULL : $this->postParam("name"));
$video->setDescription(empty($this->postParam("desc")) ? NULL : $this->postParam("desc"));
$video->save();
$this->flash("succ", tr("changes_saved"), tr("new_data_video"));
$this->flash("succ", tr("changes_saved"), tr("changes_saved_video_comment"));
$this->redirect("/video" . $video->getPrettyId());
}
@ -128,9 +128,29 @@ final class VideosPresenter extends OpenVKPresenter
$video->deleteVideo($owner, $vid);
}
} else {
$this->flashFail("err", tr("error_deleting_video"), tr("login_please"));
$this->flashFail("err", tr("cant_delete_video"), tr("cant_delete_video_comment"));
}
$this->redirect("/videos" . $owner);
}
function renderLike(int $owner, int $video_id): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
$this->assertNoCSRF();
$video = $this->videos->getByOwnerAndVID($owner, $video_id);
if(!$video || $video->isDeleted() || $video->getOwner()->isDeleted()) $this->notFound();
if(method_exists($video, "canBeViewedBy") && !$video->canBeViewedBy($this->user->identity)) {
$this->flashFail("err", tr("error"), tr("forbidden"));
}
if(!is_null($this->user)) {
$video->toggleLike($this->user->identity);
}
$this->redirect("$_SERVER[HTTP_REFERER]");
}
}

View file

@ -13,6 +13,7 @@
<script src="/language/{php echo getLanguage()}.js" crossorigin="anonymous"></script>
{script "js/node_modules/jquery/dist/jquery.min.js"}
{script "js/node_modules/jquery-ui/dist/jquery-ui.min.js"}
{script "js/node_modules/umbrellajs/umbrella.min.js"}
{script "js/l10n.js"}
{script "js/openvk.cls.js"}
@ -370,6 +371,10 @@
</p>
</div>
<div id="ajloader" class="loader">
<img src="/assets/packages/static/openvk/img/loading_mini.gif" style="width: 40px;">
</div>
{include "components/cookies.xml"}
{script "js/node_modules/msgpack-lite/dist/msgpack.min.js"}

View file

@ -327,13 +327,13 @@
</div>
<div style="padding: 5px;">
<div class="ovk-video" n:foreach="$videos as $video">
<a href="/video{$video->getPrettyId()}" class="preview" align="center">
<a href="/video{$video->getPrettyId()}" class="preview" align="center" id="videoOpen" data-id="{$video->getId()}">
<img
src="{$video->getThumbnailURL()}"
style="max-width: 170px; max-height: 127px; margin: auto;" />
</a>
<div>
<b><a href="/video{$video->getPrettyId()}">{ovk_proc_strtr($video->getName(), 30)}</a></b><br>
<b><a href="/video{$video->getPrettyId()}" id="videoOpen" data-id="{$video->getId()}">{ovk_proc_strtr($video->getName(), 30)}</a></b><br>
<span style="font-size: 10px;">{$video->getPublicationTime()} | {_comments} ({$video->getCommentsCount()})</span>
</div>
</div>

View file

@ -33,7 +33,7 @@
{/block}
{block preview}
<div class="video-preview">
<div class="video-preview" id="videoOpen" data-id="{$x->getId()}">
<img src="{$x->getThumbnailURL()}"
alt="{$x->getName()}"
style="max-width: 170px; max-height: 127px; margin: auto;" />
@ -41,7 +41,7 @@
{/block}
{block name}
{$x->getName()}
<span id="videoOpen" data-id="{$x->getId()}" style="color:unset;">{$x->getName()}</span>
{/block}
{block description}
@ -51,7 +51,7 @@
<span style="color: grey;">{_video_uploaded} {$x->getPublicationTime()}</span><br/>
<span style="color: grey;">{_video_updated} {$x->getEditTime() ?? $x->getPublicationTime()}</span>
<p>
<a href="/video{$x->getPrettyId()}">{_view_video}</a>
<a href="/video{$x->getPrettyId()}" id="videoOpen" data-id="{$x->getId()}">{_view_video}</a>
{if $x->getCommentsCount() > 0}| <a href="/video{$x->getPrettyId()}#comments">{_comments} ({$x->getCommentsCount()})</a>{/if}
</p>
{/block}

View file

@ -29,7 +29,7 @@
<hr/>
<div style="width: 100%; min-height: 100px;">
<div style="float: left; min-height: 100px; width: 68%; margin-right: 2%;">
<div style="float: left; min-height: 100px; width: 68%; margin-right: 2%;" id="comments">
{include "../components/comments.xml",
comments => $comments,
count => $cCount,
@ -50,13 +50,18 @@
{$video->getPublicationTime()}
</div>
<br/>
<div n:if="isset($thisUser) && $thisUser->getId() === $user->getId()">
<h4>{_actions}</h4>
<a href="/video{$video->getPrettyId()}/edit" class="profile_link" style="display:block;width:96%;">
{_edit}
</a>
<a href="/video{$video->getPrettyId()}/remove" class="profile_link" style="display:block;width:96%;">
{_delete}
<div>
<div n:if="isset($thisUser) && $thisUser->getId() === $user->getId()">
<h4>{_actions}</h4>
<a href="/video{$video->getPrettyId()}/edit" class="profile_link" style="display:block;width:96%;">
{_edit}
</a>
<a href="/video{$video->getPrettyId()}/remove" class="profile_link" style="display:block;width:96%;">
{_delete}
</a>
</div>
<a href="/video{$video->getPrettyId()}" class="profile_link" id="videoOpen" data-id="{$video->getId()}" style="display:block;width:96%;">
{_watch_in_window}
</a>
</div>

View file

@ -10,6 +10,7 @@
</a>
{/if}
{elseif $attachment instanceof \openvk\Web\Models\Entities\Video}
{if !$attachment->isDeleted()}
{if $attachment->getType() === 0}
<div class="bsdn media" data-name="{$attachment->getName()}" data-author="{$attachment->getOwner()->getCanonicalName()}">
<video class="media" src="{$attachment->getURL()}"></video>
@ -25,8 +26,12 @@
<div class="video-wowzer">
<img src="/assets/packages/static/openvk/img/videoico.png" />
<a href="/video{$attachment->getPrettyId()}">{$attachment->getName()}</a>
<a href="/video{$attachment->getPrettyId()}" id="videoOpen" data-id="{$attachment->getId()}">{$attachment->getName()}</a>
</div>
{else}
<span style="color:gray;">{_video_is_deleted}</span>
{/if}
{elseif $attachment instanceof \openvk\Web\Models\Entities\Poll}
{presenter "openvk!Poll->view", $attachment->getId()}
{elseif $attachment instanceof \openvk\Web\Models\Entities\Note}

View file

@ -1,39 +1,37 @@
{block content}
<table>
<tbody>
<tr>
<td valign="top">
<div class="video-preview">
<a href="/video{$video->getPrettyId()}">
<div class="video-preview">
<img src="{$video->getThumbnailURL()}"
style="max-width: 170px; max-height: 127px; margin: auto;" >
</div>
</a>
</div>
</td>
<td valign="top" style="width: 100%">
{ifset infotable}
{include infotable, x => $dat}
{else}
<a href="/video{$video->getPrettyId()}">
<b>
{$video->getName()}
</b>
</a>
<br/>
<p>
<span>{$video->getDescription() ?? ""}</span>
</p>
<span style="color: grey;">{_video_uploaded} {$video->getPublicationTime()}</span><br/>
<tbody>
<tr>
<td valign="top">
<div class="video-preview">
<a href="/video{$video->getPrettyId()}" id="videoOpen" data-id="{$video->getId()}">
<img src="{$video->getThumbnailURL()}"
style="max-width: 170px; max-height: 127px; margin: auto;" >
</a>
</div>
</td>
<td valign="top" style="width: 100%">
{ifset infotable}
{include infotable, x => $dat}
{else}
<a href="/video{$video->getPrettyId()}">
<b id="videoOpen" data-id="{$video->getId()}">
{$video->getName()}
</b>
</a>
<br/>
<p>
<span>{$video->getDescription() ?? ""}</span>
</p>
<span style="color: grey;">{_video_uploaded} {$video->getPublicationTime()}</span><br/>
<p>
<a href="/video{$video->getPrettyId()}">{_view_video}</a>
{if $video->getCommentsCount() > 0}| <a href="/video{$video->getPrettyId()}#comments">{_comments} ({$video->getCommentsCount()})</a>{/if}
</p>
{/ifset}
</td>
</tr>
</tbody>
<p>
<a href="/video{$video->getPrettyId()}" id="videoOpen" data-id="{$video->getId()}">{_view_video}</a>
{if $video->getCommentsCount() > 0}| <a href="/video{$video->getPrettyId()}#comments">{_comments} ({$video->getCommentsCount()})</a>{/if}
</p>
{/ifset}
</td>
</tr>
</tbody>
</table>
{/block}

View file

@ -187,6 +187,8 @@ routes:
handler: "Videos->edit"
- url: "/video{num}_{num}/remove"
handler: "Videos->remove"
- url: "/video{num}_{num}/like"
handler: "Videos->like"
- url: "/player/upload"
handler: "Audio->upload"
- url: "/audios{num}"

View file

@ -100,14 +100,20 @@ button.bsdn_playButton {
cursor: pointer;
}
.bsdn_fullScreenButton {
.bsdn_fullScreenButton, .bsdn_repeatButton {
cursor: pointer;
}
.bsdn_fullScreenButton > img:hover {
background: url("/assets/packages/static/openvk/img/bsdn/fullscreen_hover.gif");
object-fit: none;
object-position: -64px 0;
background: url("/assets/packages/static/openvk/img/bsdn/fullscreen_hover.gif");
object-fit: none;
object-position: -64px 0;
}
.bsdn_repeatButton.pressed > img {
background: url("/assets/packages/static/openvk/img/bsdn/repeat_hover.gif");
object-fit: none;
object-position: -64px 0;
}
.bsdn_teaserWrap {
@ -215,6 +221,6 @@ time.bsdn_timeFull {
margin: 10px 0;
}
.bsdn_fullScreenButton > img, .bsdn_soundIcon {
.bsdn_fullScreenButton > img, .bsdn_repeatButton > img, .bsdn_soundIcon {
vertical-align: middle;
}

View file

@ -58,3 +58,249 @@ body.dimmed > .dimmer {
.ovk-diag-action > .button {
margin-left: 10px;
}
/* fullscreen player */
.ovk-fullscreen-player {
top: 9%;
left: 50%;
margin-right: -50%;
transform: translate(-50%, 0%);
z-index: 6667;
position: absolute;
width: 823px;
min-height: 400px;
box-shadow: 0px 0px 9px 2px rgba(0, 0, 0, 0.2);
}
.top-part span {
color: #515151;
font-size: 13px;
transition: color 200ms ease-in-out;
}
.top-part .clickable:hover {
color: #ffffff;
}
.ovk-fullscreen-player .bsdn_teaserTitleBox span {
color: unset;
font-size: unset;
}
.ovk-fullscreen-player .bsdn-player {
max-width: 80%;
max-height: 350px;
}
.inner-player {
background: #000000;
min-height: 438px;
max-height: 439px;
position: relative;
padding-top: 11px;
}
.top-part-name {
font-size: 15px;
font-weight: bolder;
margin-left: 20px;
margin-top: 5px;
}
.top-part-buttons {
float: right;
margin-right: 20px;
}
.top-part-buttons span {
cursor: pointer;
user-select: none;
}
.fplayer {
text-align: center;
margin-top: 20px;
}
.top-part-bottom-buttons {
position: absolute;
margin-left: 20px;
bottom: 0;
margin-bottom: 20px;
}
.top-part-bottom-buttons span {
user-select: none;
}
.top-part .clickable {
cursor: pointer;
}
.bottom-part {
display: none;
background: white;
padding-bottom: 20px;
padding-top: 30px;
}
.left_block {
padding-left: 20px;
/*padding-top: 20px;*/
width: 75%;
float: left;
background: white;
padding-right: 6px;
max-height: 400px;
overflow-y: scroll;
}
/* Работает только в хроме, потому что в фурифоксе до сих пор нет кастомных скроллбаров лул */
.left_block::-webkit-scrollbar {
width: 0;
}
.right_block {
padding-left: 10px;
/*padding-top: 20px;*/
width: 20%;
border-left: 1px solid gray;
float: right;
}
.bottom-part span {
font-size: 13px;
}
.bottom-part .gray {
color: gray;
}
.ovk-fullscreen-dimmer {
/* спижжено у пулла с несколькими картинками там где просмотрщик фоток */
position: fixed;
left: 0px;
top: 0px;
right: 0px;
bottom: 0px;
overflow: auto;
padding-bottom: 20px;
z-index: 300;
}
.v_author {
margin-top: 20px;
}
.miniplayer {
position: absolute;
top:0;
background: rgba(54, 54, 54, 0.9);
border-radius: 3px;
min-width: 299px;
min-height: 192px;
padding-top: 3px;
z-index: 9999;
}
.miniplayer .bsdn-player {
max-height: 150px;
}
.miniplayer .fplayer {
max-width: 286px;
margin-left: 6px;
margin-top: 10px;
}
.miniplayer-actions {
float: right;
margin-right: 8px;
margin-top: 4px;
}
.miniplayer-name {
color: #8a8a8a;
font-size: 14px;
margin-left: 7px;
margin-top: -6px;
font-weight: bolder;
user-select: none;
}
.ui-draggable {
position:fixed !important;
}
.miniplayer-actions img {
max-width: 11px;
cursor: pointer;
transition: opacity 200ms ease-in-out;
opacity: 70%;
}
.miniplayer .fplayer iframe {
max-width: 260px;
max-height: 160px;
}
.miniplayer-actions img:hover {
opacity: 100%;
}
#vidComments {
margin-top: 10px;
}
.showMoreComments {
background: #eaeaea;
cursor: pointer;
text-align: center;
padding: 10px;
user-select: none;
margin-top: 10px;
}
.loader {
display: none;
position: fixed;
top: -10%;
background: rgba(26, 26, 26, 0.9);;
padding-top: 12px;
width: 91px;
height: 25px;
text-align: center;
border-radius: 1px;
margin: auto;
left: 0;
right: 0;
bottom: 0;
z-index: 5555;
}
.right-arrow, .left-arrow {
position: absolute;
cursor: pointer;
transition: all 200ms ease-in-out;
margin-left: -50px;
background: none;
height: 449px;
width: 57px;
user-select: none;
}
.right-arrow img, .left-arrow img {
user-select: none;
opacity: 5%;
transition: all 200ms ease-in-out;
}
.right-arrow:hover, .left-arrow:hover {
background: rgba(0, 0, 0, 0.5);
}
.right-arrow img:hover, .left-arrow img:hover {
opacity: 50%;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 B

BIN
Web/static/img/left_arr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -22,6 +22,31 @@ function trim(string) {
return newStr;
}
function trimNum(string, num) {
var newStr = string.substring(0, num);
if(newStr.length !== string.length)
newStr += "…";
return newStr;
}
function handleUpload(id) {
console.warn("блять...");
u("#post-buttons" + id + " .postFileSel").not("#" + this.id).each(input => input.value = null);
var indicator = u("#post-buttons" + id + " .post-upload");
var file = this.files[0];
if(typeof file === "undefined") {
indicator.attr("style", "display: none;");
} else {
u("span", indicator.nodes[0]).text(trim(file.name) + " (" + humanFileSize(file.size, false) + ")");
indicator.attr("style", "display: block;");
}
document.querySelector("#post-buttons" + id + " #wallAttachmentMenu").classList.add("hidden");
}
function initGraffiti(id) {
let canvas = null;
let msgbox = MessageBox(tr("draw_graffiti"), "<div id='ovkDraw'></div>", [tr("save"), tr("cancel")], [function() {
@ -52,6 +77,7 @@ function initGraffiti(id) {
});
}
$(document).on("click", ".post-like-button", function(e) {
function fastUploadImage(textareaId, file) {
// uploading images
@ -493,6 +519,395 @@ async function showArticle(note_id) {
u("body").addClass("article");
}
// Оконный плеер
$(document).on("click", "#videoOpen", async (e) => {
e.preventDefault()
document.getElementById("ajloader").style.display = "block"
if(document.querySelector(".ovk-fullscreen-dimmer") != null) {
u(".ovk-fullscreen-dimmer").remove()
}
let target = e.currentTarget
let videoId = target.dataset.id
let videoObj = null;
try {
videoObj = await API.Video.getVideo(Number(videoId))
} catch(e) {
console.error(e)
document.getElementById("ajloader").style.display = "none"
MessageBox(tr("error"), tr("video_access_denied"), [tr("cancel")], [
function() {
Function.noop
}]);
return 0;
}
document.querySelector("html").style.overflowY = "hidden"
let player = null;
if(target.dataset.dontload == null) {
document.querySelectorAll("video").forEach(vid => vid.pause())
if(videoObj.type == 0) {
if(videoObj.isProcessing) {
player = `
<span class="gray">${tr("video_processing")}</span>
`
} else {
player = `
<div class="bsdn media" data-name="${videoObj.title}" data-author="${videoObj.name}">
<video class="media" src="${videoObj.url}"></video>
</div>`
}
} else {
player = videoObj.embed
}
} else {
player = ``
}
let dialog = u(
`
<div class="ovk-fullscreen-dimmer">
<div class="ovk-fullscreen-player">
${videoObj.prevVideo != null ?
`<div class="right-arrow" id="videoOpen" data-id="${videoObj.prevVideo}">
<img src="/assets/packages/static/openvk/img/right_arr.png" draggable="false">
</div>` : ""}
${videoObj.nextVideo != null ? `
<div class="left-arrow" id="videoOpen" data-id="${videoObj.nextVideo}" style="margin-left: 820px;">
<img src="/assets/packages/static/openvk/img/left_arr.png" draggable="false">
</div>` : ""}
<div class="inner-player">
<div class="top-part">
<span class="top-part-name">${escapeHtml(videoObj.title)}</span>
<div class="top-part-buttons">
<span class="clickable" id="minimizePlayer" data-name="${videoObj.title}" data-id="${videoObj.id}">${tr("hide_player")}</span>
<span>|</span>
<span class="clickable" id="closeFplayer">${tr("close_player")}</span>
</div>
<div class="top-part-player-subdiv">
${target.dataset.dontload == null ?`
<div class="fplayer">
${player}
</div>` : ""}
</div>
<div class="top-part-bottom-buttons">
<span class="clickable" id="showComments" data-id="${videoObj.id}" data-owner="${videoObj.owner}" data-pid="${videoObj.pretty_id}">${tr("show_comments")}</span>
<span>|</span>
<span class="clickable" id="gotopage" data-id="/video${videoObj.pretty_id}">${tr("to_page")}</span>
${ videoObj.type == 0 && videoObj.isProcessing == false ? `<span>|</span>
<a class="clickable" href="${videoObj.url}" download><span class="clickable">${tr("download_video")}</span></a>` : ""}
</div>
</div>
</div>
<div class="bottom-part">
<div class="left_block">
<div class="description" style="margin-bottom: 5px;">
<span>${videoObj.description != null ? escapeHtml(videoObj.description) : "(" + tr("no_description") + ")"}</span>
</div>
<div class="bottom-part-info" style="display: flex;">
<span class="gray">${tr("added")} ${videoObj.published}&nbsp;</span><span>|</span>
<div class="like_wrap" style="float:unset;">
<a href="/video${videoObj.pretty_id}/like?hash=${encodeURIComponent(u("meta[name=csrf]").attr("value"))}" class="post-like-button" data-liked="${videoObj.has_like ? 1 : 0}" data-likes="${videoObj.likes}">
<div class="heart" id="${videoObj.has_like ? "liked" : ""}"></div>
<span class="likeCnt" style="margin-top: -2px;">${videoObj.likes > 0 ? videoObj.likes : ""}</span>
</a>
</div>
</div>
<div id="vidComments"></div>
</div>
<div class="right_block">
<div class="views">
<!--prosmoters are not implemented((-->
<span class="gray">${tr("x_views", 0)}</span>
</div>
<div class="v_author">
<span class="gray">${tr("video_author")}:</span><br>
<a href="/id${videoObj.owner}"><span style="color:unset;">${videoObj.author}</span></a>
</div>
<div class="actions" style="margin-top: 10px;margin-left: -3px;">
${videoObj.canBeEdited ? `
<a href="/video${videoObj.pretty_id}/edit" class="profile_link" style="display:block;width:96%;font-size: 13px;">
${tr("edit")}
</a>
<a href="/video${videoObj.pretty_id}/remove" class="profile_link" style="display:block;width:96%;font-size: 13px;">
${tr("delete")}
</a>`
: ""}
<a id="shareVideo" class="profile_link" id="shareVideo" data-owner="${videoObj.owner}" data-vid="${videoObj.virtual_id}" style="display:block;width:96%;font-size: 13px;">
${tr("share")}
</a>
</div>
</div>
</div>
</div>
</div>`);
u("body").addClass("dimmed").append(dialog);
if(target.dataset.dontload != null) {
let oldPlayer = document.querySelector(".miniplayer-video .fplayer")
let newPlayer = document.querySelector(".top-part-player-subdiv")
document.querySelector(".top-part-player-subdiv")
newPlayer.append(oldPlayer)
}
if(videoObj.type == 0 && videoObj.isProcessing == false) {
bsdnInitElement(document.querySelector(".fplayer .bsdn"))
}
document.getElementById("ajloader").style.display = "none"
u(".miniplayer").remove()
})
$(document).on("click", "#closeFplayer", async (e) => {
u(".ovk-fullscreen-dimmer").remove();
document.querySelector("html").style.overflowY = "scroll"
u("body").removeClass("dimmed")
})
$(document).on("click", "#minimizePlayer", async (e) => {
let targ = e.currentTarget
let player = document.querySelector(".fplayer")
let dialog = u(`
<div class="miniplayer">
<span class="miniplayer-name">${escapeHtml(trimNum(targ.dataset.name, 26))}</span>
<div class="miniplayer-actions">
<img src="/assets/packages/static/openvk/img/miniplayer_open.png" id="videoOpen" data-dontload="true" data-id="${targ.dataset.id}">
<img src="/assets/packages/static/openvk/img/miniplayer_close.png" id="closeMiniplayer">
</div>
<div class="miniplayer-video">
</div>
</div>
`);
u("body").append(dialog);
$('.miniplayer').draggable({cursor: "grabbing", containment: "body", cancel: ".miniplayer-video"});
let newPlayer = document.querySelector(".miniplayer-video")
newPlayer.append(player)
document.querySelector(".miniplayer").style.top = window.scrollY;
document.querySelector("#closeFplayer").click()
})
$(document).on("click", "#closeMiniplayer", async (e) => {
u(".miniplayer").remove()
})
$(document).on("mouseup", "#gotopage", async (e) => {
if(e.originalEvent.which === 1) {
location.href = e.currentTarget.dataset.id
} else if (e.originalEvent.which === 2) {
window.open(e.currentTarget.dataset.id, '_blank')
}
})
$(document).keydown(function(e) {
if(document.querySelector(".top-part-player-subdiv .bsdn") != null && document.activeElement.tagName == "BODY") {
let video = document.querySelector(".top-part-player-subdiv video")
switch(e.keyCode) {
// Пробел вроде
case 32:
document.querySelector(".top-part-player-subdiv .bsdn_teaserButton").click()
break
// Стрелка вниз, уменьшение громкости
case 40:
oldVolume = video.volume
if(oldVolume - 0.1 > 0) {
video.volume = oldVolume - 0.1
} else {
video.volume = 0
}
break;
// Стрелка вверх, повышение громкости
case 38:
oldVolume = video.volume
if(oldVolume + 0.1 < 1) {
video.volume = oldVolume + 0.1
} else {
video.volume = 1
}
break
// стрелка влево, отступ на 2 секунды назад
case 37:
oldTime = video.currentTime
video.currentTime = oldTime - 2
break
// стрелка вправо, отступ на 2 секунды вперёд
case 39:
oldTime = document.querySelector(".top-part-player-subdiv video").currentTime
document.querySelector(".top-part-player-subdiv video").currentTime = oldTime + 2
break
}
}
});
$(document).keyup(function(e) {
if(document.querySelector(".top-part-player-subdiv .bsdn") != null && document.activeElement.tagName == "BODY") {
let video = document.querySelector(".top-part-player-subdiv video")
switch(e.keyCode) {
// Escape, закрытие плеера
case 27:
document.querySelector("#closeFplayer").click()
break
// Блять, я перепутал лево и право, пиздец я долбаёб конечно
// Ну короче стрелка влево
case 65:
if(document.querySelector(".right-arrow") != null) {
document.querySelector(".right-arrow").click()
} else {
console.info("No left arrow bro")
}
break
// Фуллскрин
case 70:
document.querySelector(".top-part-player-subdiv .bsdn_fullScreenButton").click()
break
// стрелка вправо
case 68:
if(document.querySelector(".left-arrow") != null) {
document.querySelector(".left-arrow").click()
} else {
console.info("No right arrow bro")
}
break;
// S: Показать инфо о видео (не комментарии)
case 83:
document.querySelector(".top-part-player-subdiv #showComments").click()
break
// Мут (M)
case 77:
document.querySelector(".top-part-player-subdiv .bsdn_soundIcon").click()
break;
// Escape, выход из плеера
case 192:
document.querySelector(".top-part-buttons #minimizePlayer").click()
break
// Бля не помню сори
case 75:
document.querySelector(".top-part-player-subdiv .bsdn_playButton").click()
break
// Home, переход в начало видосика
case 36:
video.currentTime = 0
break
// End, переход в конец видосика
case 35:
video.currentTime = video.duration
break;
}
}
});
$(document).on("click", "#showComments", async (e) => {
if(document.querySelector(".bottom-part").style.display == "none" || document.querySelector(".bottom-part").style.display == "") {
if(document.getElementById("vidComments").innerHTML == "") {
let xhr = new XMLHttpRequest
xhr.open("GET", "/video"+e.currentTarget.dataset.pid)
xhr.onloadstart = () => {
document.getElementById("vidComments").innerHTML = `<img src="/assets/packages/static/openvk/img/loading_mini.gif">`
}
xhr.timeout = 10000;
xhr.onload = () => {
let parser = new DOMParser();
let body = parser.parseFromString(xhr.responseText, "text/html");
let comms = body.getElementById("comments")
let commsHTML = comms.innerHTML.replace("expand_wall_textarea(11)", "expand_wall_textarea(999)")
.replace("wall-post-input11", "wall-post-input999")
.replace("post-buttons11", "post-buttons999")
.replace("toggleMenu(11)", "toggleMenu(999)")
.replace("toggleMenu(11)", "toggleMenu(999)")
.replace(/ons11/g, "ons999")
document.getElementById("vidComments").innerHTML = commsHTML
}
xhr.onerror = () => {
document.getElementById("vidComments").innerHTML = `<span>${tr("comments_load_timeout")}</span>`
}
xhr.ontimeout = () => {
document.getElementById("vidComments").innerHTML = `<span>${tr("comments_load_timeout")}</span>`
};
xhr.send()
}
document.querySelector(".bottom-part").style.display = "flex"
e.currentTarget.innerHTML = tr("close_comments")
} else {
document.querySelector(".bottom-part").style.display = "none"
e.currentTarget.innerHTML = tr("show_comments")
}
})
$(document).on("click", "#shareVideo", async (e) => {
let owner_id = e.currentTarget.dataset.owner
let virtual_id = e.currentTarget.dataset.vid
let body = `
<b>${tr('auditory')}:</b> <br/>
<input type="radio" name="type" onchange="signs.setAttribute('hidden', 'hidden');document.getElementById('groupId').setAttribute('hidden', 'hidden')" value="0" checked>${tr("in_wall")}<br/>
<input type="radio" name="type" onchange="signs.removeAttribute('hidden');document.getElementById('groupId').removeAttribute('hidden')" value="1" id="group">${tr("in_group")}<br/>
<select style="width:50%;" id="groupId" name="groupId" hidden>
</select><br/>
<b>${tr('your_comment')}:</b>
<textarea id='uRepostMsgInput'></textarea>
<div id="signs" hidden>
<label><input onchange="signed.checked ? signed.checked = false : null" type="checkbox" id="asgroup" name="asGroup">${tr('post_as_group')}</label><br>
<label><input onchange="asgroup.checked = true" type="checkbox" id="signed" name="signed">${tr('add_signature')}</label>
</div>
`
MessageBox(tr("share_video"), body, [tr("share"), tr("cancel")], [
(async function() {
let type = $('input[name=type]:checked').val()
let club = document.getElementById("groupId").value
let asGroup = document.getElementById("asgroup").checked
let signed = document.getElementById("signed").checked
let repost = null;
try {
repost = await API.Video.shareVideo(Number(owner_id), Number(virtual_id), Number(type), uRepostMsgInput.value, Number(club), signed, asGroup)
NewNotification(tr('information_-1'), tr('shared_succ_video'), null, () => {window.location.href = "/wall" + repost.pretty_id});
} catch(e) {
console.log("tudu")
}
}), (function() {
Function.noop
})], false);
try {
clubs = await API.Groups.getWriteableClubs();
for(const el of clubs) {
document.getElementById("groupId").insertAdjacentHTML("beforeend", `<option value="${el.id}">${escapeHtml(el.name)}</option>`)
}
} catch(rejection) {
console.error(rejection)
document.getElementById("group").setAttribute("disabled", "disabled")
}
$(document).on("click", "#videoAttachment", async (e) => {
e.preventDefault()

View file

@ -1,6 +1,6 @@
Function.noop = () => {};
function MessageBox(title, body, buttons, callbacks) {
function MessageBox(title, body, buttons, callbacks, removeDimmedOnExit = true) {
if(u(".ovk-diag-cont").length > 0) return false;
document.querySelector("html").style.overflowY = "hidden"
@ -20,9 +20,9 @@ function MessageBox(title, body, buttons, callbacks) {
button.on("click", function(e) {
let __closeDialog = () => {
if(document.querySelector(".ovk-photo-view-dimmer") == null) {
if(removeDimmedOnExit) {
u("body").removeClass("dimmed");
document.querySelector("html").style.overflowY = "scroll"
}
u(".ovk-diag-cont").remove();

View file

@ -68,6 +68,12 @@ function _bsdnTpl(name, author) {
</div>
</div>
<div>
<div class="bsdn_repeatButton">
<img src="/assets/packages/static/openvk/img/bsdn/repeat.gif" />
</div>
</div>
<div>
<div class="bsdn_fullScreenButton">
<img src="/assets/packages/static/openvk/img/bsdn/fullscreen.gif" />
@ -252,6 +258,26 @@ function _bsdnEventListenerFactory(el, v) {
]
},
".bsdn_repeatButton": {
click: [
() => {
if(!v.loop) {
v.loop = true
el.querySelector(".bsdn_repeatButton").classList.add("pressed")
if(v.currentTime == v.duration) {
v.currentTime = 0
v.play()
}
} else {
v.loop = false
el.querySelector(".bsdn_repeatButton").classList.remove("pressed")
}
}
]
},
".bsdn_fullScreenButton": {
click: [
() => {

View file

@ -1972,6 +1972,50 @@
"mobile_user_info_hide" = "Hide";
"mobile_user_info_show_details" = "Show details";
/* Fullscreen player */
"hide_player" = "Minimize";
"close_player" = "Close";
"show_comments" = "Show info";
"close_comments" = "Hide info";
"to_page" = "Go to page";
"download_video" = "Download";
"added" = "Added";
"x_views" = "$1 views";
"video_author" = "Video's author";
"video_delete" = "Delete";
"no_description" = "no description";
"show_more_comments" = "Show more comments";
"video_processing" = "Video is succefully uploaded and now is processed.";
"video_access_denied" = "Access to video denied";
"open_page_to_read_comms" = "To read the comments, open <a href=\"$1\">page</a>.";
"no_video_error" = "No videofile";
"no_video_description" = "Select file or specify link.";
"error_video" = "An error has occurred";
"file_corrupted" = "File is corrupted or does not have video.";
"link_incorrect" = "Maybe, the link is wrong.";
"no_name_error" = "Video can't be published without name";
"access_denied_error" = "Access denied";
"access_denied_error_description" = "You are not allowed to edit this resource";
"changes_saved_video_comment" = "Updated data will appear on the video page";
"cant_delete_video" = "Failed to delete video";
"cant_delete_video_comment" = "You are not logged in.";
"change_video" = "Change video";
"video_is_deleted" = "Video was deleted.";
"share_video" = "Share video";
"shared_succ_video" = "Video will appear at your wall. Click on this notification to move to post.";
"watch_in_window" = "Watch in window";
"comments_load_timeout" = "The instance may have fallen";
"my" = "My";
"enter_a_name_or_artist" = "Enter a name or artist...";

View file

@ -1858,6 +1858,50 @@
"mobile_user_info_hide" = "Скрыть";
"mobile_user_info_show_details" = "Показать подробнее";
/* Fullscreen player */
"hide_player" = "Скрыть";
"close_player" = "Закрыть";
"show_comments" = "Показать информацию";
"close_comments" = "Скрыть информацию";
"to_page" = "Перейти на страницу";
"download_video" = "Скачать";
"added" = "Добавлено";
"x_views" = "$1 просмотров";
"video_author" = "Автор видео";
"video_delete" = "Удалить";
"no_description" = "описания нет";
"show_more_comments" = "Показать больше комментариев";
"video_processing" = "Видео успешно загружено и на данный момент обрабатывается.";
"video_access_denied" = "Доступ к видео запрещён";
"open_page_to_read_comms" = "Для чтения комментариев откройте <a href=\"$1\">страницу</a>.";
"no_video_error" = "Нету видеозаписи";
"no_video_description" = "Выберите файл или укажите ссылку.";
"error_video" = "Произошла ошибка";
"file_corrupted" = "Файл повреждён или не содержит видео.";
"link_incorrect" = "Возможно, ссылка некорректна.";
"no_name_error" = "Видео не может быть опубликовано без названия";
"access_denied_error" = "Ошибка доступа";
"access_denied_error_description" = "Вы не имеете права редактировать этот ресурс";
"changes_saved_video_comment" = "Обновлённые данные появятся на странице с видео";
"cant_delete_video" = "Не удалось удалить видео";
"cant_delete_video_comment" = "Вы не вошли в аккаунт.";
"change_video" = "Изменить видеозапись";
"video_is_deleted" = "Видео удалено.";
"share_video" = "Поделиться видеороликом";
"shared_succ_video" = "Видео появится на вашей стене. Нажмите на уведомление, чтобы перейти к записи.";
"watch_in_window" = "Смотреть в окне";
"comments_load_timeout" = "Возможно, инстанция упала.";
"my" = "Мои";
"enter_a_name_or_artist" = "Введите название или автора...";
@ -1897,3 +1941,4 @@
"roll_back" = "откатить";
"roll_backed" = "откачено";