diff --git a/ServiceAPI/Video.php b/ServiceAPI/Video.php new file mode 100644 index 00000000..d0eb4ee8 --- /dev/null +++ b/ServiceAPI/Video.php @@ -0,0 +1,156 @@ +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); + } +} diff --git a/VKAPI/Handlers/Likes.php b/VKAPI/Handlers/Likes.php index 08c4e6e4..fab127f2 100644 --- a/VKAPI/Handlers/Likes.php +++ b/VKAPI/Handlers/Likes.php @@ -1,72 +1,157 @@ 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) diff --git a/VKAPI/Handlers/Video.php b/VKAPI/Handlers/Video.php index c51fff3f..5d0967c7 100755 --- a/VKAPI/Handlers/Video.php +++ b/VKAPI/Handlers/Video.php @@ -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) [ diff --git a/VKAPI/Handlers/Wall.php b/VKAPI/Handlers/Wall.php index 907f06fa..2929beb9 100644 --- a/VKAPI/Handlers/Wall.php +++ b/VKAPI/Handlers/Wall.php @@ -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) { diff --git a/Web/Models/Entities/Comment.php b/Web/Models/Entities/Comment.php index 90057bdd..a553443e 100644 --- a/Web/Models/Entities/Comment.php +++ b/Web/Models/Entities/Comment.php @@ -74,8 +74,12 @@ class Comment extends Post foreach($this->getChildren() as $attachment) { 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) { diff --git a/Web/Models/Entities/Postable.php b/Web/Models/Entities/Postable.php index 365c0f26..a6478605 100644 --- a/Web/Models/Entities/Postable.php +++ b/Web/Models/Entities/Postable.php @@ -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 diff --git a/Web/Models/Entities/Video.php b/Web/Models/Entities/Video.php index a9d565c5..fa54392e 100644 --- a/Web/Models/Entities/Video.php +++ b/Web/Models/Entities/Video.php @@ -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 diff --git a/Web/Models/Repositories/Videos.php b/Web/Models/Repositories/Videos.php index 63273349..2d41c3f9 100644 --- a/Web/Models/Repositories/Videos.php +++ b/Web/Models/Repositories/Videos.php @@ -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); + } } diff --git a/Web/Presenters/VideosPresenter.php b/Web/Presenters/VideosPresenter.php index 9d2fddc6..57e73f11 100644 --- a/Web/Presenters/VideosPresenter.php +++ b/Web/Presenters/VideosPresenter.php @@ -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]"); + } } diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml index 31e0bc5c..8f1a35f0 100644 --- a/Web/Presenters/templates/@layout.xml +++ b/Web/Presenters/templates/@layout.xml @@ -13,6 +13,7 @@ {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 @@

+
+ +
+ {include "components/cookies.xml"} {script "js/node_modules/msgpack-lite/dist/msgpack.min.js"} diff --git a/Web/Presenters/templates/User/View.xml b/Web/Presenters/templates/User/View.xml index cb35102a..b8bd5afc 100644 --- a/Web/Presenters/templates/User/View.xml +++ b/Web/Presenters/templates/User/View.xml @@ -327,13 +327,13 @@
- +
- {ovk_proc_strtr($video->getName(), 30)}
+ {ovk_proc_strtr($video->getName(), 30)}
{$video->getPublicationTime()} | {_comments} ({$video->getCommentsCount()})
diff --git a/Web/Presenters/templates/Videos/List.xml b/Web/Presenters/templates/Videos/List.xml index 4f811289..57db0b8f 100644 --- a/Web/Presenters/templates/Videos/List.xml +++ b/Web/Presenters/templates/Videos/List.xml @@ -33,7 +33,7 @@ {/block} {block preview} -
+
{$x->getName()} @@ -41,7 +41,7 @@ {/block} {block name} - {$x->getName()} + {$x->getName()} {/block} {block description} @@ -51,7 +51,7 @@ {_video_uploaded} {$x->getPublicationTime()}
{_video_updated} {$x->getEditTime() ?? $x->getPublicationTime()}

- {_view_video} + {_view_video} {if $x->getCommentsCount() > 0}| {_comments} ({$x->getCommentsCount()}){/if}

{/block} diff --git a/Web/Presenters/templates/Videos/View.xml b/Web/Presenters/templates/Videos/View.xml index 38967b5e..cdaaba11 100644 --- a/Web/Presenters/templates/Videos/View.xml +++ b/Web/Presenters/templates/Videos/View.xml @@ -29,7 +29,7 @@
-
+
{include "../components/comments.xml", comments => $comments, count => $cCount, @@ -50,13 +50,18 @@ {$video->getPublicationTime()}

-
-

{_actions}

- - {_edit} - - - {_delete} + diff --git a/Web/Presenters/templates/components/attachment.xml b/Web/Presenters/templates/components/attachment.xml index 85a33f21..fbc36ede 100644 --- a/Web/Presenters/templates/components/attachment.xml +++ b/Web/Presenters/templates/components/attachment.xml @@ -10,6 +10,7 @@ {/if} {elseif $attachment instanceof \openvk\Web\Models\Entities\Video} + {if !$attachment->isDeleted()} {if $attachment->getType() === 0}
@@ -25,8 +26,12 @@ + + {else} + {_video_is_deleted} + {/if} {elseif $attachment instanceof \openvk\Web\Models\Entities\Poll} {presenter "openvk!Poll->view", $attachment->getId()} {elseif $attachment instanceof \openvk\Web\Models\Entities\Note} diff --git a/Web/Presenters/templates/components/video.xml b/Web/Presenters/templates/components/video.xml index f568e942..08cebf22 100644 --- a/Web/Presenters/templates/components/video.xml +++ b/Web/Presenters/templates/components/video.xml @@ -1,39 +1,37 @@ {block content} - - - - - - + + + + + +
- - - {ifset infotable} - {include infotable, x => $dat} - {else} - - - {$video->getName()} - - -
-

- {$video->getDescription() ?? ""} -

- {_video_uploaded} {$video->getPublicationTime()}
- -

- {_view_video} - {if $video->getCommentsCount() > 0}| {_comments} ({$video->getCommentsCount()}){/if} -

- {/ifset} -
+
+ + + +
+
+ {ifset infotable} + {include infotable, x => $dat} + {else} + + + {$video->getName()} + + +
+

+ {$video->getDescription() ?? ""} +

+ {_video_uploaded} {$video->getPublicationTime()}
+ +

+ {_view_video} + {if $video->getCommentsCount() > 0}| {_comments} ({$video->getCommentsCount()}){/if} +

+ {/ifset} +
{/block} diff --git a/Web/routes.yml b/Web/routes.yml index d12ccbc0..2acc6496 100644 --- a/Web/routes.yml +++ b/Web/routes.yml @@ -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}" diff --git a/Web/static/css/bsdn.css b/Web/static/css/bsdn.css index 59904104..ce7f6ed6 100644 --- a/Web/static/css/bsdn.css +++ b/Web/static/css/bsdn.css @@ -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; -} \ No newline at end of file +} diff --git a/Web/static/css/dialog.css b/Web/static/css/dialog.css index 465b1e0b..4e97b411 100644 --- a/Web/static/css/dialog.css +++ b/Web/static/css/dialog.css @@ -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%; +} diff --git a/Web/static/img/bsdn/repeat.gif b/Web/static/img/bsdn/repeat.gif new file mode 100644 index 00000000..c94bb035 Binary files /dev/null and b/Web/static/img/bsdn/repeat.gif differ diff --git a/Web/static/img/bsdn/repeat_hover.gif b/Web/static/img/bsdn/repeat_hover.gif new file mode 100644 index 00000000..4eb1f813 Binary files /dev/null and b/Web/static/img/bsdn/repeat_hover.gif differ diff --git a/Web/static/img/left_arr.png b/Web/static/img/left_arr.png new file mode 100644 index 00000000..6be3f80a Binary files /dev/null and b/Web/static/img/left_arr.png differ diff --git a/Web/static/img/miniplayer_close.png b/Web/static/img/miniplayer_close.png new file mode 100644 index 00000000..e2f0fb78 Binary files /dev/null and b/Web/static/img/miniplayer_close.png differ diff --git a/Web/static/img/miniplayer_open.png b/Web/static/img/miniplayer_open.png new file mode 100644 index 00000000..7f442647 Binary files /dev/null and b/Web/static/img/miniplayer_open.png differ diff --git a/Web/static/img/right_arr.png b/Web/static/img/right_arr.png new file mode 100644 index 00000000..ed4c4708 Binary files /dev/null and b/Web/static/img/right_arr.png differ diff --git a/Web/static/js/al_wall.js b/Web/static/js/al_wall.js index 5acba57f..f5e0af0a 100644 --- a/Web/static/js/al_wall.js +++ b/Web/static/js/al_wall.js @@ -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"), "
", [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 = ` + ${tr("video_processing")} + ` + } else { + player = ` +
+ +
` + } + } else { + player = videoObj.embed + } + } else { + player = `` + } + + + let dialog = u( + ` +
+
+ ${videoObj.prevVideo != null ? + `
+ +
` : ""} + ${videoObj.nextVideo != null ? ` +
+ +
` : ""} +
+
+ ${escapeHtml(videoObj.title)} +
+ ${tr("hide_player")} + | + ${tr("close_player")} +
+
+ ${target.dataset.dontload == null ?` +
+ ${player} +
` : ""} +
+
+ ${tr("show_comments")} + | + ${tr("to_page")} + ${ videoObj.type == 0 && videoObj.isProcessing == false ? `| + ${tr("download_video")}` : ""} +
+
+
+
+
+
+ ${videoObj.description != null ? escapeHtml(videoObj.description) : "(" + tr("no_description") + ")"} +
+
+ ${tr("added")} ${videoObj.published} | + +
+
+
+
+
+ + ${tr("x_views", 0)} +
+ +
+ ${tr("video_author")}:
+ ${videoObj.author} +
+
+ ${videoObj.canBeEdited ? ` + + ${tr("edit")} + + + ${tr("delete")} + ` + : ""} + + ${tr("share")} + +
+
+
+
+
`); + + 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(` +
+ ${escapeHtml(trimNum(targ.dataset.name, 26))} +
+ + +
+
+ +
+
+ `); + + 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 = `` + } + + 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 = `${tr("comments_load_timeout")}` + } + + xhr.ontimeout = () => { + document.getElementById("vidComments").innerHTML = `${tr("comments_load_timeout")}` + }; + + 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 = ` + ${tr('auditory')}:
+ ${tr("in_wall")}
+ ${tr("in_group")}
+
+ ${tr('your_comment')}: + + + ` + 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", ``) + } + } catch(rejection) { + console.error(rejection) + document.getElementById("group").setAttribute("disabled", "disabled") + } + $(document).on("click", "#videoAttachment", async (e) => { e.preventDefault() diff --git a/Web/static/js/messagebox.js b/Web/static/js/messagebox.js index e56c720f..7697875e 100644 --- a/Web/static/js/messagebox.js +++ b/Web/static/js/messagebox.js @@ -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,11 +20,11 @@ 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(); }; diff --git a/Web/static/js/player.js b/Web/static/js/player.js index 7465f023..a6ad5da3 100644 --- a/Web/static/js/player.js +++ b/Web/static/js/player.js @@ -68,6 +68,12 @@ function _bsdnTpl(name, author) {
+
+
+ +
+
+
@@ -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: [ () => { diff --git a/locales/en.strings b/locales/en.strings index 69eeb3b0..3804c5f8 100644 --- a/locales/en.strings +++ b/locales/en.strings @@ -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 page."; + +"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..."; diff --git a/locales/ru.strings b/locales/ru.strings index 42eadc6f..a04af131 100644 --- a/locales/ru.strings +++ b/locales/ru.strings @@ -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" = "Для чтения комментариев откройте страницу."; + +"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" = "откачено"; +