diff --git a/ServiceAPI/Photos.php b/ServiceAPI/Photos.php new file mode 100644 index 00000000..16d602f2 --- /dev/null +++ b/ServiceAPI/Photos.php @@ -0,0 +1,92 @@ +user = $user; + $this->photos = new PhotosRepo; + } + + function getPhotos(int $page = 1, int $album = 0, callable $resolve, callable $reject) + { + if($album == 0) { + $photos = $this->photos->getEveryUserPhoto($this->user, $page, 24); + $count = $this->photos->getUserPhotosCount($this->user); + } else { + $album = (new Albums)->get($album); + + if(!$album || $album->isDeleted()) + $reject(55, "Invalid ."); + + if($album->getOwner() instanceof User) { + if($album->getOwner()->getId() != $this->user->getId()) + $reject(555, "Access to album denied"); + } else { + if(!$album->getOwner()->canBeModifiedBy($this->user)) + $reject(555, "Access to album denied"); + } + + $photos = $album->getPhotos($page, 24); + $count = $album->size(); + } + + $arr = [ + "count" => $count, + "items" => [], + ]; + + foreach($photos as $photo) { + $res = json_decode(json_encode($photo->toVkApiStruct()), true); + + $arr["items"][] = $res; + } + + $resolve($arr); + } + + function getAlbums(int $club, callable $resolve, callable $reject) + { + $albumsRepo = (new Albums); + + $count = $albumsRepo->getUserAlbumsCount($this->user); + $albums = $albumsRepo->getUserAlbums($this->user, 1, $count); + + $arr = [ + "count" => $count, + "items" => [], + ]; + + foreach($albums as $album) { + $res = ["id" => $album->getId(), "name" => $album->getName()]; + + $arr["items"][] = $res; + } + + if($club > 0) { + $cluber = (new Clubs)->get($club); + + if(!$cluber || !$cluber->canBeModifiedBy($this->user)) + $reject(1337, "Invalid (club), or you can't modify him"); + + $clubCount = (new Albums)->getClubAlbumsCount($cluber); + $clubAlbums = (new Albums)->getClubAlbums($cluber, 1, $clubCount); + + foreach($clubAlbums as $albumr) { + $res = ["id" => $albumr->getId(), "name" => $albumr->getName()]; + + $arr["items"][] = $res; + } + + $arr["count"] = $arr["count"] + $clubCount; + } + + $resolve($arr); + } +} diff --git a/ServiceAPI/Wall.php b/ServiceAPI/Wall.php index 5677f7ba..787a998e 100644 --- a/ServiceAPI/Wall.php +++ b/ServiceAPI/Wall.php @@ -2,7 +2,7 @@ namespace openvk\ServiceAPI; use openvk\Web\Models\Entities\Post; use openvk\Web\Models\Entities\User; -use openvk\Web\Models\Repositories\{Posts, Notes}; +use openvk\Web\Models\Repositories\{Posts, Notes, Videos}; class Wall implements Handler { @@ -15,6 +15,7 @@ class Wall implements Handler $this->user = $user; $this->posts = new Posts; $this->notes = new Notes; + $this->videos = new Videos; } function getPost(int $id, callable $resolve, callable $reject): void @@ -95,4 +96,45 @@ class Wall implements Handler $resolve($arr); } + + function getVideos(int $page = 1, callable $resolve, callable $reject) + { + $videos = $this->videos->getByUser($this->user, $page, 8); + $count = $this->videos->getUserVideosCount($this->user); + + $arr = [ + "count" => $count, + "items" => [], + ]; + + foreach($videos as $video) { + $res = json_decode(json_encode($video->toVkApiStruct()), true); + $res["video"]["author_name"] = $video->getOwner()->getCanonicalName(); + + $arr["items"][] = $res; + } + + $resolve($arr); + } + + function searchVideos(int $page = 1, string $query, callable $resolve, callable $reject) + { + $dbc = $this->videos->find($query); + $videos = $dbc->page($page, 8); + $count = $dbc->size(); + + $arr = [ + "count" => $count, + "items" => [], + ]; + + foreach($videos as $video) { + $res = json_decode(json_encode($video->toVkApiStruct()), true); + $res["video"]["author_name"] = $video->getOwner()->getCanonicalName(); + + $arr["items"][] = $res; + } + + $resolve($arr); + } } diff --git a/VKAPI/Handlers/Wall.php b/VKAPI/Handlers/Wall.php index ca65e961..61c75703 100644 --- a/VKAPI/Handlers/Wall.php +++ b/VKAPI/Handlers/Wall.php @@ -471,28 +471,25 @@ final class Wall extends VKAPIRequestHandler if($attachmentType == "photo") { $attacc = (new PhotosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); if(!$attacc || $attacc->isDeleted()) - $this->fail(100, "Photo does not exists"); - if($attacc->getOwner()->getId() != $this->getUser()->getId()) - $this->fail(43, "You do not have access to this photo"); + $this->fail(100, "Invalid photo"); + if(!$attacc->getOwner()->getPrivacyPermission('photos.read', $this->getUser())) + $this->fail(43, "Access to photo denied"); $post->attach($attacc); } elseif($attachmentType == "video") { $attacc = (new VideosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); if(!$attacc || $attacc->isDeleted()) $this->fail(100, "Video does not exists"); - if($attacc->getOwner()->getId() != $this->getUser()->getId()) - $this->fail(43, "You do not have access to this video"); + if(!$attacc->getOwner()->getPrivacyPermission('videos.read', $this->getUser())) + $this->fail(43, "Access to video denied"); $post->attach($attacc); } elseif($attachmentType == "note") { $attacc = (new NotesRepo)->getNoteById($attachmentOwner, $attachmentId); if(!$attacc || $attacc->isDeleted()) $this->fail(100, "Note does not exist"); - if($attacc->getOwner()->getId() != $this->getUser()->getId()) - $this->fail(43, "You do not have access to this note"); - - if($attacc->getOwner()->getPrivacySetting("notes.read") < 1) - $this->fail(11, "You can't attach note to post, because your notes list is closed. Change it in privacy settings in web-version."); + if(!$attacc->getOwner()->getPrivacyPermission('notes.read', $this->getUser())) + $this->fail(11, "Access to note denied"); $post->attach($attacc); } elseif($attachmentType == "poll") { @@ -695,7 +692,7 @@ final class Wall extends VKAPIRequestHandler return $response; } - function createComment(int $owner_id, int $post_id, string $message, int $from_group = 0, string $attachments = "") { + function createComment(int $owner_id, int $post_id, string $message = "", int $from_group = 0, string $attachments = "") { $this->requireUser(); $this->willExecuteWriteAction(); @@ -753,16 +750,16 @@ final class Wall extends VKAPIRequestHandler $attacc = (new PhotosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); if(!$attacc || $attacc->isDeleted()) $this->fail(100, "Photo does not exists"); - if($attacc->getOwner()->getId() != $this->getUser()->getId()) - $this->fail(43, "You do not have access to this photo"); + if(!$attacc->getOwner()->getPrivacyPermission('photos.read', $this->getUser())) + $this->fail(11, "Access to photo denied"); $comment->attach($attacc); } elseif($attachmentType == "video") { $attacc = (new VideosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); if(!$attacc || $attacc->isDeleted()) $this->fail(100, "Video does not exists"); - if($attacc->getOwner()->getId() != $this->getUser()->getId()) - $this->fail(43, "You do not have access to this video"); + if(!$attacc->getOwner()->getPrivacyPermission('videos.read', $this->getUser())) + $this->fail(11, "Access to video denied"); $comment->attach($attacc); } diff --git a/Web/Models/Entities/Club.php b/Web/Models/Entities/Club.php index 79ce08a4..46e8e747 100644 --- a/Web/Models/Entities/Club.php +++ b/Web/Models/Entities/Club.php @@ -224,7 +224,7 @@ class Club extends RowModel "shape" => "spline", "color" => "#597da3", ], - "name" => $unique ? "Полный охват" : "Все просмотры", + "name" => $unique ? tr("full_coverage") : tr("all_views"), ], "subs" => [ "x" => array_reverse(range(1, 7)), @@ -235,7 +235,7 @@ class Club extends RowModel "color" => "#b05c91", ], "fill" => "tozeroy", - "name" => $unique ? "Охват подписчиков" : "Просмотры подписчиков", + "name" => $unique ? tr("subs_coverage") : tr("subs_views"), ], "viral" => [ "x" => array_reverse(range(1, 7)), @@ -246,7 +246,7 @@ class Club extends RowModel "color" => "#4d9fab", ], "fill" => "tozeroy", - "name" => $unique ? "Виральный охват" : "Виральные просмотры", + "name" => $unique ? tr("viral_coverage") : tr("viral_views"), ], ]; } diff --git a/Web/Models/Entities/Comment.php b/Web/Models/Entities/Comment.php index fdfb6b8c..90057bdd 100644 --- a/Web/Models/Entities/Comment.php +++ b/Web/Models/Entities/Comment.php @@ -11,7 +11,7 @@ class Comment extends Post function getPrettyId(): string { - return $this->getRecord()->id; + return (string)$this->getRecord()->id; } function getVirtualId(): int @@ -90,7 +90,7 @@ class Comment extends Post { return "/wall" . $this->getTarget()->getPrettyId() . "#_comment" . $this->getId(); } - + function toNotifApiStruct() { $res = (object)[]; @@ -103,7 +103,7 @@ class Comment extends Post return $res; } - + function canBeEditedBy(?User $user = NULL): bool { if(!$user) diff --git a/Web/Models/Entities/IP.php b/Web/Models/Entities/IP.php index df2c9787..0d9b8fd0 100644 --- a/Web/Models/Entities/IP.php +++ b/Web/Models/Entities/IP.php @@ -105,7 +105,7 @@ class IP extends RowModel $this->stateChanges("ip", $ip); } - function save($log): void + function save(?bool $log = false): void { if(is_null($this->getRecord())) $this->stateChanges("first_seen", time()); diff --git a/Web/Models/Entities/Media.php b/Web/Models/Entities/Media.php index 9377f3e8..648d3564 100644 --- a/Web/Models/Entities/Media.php +++ b/Web/Models/Entities/Media.php @@ -121,14 +121,14 @@ abstract class Media extends Postable $this->stateChanges("hash", $hash); } - function save(): void + function save(?bool $log = false): void { if(!is_null($this->processingPlaceholder) && is_null($this->getRecord())) { $this->stateChanges("processed", 0); $this->stateChanges("last_checked", time()); } - parent::save(); + parent::save($log); } function delete(bool $softly = true): void diff --git a/Web/Models/Entities/Post.php b/Web/Models/Entities/Post.php index 38a9f850..a8d444eb 100644 --- a/Web/Models/Entities/Post.php +++ b/Web/Models/Entities/Post.php @@ -245,7 +245,7 @@ class Post extends Postable $this->unwire(); $this->save(); } - + function toNotifApiStruct() { $res = (object)[]; @@ -262,7 +262,7 @@ class Post extends Postable return $res; } - + function canBeEditedBy(?User $user = NULL): bool { if(!$user) diff --git a/Web/Models/Entities/Postable.php b/Web/Models/Entities/Postable.php index c86aec96..365c0f26 100644 --- a/Web/Models/Entities/Postable.php +++ b/Web/Models/Entities/Postable.php @@ -153,7 +153,7 @@ abstract class Postable extends Attachable throw new ISE("Setting virtual id manually is forbidden"); } - function save(): void + function save(?bool $log = false): void { $vref = $this->upperNodeReferenceColumnName; @@ -168,11 +168,11 @@ abstract class Postable extends Attachable $this->stateChanges("created", time()); $this->stateChanges("virtual_id", $pCount + 1); - } else { + } /*else { $this->stateChanges("edited", time()); - } + }*/ - parent::save(); + parent::save($log); } use Traits\TAttachmentHost; diff --git a/Web/Models/Entities/Traits/TAttachmentHost.php b/Web/Models/Entities/Traits/TAttachmentHost.php index cbe7cad2..db814cce 100644 --- a/Web/Models/Entities/Traits/TAttachmentHost.php +++ b/Web/Models/Entities/Traits/TAttachmentHost.php @@ -1,6 +1,7 @@ get($rel->attachable_id); } } + + function getChildrenWithLayout(int $w, int $h = -1): object + { + if($h < 0) + $h = $w; + + $children = $this->getChildren(); + $skipped = $photos = $result = []; + foreach($children as $child) { + if($child instanceof Photo) { + $photos[] = $child; + continue; + } + + $skipped[] = $child; + } + + $height = "unset"; + $width = $w; + if(sizeof($photos) < 2) { + if(isset($photos[0])) + $result[] = ["100%", "unset", $photos[0], "unset"]; + } else { + $mak = new Makima($photos); + $layout = $mak->computeMasonryLayout($w, $h); + $height = $layout->height; + $width = $layout->width; + for($i = 0; $i < sizeof($photos); $i++) { + $tile = $layout->tiles[$i]; + $result[] = [$tile->width . "px", $tile->height . "px", $photos[$i], "left"]; + } + } + + return (object) [ + "width" => $width . "px", + "height" => $height . "px", + "tiles" => $result, + "extras" => $skipped, + ]; + } function attach(Attachable $attachment): void { diff --git a/Web/Models/Entities/User.php b/Web/Models/Entities/User.php index e5b8da06..aaf00ec9 100644 --- a/Web/Models/Entities/User.php +++ b/Web/Models/Entities/User.php @@ -462,6 +462,7 @@ class User extends RowModel "news", "links", "poster", + "apps" ], ])->get($id); } @@ -1026,6 +1027,7 @@ class User extends RowModel "news", "links", "poster", + "apps" ], ])->set($id, (int) $status)->toInteger(); diff --git a/Web/Models/Repositories/Gifts.php b/Web/Models/Repositories/Gifts.php index f36b82a5..3baa2397 100644 --- a/Web/Models/Repositories/Gifts.php +++ b/Web/Models/Repositories/Gifts.php @@ -42,4 +42,10 @@ class Gifts foreach($cats as $cat) yield new GiftCategory($cat); } + + function getCategoriesCount(): int + { + $cats = $this->cats->where("deleted", false); + return $cats->count(); + } } diff --git a/Web/Models/Repositories/Photos.php b/Web/Models/Repositories/Photos.php index 88c7e804..0698c914 100644 --- a/Web/Models/Repositories/Photos.php +++ b/Web/Models/Repositories/Photos.php @@ -33,14 +33,26 @@ class Photos return new Photo($photo); } - function getEveryUserPhoto(User $user): \Traversable + function getEveryUserPhoto(User $user, int $page = 1, ?int $perPage = NULL): \Traversable { + $perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE; $photos = $this->photos->where([ - "owner" => $user->getId() - ]); + "owner" => $user->getId(), + "deleted" => 0 + ])->order("id DESC"); - foreach($photos as $photo) { + foreach($photos->page($page, $perPage) as $photo) { yield new Photo($photo); } } + + function getUserPhotosCount(User $user) + { + $photos = $this->photos->where([ + "owner" => $user->getId(), + "deleted" => 0 + ]); + + return sizeof($photos); + } } diff --git a/Web/Presenters/AdminPresenter.php b/Web/Presenters/AdminPresenter.php index f5c40bcc..14fbbc74 100644 --- a/Web/Presenters/AdminPresenter.php +++ b/Web/Presenters/AdminPresenter.php @@ -283,7 +283,7 @@ final class AdminPresenter extends OpenVKPresenter $this->notFound(); $gift->delete(); - $this->flashFail("succ", "Gift moved successfully", "This gift will now be in Recycle Bin."); + $this->flashFail("succ", tr("admin_gift_moved_successfully"), tr("admin_gift_moved_to_recycle")); break; case "copy": case "move": @@ -302,7 +302,7 @@ final class AdminPresenter extends OpenVKPresenter $catTo->addGift($gift); $name = $catTo->getName(); - $this->flash("succ", "Gift moved successfully", "This gift will now be in $name."); + $this->flash("succ", tr("admin_gift_moved_successfully"), "This gift will now be in $name."); $this->redirect("/admin/gifts/" . $catTo->getSlug() . "." . $catTo->getId() . "/"); break; default: @@ -333,10 +333,10 @@ final class AdminPresenter extends OpenVKPresenter $gift->setUsages((int) $this->postParam("usages")); if(isset($_FILES["pic"]) && $_FILES["pic"]["error"] === UPLOAD_ERR_OK) { if(!$gift->setImage($_FILES["pic"]["tmp_name"])) - $this->flashFail("err", "Не удалось сохранить подарок", "Изображение подарка кривое."); + $this->flashFail("err", tr("error_when_saving_gift"), tr("error_when_saving_gift_bad_image")); } else if($gen) { # If there's no gift pic but it's newly created - $this->flashFail("err", "Не удалось сохранить подарок", "Пожалуйста, загрузите изображение подарка."); + $this->flashFail("err", tr("error_when_saving_gift"), tr("error_when_saving_gift_no_image")); } $gift->save(); @@ -363,7 +363,7 @@ final class AdminPresenter extends OpenVKPresenter if (str_contains($this->queryParam("reason"), "*")) exit(json_encode([ "error" => "Incorrect reason" ])); - $unban_time = strtotime($this->queryParam("date")) ?: NULL; + $unban_time = strtotime($this->queryParam("date")) ?: "permanent"; $user = $this->users->get($id); if(!$user) diff --git a/Web/Presenters/AuthPresenter.php b/Web/Presenters/AuthPresenter.php index 23b55dc9..c6a7f143 100644 --- a/Web/Presenters/AuthPresenter.php +++ b/Web/Presenters/AuthPresenter.php @@ -1,7 +1,7 @@ authenticator->authenticate($chUser->getId()); - (new Logs)->create($user->getId(), "profiles", "openvk\\Web\\Models\\Entities\\User", 0, $user, $user, $_SERVER["REMOTE_ADDR"], $_SERVER["HTTP_USER_AGENT"]); $this->redirect("/id" . $user->getId()); $user->save(); } diff --git a/Web/Presenters/CommentPresenter.php b/Web/Presenters/CommentPresenter.php index cb0efd0d..e005af86 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}; +use openvk\Web\Models\Repositories\{Comments, Clubs, Videos, Photos}; final class CommentPresenter extends OpenVKPresenter { @@ -54,9 +54,6 @@ final class CommentPresenter extends OpenVKPresenter if ($entity instanceof Post && $entity->getWallOwner()->isBanned()) $this->flashFail("err", tr("error"), tr("forbidden")); - if($_FILES["_vid_attachment"] && OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading']) - $this->flashFail("err", tr("error"), "Video uploads are disabled by the system administrator."); - $flags = 0; if($this->postParam("as_group") === "on" && !is_null($club) && $club->canBeModifiedBy($this->user->identity)) $flags |= 0b10000000; @@ -66,31 +63,49 @@ final class CommentPresenter extends OpenVKPresenter try { $photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"]); } catch(ISE $ex) { - $this->flashFail("err", "Не удалось опубликовать пост", "Файл изображения повреждён, слишком велик или одна сторона изображения в разы больше другой."); + $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_when_publishing_comment_description")); } } - # TODO move to trait - try { - $photo = NULL; - $video = NULL; - if($_FILES["_pic_attachment"]["error"] === UPLOAD_ERR_OK) { - $album = NULL; - if($wall > 0 && $wall === $this->user->id) - $album = (new Albums)->getUserWallAlbum($wallOwner); - - $photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"], $album); + $photos = []; + if(!empty($this->postParam("photos"))) { + $un = rtrim($this->postParam("photos"), ","); + $arr = explode(",", $un); + + if(sizeof($arr) < 11) { + foreach($arr as $dat) { + $ids = explode("_", $dat); + $photo = (new Photos)->getByOwnerAndVID((int)$ids[0], (int)$ids[1]); + + if(!$photo || $photo->isDeleted()) + continue; + + $photos[] = $photo; + } } + } + + $videos = []; + + if(!empty($this->postParam("videos"))) { + $un = rtrim($this->postParam("videos"), ","); + $arr = explode(",", $un); - if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK) { - $video = Video::fastMake($this->user->id, $_FILES["_vid_attachment"]["name"], $this->postParam("text"), $_FILES["_vid_attachment"]); + if(sizeof($arr) < 11) { + foreach($arr as $dat) { + $ids = explode("_", $dat); + $video = (new Videos)->getByOwnerAndVID((int)$ids[0], (int)$ids[1]); + + if(!$video || $video->isDeleted()) + continue; + + $videos[] = $video; + } } - } catch(ISE $ex) { - $this->flashFail("err", "Не удалось опубликовать комментарий", "Файл медиаконтента повреждён или слишком велик."); } - if(empty($this->postParam("text")) && !$photo && !$video) - $this->flashFail("err", "Не удалось опубликовать комментарий", "Комментарий пустой или слишком большой."); + if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1) + $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_empty")); try { $comment = new Comment; @@ -102,14 +117,15 @@ final class CommentPresenter extends OpenVKPresenter $comment->setFlags($flags); $comment->save(); } catch (\LengthException $ex) { - $this->flashFail("err", "Не удалось опубликовать комментарий", "Комментарий слишком большой."); + $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_too_big")); } - if(!is_null($photo)) - $comment->attach($photo); + foreach($photos as $photo) + $comment->attach($photo); - if(!is_null($video)) - $comment->attach($video); + if(sizeof($videos) > 0) + foreach($videos as $vid) + $comment->attach($vid); if($entity->getOwner()->getId() !== $this->user->identity->getId()) if(($owner = $entity->getOwner()) instanceof User) @@ -124,7 +140,7 @@ final class CommentPresenter extends OpenVKPresenter if($mentionee instanceof User) (new MentionNotification($mentionee, $entity, $comment->getOwner(), strip_tags($comment->getText())))->emit(); - $this->flashFail("succ", "Комментарий добавлен", "Ваш комментарий появится на странице."); + $this->flashFail("succ", tr("comment_is_added"), tr("comment_is_added_desc")); } function renderDeleteComment(int $id): void @@ -135,15 +151,15 @@ final class CommentPresenter extends OpenVKPresenter $comment = (new Comments)->get($id); if(!$comment) $this->notFound(); if(!$comment->canBeDeletedBy($this->user->identity)) - $this->throwError(403, "Forbidden", "У вас недостаточно прав чтобы редактировать этот ресурс."); + $this->throwError(403, "Forbidden", tr("error_access_denied")); if ($comment->getTarget() instanceof Post && $comment->getTarget()->getWallOwner()->isBanned()) $this->flashFail("err", tr("error"), tr("forbidden")); $comment->delete(); $this->flashFail( "succ", - "Успешно", - "Этот комментарий больше не будет показыватся.
Отметить как спам?" + tr("success"), + tr("comment_will_not_appear") ); } } diff --git a/Web/Presenters/GiftsPresenter.php b/Web/Presenters/GiftsPresenter.php index 8f59bdcb..39359add 100644 --- a/Web/Presenters/GiftsPresenter.php +++ b/Web/Presenters/GiftsPresenter.php @@ -41,6 +41,7 @@ final class GiftsPresenter extends OpenVKPresenter $this->template->user = $user; $this->template->iterator = $cats; + $this->template->count = $this->gifts->getCategoriesCount(); $this->template->_template = "Gifts/Menu.xml"; } @@ -49,7 +50,7 @@ final class GiftsPresenter extends OpenVKPresenter $user = $this->users->get((int) ($this->queryParam("user") ?? 0)); $cat = $this->gifts->getCat((int) ($this->queryParam("pack") ?? 0)); if(!$user || !$cat) - $this->flashFail("err", "Не удалось подарить", "Пользователь или набор не существуют."); + $this->flashFail("err", tr("error_when_gifting"), tr("error_user_not_exists")); $this->template->page = $page = (int) ($this->queryParam("p") ?? 1); $gifts = $cat->getGifts($page, null, $this->template->count); @@ -66,14 +67,14 @@ final class GiftsPresenter extends OpenVKPresenter $gift = $this->gifts->get((int) ($this->queryParam("elid") ?? 0)); $cat = $this->gifts->getCat((int) ($this->queryParam("pack") ?? 0)); if(!$user || !$cat || !$gift || !$cat->hasGift($gift)) - $this->flashFail("err", "Не удалось подарить", "Не удалось подтвердить права на подарок."); + $this->flashFail("err", tr("error_when_gifting"), tr("error_no_rights_gifts")); if(!$gift->canUse($this->user->identity)) - $this->flashFail("err", "Не удалось подарить", "У вас больше не осталось таких подарков."); + $this->flashFail("err", tr("error_when_gifting"), tr("error_no_more_gifts")); $coinsLeft = $this->user->identity->getCoins() - $gift->getPrice(); if($coinsLeft < 0) - $this->flashFail("err", "Не удалось подарить", "Ору нищ не пук."); + $this->flashFail("err", tr("error_when_gifting"), tr("error_no_money")); $this->template->_template = "Gifts/Confirm.xml"; if($_SERVER["REQUEST_METHOD"] !== "POST") { @@ -91,7 +92,7 @@ final class GiftsPresenter extends OpenVKPresenter $user->gift($this->user->identity, $gift, $comment, !is_null($this->postParam("anonymous"))); $gift->used(); - $this->flash("succ", "Подарок отправлен", "Вы отправили подарок " . $user->getFirstName() . " за " . $gift->getPrice() . " голосов."); + $this->flash("succ", tr("gift_sent"), tr("gift_sent_desc", $user->getFirstName(), $gift->getPrice())); $this->redirect($user->getURL()); } diff --git a/Web/Presenters/GroupPresenter.php b/Web/Presenters/GroupPresenter.php index d8fbcb79..d3a46fd5 100644 --- a/Web/Presenters/GroupPresenter.php +++ b/Web/Presenters/GroupPresenter.php @@ -54,7 +54,7 @@ final class GroupPresenter extends OpenVKPresenter $club->save(); } catch(\PDOException $ex) { if($ex->getCode() == 23000) - $this->flashFail("err", "Ошибка", "Произошла ошибка на стороне сервера. Обратитесь к системному администратору."); + $this->flashFail("err", tr("error"), tr("error_on_server_side")); else throw $ex; } @@ -62,7 +62,7 @@ final class GroupPresenter extends OpenVKPresenter $club->toggleSubscription($this->user->identity); $this->redirect("/club" . $club->getId()); }else{ - $this->flashFail("err", "Ошибка", "Вы не ввели название группы."); + $this->flashFail("err", tr("error"), tr("error_no_group_name")); } } } @@ -132,7 +132,7 @@ final class GroupPresenter extends OpenVKPresenter $this->notFound(); if(!$club->canBeModifiedBy($this->user->identity ?? NULL)) - $this->flashFail("err", "Ошибка доступа", "У вас недостаточно прав, чтобы изменять этот ресурс."); + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); if(!is_null($hidden)) { if($club->getOwner()->getId() == $user->getId()) { @@ -150,9 +150,9 @@ final class GroupPresenter extends OpenVKPresenter } if($hidden) { - $this->flashFail("succ", "Операция успешна", "Теперь " . $user->getCanonicalName() . " будет показываться как обычный подписчик всем кроме других администраторов"); + $this->flashFail("succ", tr("success_action"), tr("x_is_now_hidden", $user->getCanonicalName())); } else { - $this->flashFail("succ", "Операция успешна", "Теперь все будут знать про то что " . $user->getCanonicalName() . " - администратор"); + $this->flashFail("succ", tr("success_action"), tr("x_is_now_showed", $user->getCanonicalName())); } } elseif($removeComment) { if($club->getOwner()->getId() == $user->getId()) { @@ -164,11 +164,11 @@ final class GroupPresenter extends OpenVKPresenter $manager->save(); } - $this->flashFail("succ", "Операция успешна", "Комментарий к администратору удален"); + $this->flashFail("succ", tr("success_action"), tr("comment_is_deleted")); } elseif($comment) { if(mb_strlen($comment) > 36) { $commentLength = (string) mb_strlen($comment); - $this->flashFail("err", "Ошибка", "Комментарий слишком длинный ($commentLength символов вместо 36 символов)"); + $this->flashFail("err", tr("error"), tr("comment_is_too_long", $commentLength)); } if($club->getOwner()->getId() == $user->getId()) { @@ -180,16 +180,16 @@ final class GroupPresenter extends OpenVKPresenter $manager->save(); } - $this->flashFail("succ", "Операция успешна", "Комментарий к администратору изменён"); + $this->flashFail("succ", tr("success_action"), tr("comment_is_changed")); }else{ if($club->canBeModifiedBy($user)) { $club->removeManager($user); - $this->flashFail("succ", "Операция успешна", $user->getCanonicalName() . " более не администратор."); + $this->flashFail("succ", tr("success_action"), tr("x_no_more_admin", $user->getCanonicalName())); } else { $club->addManager($user); (new ClubModeratorNotification($user, $club, $this->user->identity))->emit(); - $this->flashFail("succ", "Операция успешна", $user->getCanonicalName() . " назначен(а) администратором."); + $this->flashFail("succ", tr("success_action"), tr("x_is_admin", $user->getCanonicalName())); } } @@ -245,7 +245,7 @@ final class GroupPresenter extends OpenVKPresenter (new Albums)->getClubAvatarAlbum($club)->addPhoto($photo); } catch(ISE $ex) { $name = $album->getName(); - $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию."); + $this->flashFail("err", tr("error"), tr("error_when_uploading_photo")); } } @@ -253,12 +253,12 @@ final class GroupPresenter extends OpenVKPresenter $club->save(); } catch(\PDOException $ex) { if($ex->getCode() == 23000) - $this->flashFail("err", "Ошибка", "Произошла ошибка на стороне сервера. Обратитесь к системному администратору."); + $this->flashFail("err", tr("error"), tr("error_on_server_side")); else throw $ex; } - $this->flash("succ", "Изменения сохранены", "Новые данные появятся в вашей группе."); + $this->flash("succ", tr("changes_saved"), tr("new_changes_desc")); } } @@ -298,7 +298,7 @@ final class GroupPresenter extends OpenVKPresenter } catch(ISE $ex) { $name = $album->getName(); - $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию."); + $this->flashFail("err", tr("error"), tr("error_when_uploading_photo")); } } $this->returnJson([ @@ -350,7 +350,7 @@ final class GroupPresenter extends OpenVKPresenter $this->assertUserLoggedIn(); if(!eventdb()) - $this->flashFail("err", "Ошибка подключения", "Не удалось подключится к службе телеметрии."); + $this->flashFail("err", tr("connection_error"), tr("connection_error_desc")); $club = $this->clubs->get($id); if(!$club->canBeModifiedBy($this->user->identity)) diff --git a/Web/Presenters/InternalAPIPresenter.php b/Web/Presenters/InternalAPIPresenter.php index 1a107659..e2e6b50e 100644 --- a/Web/Presenters/InternalAPIPresenter.php +++ b/Web/Presenters/InternalAPIPresenter.php @@ -1,5 +1,6 @@ postParam("parentType", false) == "post") { + $post = (new Posts)->getPostById($owner_id, $post_id); + } else { + $post = (new Comments)->get($post_id); + } + + + if(is_null($post)) { + $this->returnJson([ + "success" => 0 + ]); + } else { + $response = []; + $attachments = $post->getChildren(); + foreach($attachments as $attachment) + { + if($attachment instanceof \openvk\Web\Models\Entities\Photo) + { + $response[] = [ + "url" => $attachment->getURLBySizeId('normal'), + "id" => $attachment->getPrettyId() + ]; + } + } + $this->returnJson([ + "success" => 1, + "body" => $response + ]); + } + } } diff --git a/Web/Presenters/NoSpamPresenter.php b/Web/Presenters/NoSpamPresenter.php index 1560ba63..8164d05e 100644 --- a/Web/Presenters/NoSpamPresenter.php +++ b/Web/Presenters/NoSpamPresenter.php @@ -177,26 +177,25 @@ final class NoSpamPresenter extends OpenVKPresenter if ($conditions) { $logs = $db->query("SELECT * FROM `ChandlerLogs` $whereStart $conditions GROUP BY `object_id`, `object_model`"); - if (!$where) { - foreach ($logs as $log) { - $log = (new Logs)->get($log->id); - $response[] = $log->getObject()->unwrap(); - } - } else { - foreach ($logs as $log) { - $log = (new Logs)->get($log->id); - $object = $log->getObject()->unwrap(); + foreach ($logs as $log) { + $log = (new Logs)->get($log->id); + $object = $log->getObject()->unwrap(); - if (!$object) continue; + if (!$object) continue; + if ($where) { if (str_starts_with($where, " AND")) { $where = substr_replace($where, "", 0, strlen(" AND")); } - foreach ($db->query("SELECT * FROM `$table` WHERE $where")->fetchAll() as $o) { - if ($object->id === $o["id"]) { + $a = $db->query("SELECT * FROM `$table` WHERE $where")->fetchAll(); + foreach ($a as $o) { + if ($object->id == $o["id"]) { $response[] = $object; } } + + } else { + $response[] = $object; } } } @@ -206,70 +205,72 @@ final class NoSpamPresenter extends OpenVKPresenter } try { - $response = []; - $processed = 0; + $response = []; + $processed = 0; - $where = $this->postParam("where"); - $ip = $this->postParam("ip"); - $useragent = $this->postParam("useragent"); - $searchTerm = $this->postParam("q"); - $ts = (int)$this->postParam("ts"); - $te = (int)$this->postParam("te"); - $user = $this->postParam("user"); + $where = $this->postParam("where"); + $ip = addslashes($this->postParam("ip")); + $useragent = addslashes($this->postParam("useragent")); + $searchTerm = addslashes($this->postParam("q")); + $ts = (int)$this->postParam("ts"); + $te = (int)$this->postParam("te"); + $user = addslashes($this->postParam("user")); - if (!$ip && !$useragent && !$searchTerm && !$ts && !$te && !$where && !$searchTerm && !$user) - $this->returnJson(["success" => false, "error" => "Нет запроса. Заполните поле \"подстрока\" или введите запрос \"WHERE\" в поле под ним."]); - - $models = explode(",", $this->postParam("models")); - - foreach ($models as $_model) { - $model_name = NoSpamPresenter::ENTITIES_NAMESPACE . "\\" . $_model; - if (!class_exists($model_name)) { - continue; + if ($where) { + $where = explode(";", $where)[0]; } - $model = new $model_name; + if (!$ip && !$useragent && !$searchTerm && !$ts && !$te && !$where && !$searchTerm && !$user) + $this->returnJson(["success" => false, "error" => "Нет запроса. Заполните поле \"подстрока\" или введите запрос \"WHERE\" в поле под ним."]); - $c = new \ReflectionClass($model_name); - if ($c->isAbstract() || $c->getName() == NoSpamPresenter::ENTITIES_NAMESPACE . "\\Correspondence") { - continue; - } + $models = explode(",", $this->postParam("models")); - $db = DatabaseConnection::i()->getContext(); - $table = $model->getTableName(); - $columns = $db->getStructure()->getColumns($table); - - if ($searchTerm) { - $conditions = []; - $need_deleted = false; - foreach ($columns as $column) { - if ($column["name"] == "deleted") { - $need_deleted = true; - } else { - $conditions[] = "`$column[name]` REGEXP '$searchTerm'"; - } + foreach ($models as $_model) { + $model_name = NoSpamPresenter::ENTITIES_NAMESPACE . "\\" . $_model; + if (!class_exists($model_name)) { + continue; } - $conditions = implode(" OR ", $conditions); - $where = ($this->postParam("where") ? " AND ($conditions)" : "($conditions)"); - if ($need_deleted) $where .= " AND (`deleted` = 0)"; - } + $model = new $model_name; - $rows = []; - if ($ip || $useragent || $ts || $te || $user) { - $rows = searchByAdditionalParams($table, $where, $ip, $useragent, $ts, $te, $user); - } + $c = new \ReflectionClass($model_name); + if ($c->isAbstract() || $c->getName() == NoSpamPresenter::ENTITIES_NAMESPACE . "\\Correspondence") { + continue; + } - if (count($rows) === 0) { - if (!$searchTerm) { - if (str_starts_with($where, " AND")) { - if ($searchTerm && !$this->postParam("where")) { - $where = substr_replace($where, "", 0, strlen(" AND")); + $db = DatabaseConnection::i()->getContext(); + $table = $model->getTableName(); + $columns = $db->getStructure()->getColumns($table); + + if ($searchTerm) { + $conditions = []; + $need_deleted = false; + foreach ($columns as $column) { + if ($column["name"] == "deleted") { + $need_deleted = true; } else { - $where = "(" . $this->postParam("where") . ")" . $where; + $conditions[] = "`$column[name]` REGEXP '$searchTerm'"; } } + $conditions = implode(" OR ", $conditions); + $where = ($this->postParam("where") ? " AND ($conditions)" : "($conditions)"); + if ($need_deleted) $where .= " AND (`deleted` = 0)"; + } + + $rows = []; + + if (str_starts_with($where, " AND")) { + if ($searchTerm && !$this->postParam("where")) { + $where = substr_replace($where, "", 0, strlen(" AND")); + } else { + $where = "(" . $this->postParam("where") . ")" . $where; + } + } + + if ($ip || $useragent || $ts || $te || $user) { + $rows = searchByAdditionalParams($table, $where, $ip, $useragent, $ts, $te, $user); + } else { if (!$where) { $rows = []; } else { @@ -277,99 +278,105 @@ final class NoSpamPresenter extends OpenVKPresenter $rows = $result->fetchAll(); } } - } - if (!in_array((int)$this->postParam("ban"), [1, 2, 3])) { - foreach ($rows as $key => $object) { - $object = (array)$object; - $_obj = []; - foreach ($object as $key => $value) { - foreach ($columns as $column) { - if ($column["name"] === $key && in_array(strtoupper($column["nativetype"]), ["BLOB", "BINARY", "VARBINARY", "TINYBLOB", "MEDIUMBLOB", "LONGBLOB"])) { - $value = "[BINARY]"; - break; - } - } - - $_obj[$key] = $value; - $_obj["__model_name"] = $_model; - } - $response[] = $_obj; - } - } else { - $ids = []; - - foreach ($rows as $object) { - $object = new $model_name($db->table($table)->get($object->id)); - if (!$object) continue; - $ids[] = $object->getId(); - } - - $log = new NoSpamLog; - $log->setUser($this->user->id); - $log->setModel($_model); - if ($searchTerm) { - $log->setRegex($searchTerm); - } else { - $log->setRequest($where); - } - $log->setBan_Type((int)$this->postParam("ban")); - $log->setCount(count($rows)); - $log->setTime(time()); - $log->setItems(implode(",", $ids)); - $log->save(); - - $banned_ids = []; - foreach ($rows as $object) { - $object = new $model_name($db->table($table)->get($object->id)); - if (!$object) continue; - - $owner = NULL; - $methods = ["getOwner", "getUser", "getRecipient", "getInitiator"]; - - if (method_exists($object, "ban")) { - $owner = $object; - } else { - foreach ($methods as $method) { - if (method_exists($object, $method)) { - $owner = $object->$method(); - break; - } - } - } - - if ($owner instanceof User && $owner->getId() === $this->user->id) { - if (count($rows) === 1) { - $this->returnJson(["success" => false, "error" => "\"Производственная травма\" — Вы не можете блокировать или удалять свой же контент"]); - } else { - continue; - } - } - - if (in_array((int)$this->postParam("ban"), [2, 3])) { - if ($owner) { - $_id = ($owner instanceof Club ? $owner->getId() * -1 : $owner->getId()); - if (!in_array($_id, $banned_ids)) { - if ($owner instanceof User) { - $owner->ban("**content-noSpamTemplate-" . $log->getId() . "**", false, time() + $owner->getNewBanTime(), $this->user->id); - } else { - $owner->ban("Подозрительная активность"); + if (!in_array((int)$this->postParam("ban"), [1, 2, 3])) { + foreach ($rows as $key => $object) { + $object = (array)$object; + $_obj = []; + foreach ($object as $key => $value) { + foreach ($columns as $column) { + if ($column["name"] === $key && in_array(strtoupper($column["nativetype"]), ["BLOB", "BINARY", "VARBINARY", "TINYBLOB", "MEDIUMBLOB", "LONGBLOB"])) { + $value = "[BINARY]"; + break; } - - $banned_ids[] = $_id; } + + $_obj[$key] = $value; + $_obj["__model_name"] = $_model; } + $response[] = $_obj; + } + } else { + $ids = []; + + foreach ($rows as $object) { + $object = new $model_name($db->table($table)->get($object->id)); + if (!$object) continue; + $ids[] = $object->getId(); } - if (in_array((int)$this->postParam("ban"), [1, 3])) - $object->delete(); + $log = new NoSpamLog; + $log->setUser($this->user->id); + $log->setModel($_model); + if ($searchTerm) { + $log->setRegex($searchTerm); + } else { + $log->setRequest($where); + } + $log->setBan_Type((int)$this->postParam("ban")); + $log->setCount(count($rows)); + $log->setTime(time()); + $log->setItems(implode(",", $ids)); + $log->save(); + + $banned_ids = []; + foreach ($rows as $object) { + $object = new $model_name($db->table($table)->get($object->id)); + if (!$object) continue; + + $owner = NULL; + $methods = ["getOwner", "getUser", "getRecipient", "getInitiator"]; + + if (method_exists($object, "ban")) { + $owner = $object; + } else { + foreach ($methods as $method) { + if (method_exists($object, $method)) { + $owner = $object->$method(); + break; + } + } + } + + if ($owner instanceof User && $owner->getId() === $this->user->id) { + if (count($rows) === 1) { + $this->returnJson(["success" => false, "error" => "\"Производственная травма\" — Вы не можете блокировать или удалять свой же контент"]); + } else { + continue; + } + } + + if (in_array((int)$this->postParam("ban"), [2, 3])) { + $reason = mb_strlen(trim($this->postParam("ban_reason"))) > 0 ? addslashes($this->postParam("ban_reason")) : ("**content-noSpamTemplate-" . $log->getId() . "**"); + $is_forever = (string)$this->postParam("is_forever") === "true"; + $unban_time = $is_forever ? 0 : (int)$this->postParam("unban_time") ?? NULL; + + if ($owner) { + $_id = ($owner instanceof Club ? $owner->getId() * -1 : $owner->getId()); + if (!in_array($_id, $banned_ids)) { + if ($owner instanceof User) { + if (!$unban_time && !$is_forever) + $unban_time = time() + $owner->getNewBanTime(); + + $owner->ban($reason, false, $unban_time, $this->user->id); + } else { + $owner->ban("Подозрительная активность"); + } + + $banned_ids[] = $_id; + } + } + } + + if (in_array((int)$this->postParam("ban"), [1, 3])) + $object->delete(); + } + + $processed++; } - - $processed++; } - } - $this->returnJson(["success" => true, "processed" => $processed, "count" => count($response), "list" => $response]); + $this->returnJson(["success" => true, "processed" => $processed, "count" => count($response), "list" => $response]); } catch (\Throwable $e) { $this->returnJson(["success" => false, "error" => $e->getMessage()]); } diff --git a/Web/Presenters/NotesPresenter.php b/Web/Presenters/NotesPresenter.php index 50437ad7..4b71c8b1 100644 --- a/Web/Presenters/NotesPresenter.php +++ b/Web/Presenters/NotesPresenter.php @@ -107,7 +107,7 @@ final class NotesPresenter extends OpenVKPresenter if(!$note || $note->getOwner()->getId() !== $owner || $note->isDeleted()) $this->notFound(); if(is_null($this->user) || !$note->canBeModifiedBy($this->user->identity)) - $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); $this->template->note = $note; if($_SERVER["REQUEST_METHOD"] === "POST") { @@ -135,11 +135,11 @@ final class NotesPresenter extends OpenVKPresenter if(!$note) $this->notFound(); if($note->getOwner()->getId() . "_" . $note->getId() !== $owner . "_" . $id || $note->isDeleted()) $this->notFound(); if(is_null($this->user) || !$note->canBeModifiedBy($this->user->identity)) - $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); $name = $note->getName(); $note->delete(); - $this->flash("succ", "Заметка удалена", "Заметка \"$name\" была успешно удалена."); + $this->flash("succ", tr("note_is_deleted"), tr("note_x_is_now_deleted", $name)); $this->redirect("/notes" . $this->user->id); } } diff --git a/Web/Presenters/PhotosPresenter.php b/Web/Presenters/PhotosPresenter.php index 9d64ba20..0a8b87e4 100644 --- a/Web/Presenters/PhotosPresenter.php +++ b/Web/Presenters/PhotosPresenter.php @@ -27,7 +27,7 @@ final class PhotosPresenter extends OpenVKPresenter if(!$user) $this->notFound(); if (!$user->getPrivacyPermission('photos.read', $this->user->identity ?? NULL)) $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); - $this->template->albums = $this->albums->getUserAlbums($user, $this->queryParam("p") ?? 1); + $this->template->albums = $this->albums->getUserAlbums($user, (int)($this->queryParam("p") ?? 1)); $this->template->count = $this->albums->getUserAlbumsCount($user); $this->template->owner = $user; $this->template->canEdit = false; @@ -36,7 +36,7 @@ final class PhotosPresenter extends OpenVKPresenter } else { $club = (new Clubs)->get(abs($owner)); if(!$club) $this->notFound(); - $this->template->albums = $this->albums->getClubAlbums($club, $this->queryParam("p") ?? 1); + $this->template->albums = $this->albums->getClubAlbums($club, (int)($this->queryParam("p") ?? 1)); $this->template->count = $this->albums->getClubAlbumsCount($club); $this->template->owner = $club; $this->template->canEdit = false; @@ -46,7 +46,7 @@ final class PhotosPresenter extends OpenVKPresenter $this->template->paginatorConf = (object) [ "count" => $this->template->count, - "page" => $this->queryParam("p") ?? 1, + "page" => (int)($this->queryParam("p") ?? 1), "amount" => NULL, "perPage" => OPENVK_DEFAULT_PER_PAGE, ]; @@ -94,7 +94,7 @@ final class PhotosPresenter extends OpenVKPresenter if(!$album) $this->notFound(); if($album->getPrettyId() !== $owner . "_" . $id || $album->isDeleted()) $this->notFound(); if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity) || $album->isDeleted()) - $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); $this->template->album = $album; if($_SERVER["REQUEST_METHOD"] === "POST") { @@ -106,7 +106,7 @@ final class PhotosPresenter extends OpenVKPresenter $album->setEdited(time()); $album->save(); - $this->flash("succ", "Изменения сохранены", "Новые данные приняты."); + $this->flash("succ", tr("changes_saved"), tr("new_data_accepted")); } } @@ -120,13 +120,13 @@ final class PhotosPresenter extends OpenVKPresenter if(!$album) $this->notFound(); if($album->getPrettyId() !== $owner . "_" . $id || $album->isDeleted()) $this->notFound(); if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity)) - $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); $name = $album->getName(); $owner = $album->getOwner(); $album->delete(); - $this->flash("succ", "Альбом удалён", "Альбом $name был успешно удалён."); + $this->flash("succ", tr("album_is_deleted"), tr("album_x_is_deleted", $name)); $this->redirect("/albums" . ($owner instanceof Club ? "-" : "") . $owner->getId()); } @@ -147,7 +147,7 @@ final class PhotosPresenter extends OpenVKPresenter $this->template->photos = iterator_to_array( $album->getPhotos( (int) ($this->queryParam("p") ?? 1), 20) ); $this->template->paginatorConf = (object) [ "count" => $album->getPhotosCount(), - "page" => $this->queryParam("p") ?? 1, + "page" => (int)($this->queryParam("p") ?? 1), "amount" => sizeof($this->template->photos), "perPage" => 20, "atBottom" => true @@ -205,13 +205,13 @@ final class PhotosPresenter extends OpenVKPresenter $photo = $this->photos->getByOwnerAndVID($ownerId, $photoId); if(!$photo) $this->notFound(); if(is_null($this->user) || $this->user->id != $ownerId) - $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); if($_SERVER["REQUEST_METHOD"] === "POST") { $photo->setDescription(empty($this->postParam("desc")) ? NULL : $this->postParam("desc")); $photo->save(); - $this->flash("succ", "Изменения сохранены", "Обновлённое описание появится на странице с фоткой."); + $this->flash("succ", tr("changes_saved"), tr("new_description_will_appear")); $this->redirect("/photo" . $photo->getPrettyId()); } @@ -221,39 +221,82 @@ final class PhotosPresenter extends OpenVKPresenter function renderUploadPhoto(): void { $this->assertUserLoggedIn(); - $this->willExecuteWriteAction(); - - if(is_null($this->queryParam("album"))) - $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в DELETED."); - - [$owner, $id] = explode("_", $this->queryParam("album")); - $album = $this->albums->get((int) $id); + $this->willExecuteWriteAction(true); + + if(is_null($this->queryParam("album"))) { + $album = $this->albums->getUserWallAlbum($this->user->identity); + } else { + [$owner, $id] = explode("_", $this->queryParam("album")); + $album = $this->albums->get((int) $id); + } + if(!$album) - $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в DELETED."); - if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity)) - $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); + $this->flashFail("err", tr("error"), tr("error_adding_to_deleted"), 500, true); + + # Для быстрой загрузки фоток из пикера фотографий нужен альбом, но юзер не может загружать фото + # в системные альбомы, так что так. + if(is_null($this->user) || !is_null($this->queryParam("album")) && !$album->canBeModifiedBy($this->user->identity)) + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"), 500, true); if($_SERVER["REQUEST_METHOD"] === "POST") { - if(!isset($_FILES["blob"])) - $this->flashFail("err", "Нету фотографии", "Выберите файл."); - - try { - $photo = new Photo; - $photo->setOwner($this->user->id); - $photo->setDescription($this->postParam("desc")); - $photo->setFile($_FILES["blob"]); - $photo->setCreated(time()); - $photo->save(); - } catch(ISE $ex) { - $name = $album->getName(); - $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в $name."); - } - - $album->addPhoto($photo); - $album->setEdited(time()); - $album->save(); + if($this->queryParam("act") == "finish") { + $result = json_decode($this->postParam("photos"), true); + + foreach($result as $photoId => $description) { + $phot = $this->photos->get($photoId); - $this->redirect("/photo" . $photo->getPrettyId() . "?from=album" . $album->getId()); + if(!$phot || $phot->isDeleted() || $phot->getOwner()->getId() != $this->user->id) + continue; + + if(iconv_strlen($description) > 255) + $this->flashFail("err", tr("error"), tr("description_too_long"), 500, true); + + $phot->setDescription($description); + $phot->save(); + + $album = $phot->getAlbum(); + } + + $this->returnJson(["success" => true, + "album" => $album->getId(), + "owner" => $album->getOwner() instanceof User ? $album->getOwner()->getId() : $album->getOwner()->getId() * -1]); + } + + if(!isset($_FILES)) + $this->flashFail("err", tr("no_photo"), tr("select_file"), 500, true); + + $photos = []; + if((int)$this->postParam("count") > 10) + $this->flashFail("err", tr("no_photo"), "ты еблан", 500, true); + + for($i = 0; $i < $this->postParam("count"); $i++) { + try { + $photo = new Photo; + $photo->setOwner($this->user->id); + $photo->setDescription(""); + $photo->setFile($_FILES["photo_".$i]); + $photo->setCreated(time()); + $photo->save(); + + $photos[] = [ + "url" => $photo->getURLBySizeId("tiny"), + "id" => $photo->getId(), + "vid" => $photo->getVirtualId(), + "owner" => $photo->getOwner()->getId(), + "link" => $photo->getURL() + ]; + } catch(ISE $ex) { + $name = $album->getName(); + $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в $name.", 500, true); + } + + $album->addPhoto($photo); + $album->setEdited(time()); + $album->save(); + } + + $this->returnJson(["success" => true, + "photos" => $photos]); } else { $this->template->album = $album; } @@ -269,7 +312,7 @@ final class PhotosPresenter extends OpenVKPresenter if(!$album || !$photo) $this->notFound(); if(!$album->hasPhoto($photo)) $this->notFound(); if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity)) - $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); if($_SERVER["REQUEST_METHOD"] === "POST") { $this->assertNoCSRF(); @@ -277,7 +320,7 @@ final class PhotosPresenter extends OpenVKPresenter $album->setEdited(time()); $album->save(); - $this->flash("succ", "Фотография удалена", "Эта фотография была успешно удалена."); + $this->flash("succ", tr("photo_is_deleted"), tr("photo_is_deleted_desc")); $this->redirect("/album" . $album->getPrettyId()); } } @@ -285,20 +328,23 @@ final class PhotosPresenter extends OpenVKPresenter function renderDeletePhoto(int $ownerId, int $photoId): void { $this->assertUserLoggedIn(); - $this->willExecuteWriteAction(); + $this->willExecuteWriteAction($_SERVER["REQUEST_METHOD"] === "POST"); $this->assertNoCSRF(); $photo = $this->photos->getByOwnerAndVID($ownerId, $photoId); if(!$photo) $this->notFound(); if(is_null($this->user) || $this->user->id != $ownerId) - $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); $redirect = $photo->getAlbum()->getOwner() instanceof User ? "/id0" : "/club" . $ownerId; $photo->isolate(); $photo->delete(); - $this->flash("succ", "Фотография удалена", "Эта фотография была успешно удалена."); + if($_SERVER["REQUEST_METHOD"] === "POST") + $this->returnJson(["success" => true]); + + $this->flash("succ", tr("photo_is_deleted"), tr("photo_is_deleted_desc")); $this->redirect($redirect); } } diff --git a/Web/Presenters/ReportPresenter.php b/Web/Presenters/ReportPresenter.php index 68d27861..a87154c8 100644 --- a/Web/Presenters/ReportPresenter.php +++ b/Web/Presenters/ReportPresenter.php @@ -118,22 +118,22 @@ final class ReportPresenter extends OpenVKPresenter $report->deleteContent(); $report->banUser($this->user->identity->getId()); - $this->flash("suc", "Смэрть...", "Пользователь успешно забанен."); + $this->flash("suc", tr("death"), tr("user_successfully_banned")); } else if ($this->postParam("delete")) { $report->deleteContent(); - $this->flash("suc", "Нехай живе!", "Контент удалён, а пользователю прилетело предупреждение."); + $this->flash("suc", tr("nehay"), tr("content_is_deleted")); } else if ($this->postParam("ignore")) { $report->delete(); - $this->flash("suc", "Нехай живе!", "Жалоба проигнорирована."); + $this->flash("suc", tr("nehay"), tr("report_is_ignored")); } else if ($this->postParam("banClubOwner") || $this->postParam("banClub")) { if ($report->getContentType() !== "group") - $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); $club = $report->getContentObject(); if (!$club || $club->isBanned()) - $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); if ($this->postParam("banClubOwner")) { $club->getOwner()->ban("**content-" . $report->getContentType() . "-" . $report->getContentId() . "**", false, $club->getOwner()->getNewBanTime(), $this->user->identity->getId()); @@ -143,7 +143,7 @@ final class ReportPresenter extends OpenVKPresenter $report->delete(); - $this->flash("suc", "Смэрть...", ($this->postParam("banClubOwner") ? "Создатель сообщества успешно забанен." : "Сообщество успешно забанено")); + $this->flash("suc", tr("death"), ($this->postParam("banClubOwner") ? tr("group_owner_is_banned") : tr("group_is_banned"))); } $this->redirect("/scumfeed"); diff --git a/Web/Presenters/TopicsPresenter.php b/Web/Presenters/TopicsPresenter.php index e7b08ac3..92d67e84 100644 --- a/Web/Presenters/TopicsPresenter.php +++ b/Web/Presenters/TopicsPresenter.php @@ -111,7 +111,7 @@ final class TopicsPresenter extends OpenVKPresenter $video = Video::fastMake($this->user->id, $_FILES["_vid_attachment"]["name"], $this->postParam("text"), $_FILES["_vid_attachment"]); } } catch(ISE $ex) { - $this->flash("err", "Не удалось опубликовать комментарий", "Файл медиаконтента повреждён или слишком велик."); + $this->flash("err", tr("error_when_publishing_comment"), tr("error_comment_file_too_big")); $this->redirect("/topic" . $topic->getPrettyId()); } @@ -126,7 +126,7 @@ final class TopicsPresenter extends OpenVKPresenter $comment->setFlags($flags); $comment->save(); } catch (\LengthException $ex) { - $this->flash("err", "Не удалось опубликовать комментарий", "Комментарий слишком большой."); + $this->flash("err", tr("error_when_publishing_comment"), tr("error_comment_too_big")); $this->redirect("/topic" . $topic->getPrettyId()); } diff --git a/Web/Presenters/UserPresenter.php b/Web/Presenters/UserPresenter.php index 9cfa3654..51ddc6aa 100644 --- a/Web/Presenters/UserPresenter.php +++ b/Web/Presenters/UserPresenter.php @@ -72,7 +72,7 @@ final class UserPresenter extends OpenVKPresenter if(!is_null($this->user)) { if($this->template->mode !== "friends" && $this->user->id !== $id) { $name = $user->getFullName(); - $this->flash("err", "Ошибка доступа", "Вы не можете просматривать полный список подписок $name."); + $this->flash("err", tr("error_access_denied_short"), tr("error_viewing_subs", $name)); $this->redirect($user->getURL()); } @@ -107,11 +107,11 @@ final class UserPresenter extends OpenVKPresenter $this->notFound(); if(!$club->canBeModifiedBy($this->user->identity ?? NULL)) - $this->flashFail("err", "Ошибка доступа", "У вас недостаточно прав, чтобы изменять этот ресурс.", NULL, true); + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"), NULL, true); $isClubPinned = $this->user->identity->isClubPinned($club); if(!$isClubPinned && $this->user->identity->getPinnedClubCount() > 10) - $this->flashFail("err", "Ошибка", "Находится в левом меню могут максимум 10 групп", NULL, true); + $this->flashFail("err", tr("error"), tr("error_max_pinned_clubs"), NULL, true); if($club->getOwner()->getId() === $this->user->identity->getId()) { $club->setOwner_Club_Pinned(!$isClubPinned); @@ -237,7 +237,7 @@ final class UserPresenter extends OpenVKPresenter } elseif($_GET['act'] === "status") { if(mb_strlen($this->postParam("status")) > 255) { $statusLength = (string) mb_strlen($this->postParam("status")); - $this->flashFail("err", "Ошибка", "Статус слишком длинный ($statusLength символов вместо 255 символов)", NULL, true); + $this->flashFail("err", tr("error"), tr("error_status_too_long", $statusLength), NULL, true); } $user->setStatus(empty($this->postParam("status")) ? NULL : $this->postParam("status")); @@ -281,7 +281,7 @@ final class UserPresenter extends OpenVKPresenter if($_SERVER["REQUEST_METHOD"] === "POST") { if(!$user->verifyNumber($this->postParam("code") ?? 0)) - $this->flashFail("err", "Ошибка", "Не удалось подтвердить номер телефона: неверный код."); + $this->flashFail("err", tr("error"), tr("invalid_code")); $this->flash("succ", tr("changes_saved"), tr("changes_saved_comment")); } @@ -481,6 +481,7 @@ final class UserPresenter extends OpenVKPresenter "menu_novajoj" => "news", "menu_ligiloj" => "links", "menu_standardo" => "poster", + "menu_aplikoj" => "apps" ]; foreach($settings as $checkbox => $setting) $user->setLeftMenuItemStatus($setting, $this->checkbox($checkbox)); diff --git a/Web/Presenters/VideosPresenter.php b/Web/Presenters/VideosPresenter.php index 4e4d484a..9d2fddc6 100644 --- a/Web/Presenters/VideosPresenter.php +++ b/Web/Presenters/VideosPresenter.php @@ -58,7 +58,7 @@ final class VideosPresenter extends OpenVKPresenter $this->willExecuteWriteAction(); if(OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading']) - $this->flashFail("err", tr("error"), "Video uploads are disabled by the system administrator."); + $this->flashFail("err", tr("error"), tr("video_uploads_disabled")); if($_SERVER["REQUEST_METHOD"] === "POST") { if(!empty($this->postParam("name"))) { @@ -74,18 +74,18 @@ final class VideosPresenter extends OpenVKPresenter else if(!empty($this->postParam("link"))) $video->setLink($this->postParam("link")); else - $this->flashFail("err", "Нету видеозаписи", "Выберите файл или укажите ссылку."); + $this->flashFail("err", tr("no_video"), tr("no_video_desc")); } catch(\DomainException $ex) { - $this->flashFail("err", "Произошла ошибка", "Файл повреждён или не содержит видео." ); + $this->flashFail("err", tr("error_occured"), tr("error_video_damaged_file")); } catch(ISE $ex) { - $this->flashFail("err", "Произошла ошибка", "Возможно, ссылка некорректна."); + $this->flashFail("err", tr("error_occured"), tr("error_video_incorrect_link")); } $video->save(); $this->redirect("/video" . $video->getPrettyId()); } else { - $this->flashFail("err", "Произошла ошибка", "Видео не может быть опубликовано без названия."); + $this->flashFail("err", tr("error_occured"), tr("error_video_no_title")); } } } @@ -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", "Ошибка доступа", "Вы не имеете права редактировать этот ресурс."); + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); 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", "Изменения сохранены", "Обновлённое описание появится на странице с видосиком."); + $this->flash("succ", tr("changes_saved"), tr("new_data_video")); $this->redirect("/video" . $video->getPrettyId()); } @@ -128,7 +128,7 @@ final class VideosPresenter extends OpenVKPresenter $video->deleteVideo($owner, $vid); } } else { - $this->flashFail("err", "Не удалось удалить пост", "Вы не вошли в аккаунт."); + $this->flashFail("err", tr("error_deleting_video"), tr("login_please")); } $this->redirect("/videos" . $owner); diff --git a/Web/Presenters/WallPresenter.php b/Web/Presenters/WallPresenter.php index 3e115ec7..2f9d611d 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}; +use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums, Notes, Videos, Comments, Photos}; use Chandler\Database\DatabaseConnection; use Nette\InvalidStateException as ISE; use Bhaktaraz\RSSGenerator\Item; @@ -231,10 +231,7 @@ final class WallPresenter extends OpenVKPresenter if(!$canPost) $this->flashFail("err", tr("not_enough_permissions"), tr("not_enough_permissions_comment")); - - if($_FILES["_vid_attachment"] && OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading']) - $this->flashFail("err", tr("error"), "Video uploads are disabled by the system administrator."); - + $anon = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"]; if($wallOwner instanceof Club && $this->postParam("as_group") === "on" && $this->postParam("force_sign") !== "on" && $anon) { $manager = $wallOwner->getManager($this->user->identity); @@ -252,23 +249,23 @@ final class WallPresenter extends OpenVKPresenter if($this->postParam("force_sign") === "on") $flags |= 0b01000000; - try { - $photo = NULL; - $video = NULL; - if($_FILES["_pic_attachment"]["error"] === UPLOAD_ERR_OK) { - $album = NULL; - if(!$anon && $wall > 0 && $wall === $this->user->id) - $album = (new Albums)->getUserWallAlbum($wallOwner); - - $photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"], $album, $anon); + $photos = []; + + if(!empty($this->postParam("photos"))) { + $un = rtrim($this->postParam("photos"), ","); + $arr = explode(",", $un); + + if(sizeof($arr) < 11) { + foreach($arr as $dat) { + $ids = explode("_", $dat); + $photo = (new Photos)->getByOwnerAndVID((int)$ids[0], (int)$ids[1]); + + if(!$photo || $photo->isDeleted()) + continue; + + $photos[] = $photo; + } } - - if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK) - $video = Video::fastMake($this->user->id, $_FILES["_vid_attachment"]["name"], $this->postParam("text"), $_FILES["_vid_attachment"], $anon); - } catch(\DomainException $ex) { - $this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted")); - } catch(ISE $ex) { - $this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted_or_too_large")); } try { @@ -295,8 +292,27 @@ final class WallPresenter extends OpenVKPresenter $this->flashFail("err", " "); } } + + $videos = []; + + if(!empty($this->postParam("videos"))) { + $un = rtrim($this->postParam("videos"), ","); + $arr = explode(",", $un); + + if(sizeof($arr) < 11) { + foreach($arr as $dat) { + $ids = explode("_", $dat); + $video = (new Videos)->getByOwnerAndVID((int)$ids[0], (int)$ids[1]); + + if(!$video || $video->isDeleted()) + continue; + + $videos[] = $video; + } + } + } - if(empty($this->postParam("text")) && !$photo && !$video && !$poll && !$note) + if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1 && !$poll && !$note) $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big")); try { @@ -313,11 +329,12 @@ final class WallPresenter extends OpenVKPresenter $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_too_big")); } - if(!is_null($photo)) - $post->attach($photo); + foreach($photos as $photo) + $post->attach($photo); - if(!is_null($video)) - $post->attach($video); + if(sizeof($videos) > 0) + foreach($videos as $vid) + $post->attach($vid); if(!is_null($poll)) $post->attach($poll); @@ -498,4 +515,64 @@ final class WallPresenter extends OpenVKPresenter # TODO localize message based on language and ?act=(un)pin $this->flashFail("succ", tr("information_-1"), tr("changes_saved_comment")); } + + function renderEdit() + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + + if($_SERVER["REQUEST_METHOD"] !== "POST") + $this->redirect("/id0"); + + if($this->postParam("type") == "post") + $post = $this->posts->get((int)$this->postParam("postid")); + else + $post = (new Comments)->get((int)$this->postParam("postid")); + + if(!$post || $post->isDeleted()) + $this->returnJson(["error" => "Invalid post"]); + + if(!$post->canBeEditedBy($this->user->identity)) + $this->returnJson(["error" => "Access denied"]); + + $attachmentsCount = sizeof(iterator_to_array($post->getChildren())); + + if(empty($this->postParam("newContent")) && $attachmentsCount < 1) + $this->returnJson(["error" => "Empty post"]); + + $post->setEdited(time()); + + try { + $post->setContent($this->postParam("newContent")); + } catch(\LengthException $e) { + $this->returnJson(["error" => $e->getMessage()]); + } + + if($this->postParam("type") === "post") { + $post->setNsfw($this->postParam("nsfw") == "true"); + $flags = 0; + + if($post->getTargetWall() < 0 && $post->getWallOwner()->canBeModifiedBy($this->user->identity)) { + if($this->postParam("fromgroup") == "true") { + $flags |= 0b10000000; + $post->setFlags($flags); + } else + $post->setFlags($flags); + } + } + + $post->save(true); + + $this->returnJson(["error" => "no", + "new_content" => $post->getText(), + "new_edited" => (string)$post->getEditTime(), + "nsfw" => $this->postParam("type") === "post" ? (int)$post->isExplicit() : 0, + "from_group" => $this->postParam("type") === "post" && $post->getTargetWall() < 0 ? + ((int)$post->isPostedOnBehalfOfGroup()) : "false", + "new_text" => $post->getText(false), + "author" => [ + "name" => $post->getOwner()->getCanonicalName(), + "avatar" => $post->getOwner()->getAvatarUrl() + ]]); + } } diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml index 3d724cf5..f8a975e0 100644 --- a/Web/Presenters/templates/@layout.xml +++ b/Web/Presenters/templates/@layout.xml @@ -38,9 +38,8 @@

- Вы вошли как {$thisUser->getCanonicalName()}. Пожалуйста, уважайте - право на тайну переписки других людей и не злоупотребляйте подменой пользователя. - Нажмите здесь, чтобы выйти. + {_you_entered_as} {$thisUser->getCanonicalName()}. {_please_rights} + {_click_on} {_there}, {_to_leave}.

@@ -196,7 +195,7 @@ ({$thisUser->getNotificationsCount()}) {/if} - {_my_apps} + {_my_apps} {_my_settings} {var $canAccessAdminPanel = $thisUser->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL)} diff --git a/Web/Presenters/templates/About/BB.xml b/Web/Presenters/templates/About/BB.xml index 17ab3b0f..9aa745b6 100644 --- a/Web/Presenters/templates/About/BB.xml +++ b/Web/Presenters/templates/About/BB.xml @@ -1,12 +1,10 @@ {extends "../@layout.xml"} -{block title}Ваш браузер устарел{/block} +{block title}{_deprecated_browser}{/block} {block header} - Устаревший браузер + {_deprecated_browser} {/block} {block content} - Для просмотра этого контента вам понадобится Firefox ESR 52+ или - эквивалентный по функционалу навигатор по всемирной сети интернет.
- Сожалеем об этом. + {_deprecated_browser_description} {/block} diff --git a/Web/Presenters/templates/About/Help.xml b/Web/Presenters/templates/About/Help.xml index 060f1381..64be74a0 100644 --- a/Web/Presenters/templates/About/Help.xml +++ b/Web/Presenters/templates/About/Help.xml @@ -9,5 +9,5 @@
Для кого этот сайт?
Сайт предназначен для поиска друзей и знакомых, а также просмотр данных пользователя. Это как справочник города, с помощью которого люди могут быстро найти актуальную информацию о человеке. Также этот сайт подойдёт для ностальгираторов и тех, кто решил слезть с трубы "ВКонтакте", которого клон и является.
Я попозже допишу ок ~~ veselcraft - 12.01.2020 - 22:05 GMT+3 - + Давай {/block} diff --git a/Web/Presenters/templates/About/Sandbox.xml b/Web/Presenters/templates/About/Sandbox.xml index b84b515a..1b548a48 100644 --- a/Web/Presenters/templates/About/Sandbox.xml +++ b/Web/Presenters/templates/About/Sandbox.xml @@ -2,7 +2,7 @@ {block title}Sandbox{/block} {block header} - Sandbox для разработчиков + {_sandbox_for_developers} {/block} {block content} diff --git a/Web/Presenters/templates/Admin/BansHistory.xml b/Web/Presenters/templates/Admin/BansHistory.xml index c0dc1b64..2144c949 100644 --- a/Web/Presenters/templates/Admin/BansHistory.xml +++ b/Web/Presenters/templates/Admin/BansHistory.xml @@ -1,7 +1,7 @@ {extends "./@layout.xml"} {block title} - История блокировок + {_bans_history} {/block} {block heading} @@ -13,13 +13,13 @@ ID - Забаненный - Инициатор - Начало - Конец - Время - Причина - Снята + {_bans_history_blocked} + {_bans_history_initiator} + {_bans_history_start} + {_bans_history_end} + {_bans_history_time} + {_bans_history_reason} + {_bans_history_removed} @@ -77,7 +77,7 @@ {_admin_banned} {else} - Активная блокировка + {_bans_history_active} {/if} diff --git a/Web/Presenters/templates/Admin/Logs.xml b/Web/Presenters/templates/Admin/Logs.xml index d953a378..ab5e62f5 100644 --- a/Web/Presenters/templates/Admin/Logs.xml +++ b/Web/Presenters/templates/Admin/Logs.xml @@ -1,11 +1,11 @@ {extends "@layout.xml"} {block title} - Логи + {_logs} {/block} {block heading} - Логи + {_logs} {/block} {block content} @@ -18,23 +18,23 @@
- + + + + + - - + +
- + - +
@@ -42,11 +42,11 @@ ID - Пользователь - Объект - Тип - Изменения - Время + {_logs_user} + {_logs_object} + {_logs_type} + {_logs_changes} + {_logs_time} diff --git a/Web/Presenters/templates/Apps/Play.xml b/Web/Presenters/templates/Apps/Play.xml index facaa273..93637a2d 100644 --- a/Web/Presenters/templates/Apps/Play.xml +++ b/Web/Presenters/templates/Apps/Play.xml @@ -7,7 +7,7 @@ {block header} {$name} - Пожаловаться + {_report} {/block} {block content} @@ -37,20 +37,20 @@ - {_open_original}
{/block} diff --git a/Web/Presenters/templates/Photos/UnlinkPhoto.xml b/Web/Presenters/templates/Photos/UnlinkPhoto.xml index 54498e06..80a4ad2e 100644 --- a/Web/Presenters/templates/Photos/UnlinkPhoto.xml +++ b/Web/Presenters/templates/Photos/UnlinkPhoto.xml @@ -1,20 +1,20 @@ {extends "../@layout.xml"} -{block title}Удалить фотографию?{/block} +{block title}{_delete_photo}{/block} {block header} - Удаление фотографии + {_delete_photo} {/block} {block content} - Вы уверены что хотите удалить эту фотографию? + {_sure_deleting_photo}

- Нет + {_no}   - +
{/block} diff --git a/Web/Presenters/templates/Photos/UploadPhoto.xml b/Web/Presenters/templates/Photos/UploadPhoto.xml index 9876e5b9..ab81d3aa 100644 --- a/Web/Presenters/templates/Photos/UploadPhoto.xml +++ b/Web/Presenters/templates/Photos/UploadPhoto.xml @@ -2,9 +2,13 @@ {block title}{_upload_photo}{/block} {block header} - {$thisUser->getCanonicalName()} + {$album->getOwner()->getCanonicalName()} » - {_albums} + {if $album->getOwner() instanceof openvk\Web\Models\Entities\Club} + {_albums} + {else} + {_albums} + {/if} » {$album->getName()} » @@ -12,32 +16,53 @@ {/block} {block content} -
- - - - - - - - - - - - - - - -
{_description}:
{_photo}: - -
-
- - -
- - -
+
+
+ {_edit_album} +
+
+ {_add_photos} +
+
+ + + +
+
+
+
+

{_uploading_photos_from_computer}

+ +
+ {_admin_limits} +
    +
  • {_supported_formats}
  • +
  • {_max_load_photos}
  • +
+ +
+ +
+ +
+ {_tip}: {_tip_ctrl} +
+
+
+
+ +
+ + +
+ + + + +{/block} + +{block bodyScripts} + {script "js/al_photos.js"} {/block} diff --git a/Web/Presenters/templates/User/Edit.xml b/Web/Presenters/templates/User/Edit.xml index 6df070ea..b8e8398c 100644 --- a/Web/Presenters/templates/User/Edit.xml +++ b/Web/Presenters/templates/User/Edit.xml @@ -344,9 +344,9 @@
- +
- +
diff --git a/Web/Presenters/templates/User/Settings.xml b/Web/Presenters/templates/User/Settings.xml index e61f900d..c8f0f61b 100644 --- a/Web/Presenters/templates/User/Settings.xml +++ b/Web/Presenters/templates/User/Settings.xml @@ -190,13 +190,13 @@ {if $graffiti} diff --git a/Web/Util/Makima/Makima.php b/Web/Util/Makima/Makima.php new file mode 100644 index 00000000..e31bfa42 --- /dev/null +++ b/Web/Util/Makima/Makima.php @@ -0,0 +1,305 @@ +photos = $photos; + } + + private function getOrientation(Photo $photo, &$ratio): int + { + [$width, $height] = $photo->getDimensions(); + $ratio = $width / $height; + if($ratio >= 1.2) + return Makima::ORIENT_WIDE; + else if($ratio >= 0.8) + return Makima::ORIENT_REGULAR; + else + return Makima::ORIENT_SLIM; + } + + private function calculateMultiThumbsHeight(array $ratios, float $w, float $m): float + { + return ($w - (sizeof($ratios) - 1) * $m) / array_sum($ratios); + } + + private function extractSubArr(array $arr, int $from, int $to): array + { + return array_slice($arr, $from, sizeof($arr) - $from - (sizeof($arr) - $to)); + } + + function computeMasonryLayout(float $maxWidth, float $maxHeight): MasonryLayout + { + $orients = []; + $ratios = []; + $count = sizeof($this->photos); + $result = new MasonryLayout; + + foreach($this->photos as $photo) { + $orients[] = $this->getOrientation($photo, $ratio); + $ratios[] = $ratio; + } + + $avgRatio = array_sum($ratios) / sizeof($ratios); + if($maxWidth < 0) + $maxWidth = $maxHeight = 510; + + $maxRatio = $maxWidth / $maxHeight; + $marginWidth = $marginHeight = 2; + + switch($count) { + case 2: + if( + $orients == [Makima::ORIENT_WIDE, Makima::ORIENT_WIDE] # two wide pics + && $avgRatio > (1.4 * $maxRatio) && abs($ratios[0] - $ratios[1]) < 0.2 # that can be positioned on top of each other + ) { + $computedHeight = ceil( min( $maxWidth / $ratios[0], min( $maxWidth / $ratios[1], ($maxHeight - $marginHeight) / 2 ) ) ); + + $result->colSizes = [1]; + $result->rowSizes = [1, 1]; + $result->width = ceil($maxWidth); + $result->height = $computedHeight; + $result->tiles = [new ThumbTile(1, 1, $maxWidth, $computedHeight), new ThumbTile(1, 1, $maxWidth, $computedHeight)]; + } else if( + $orients == [Makima::ORIENT_WIDE, Makima::ORIENT_WIDE] + || $orients == [Makima::ORIENT_REGULAR, Makima::ORIENT_REGULAR] # two normal pics of same ratio + ) { + $computedWidth = ($maxWidth - $marginWidth) / 2; + $height = min( $computedWidth / $ratios[0], min( $computedWidth / $ratios[1], $maxHeight ) ); + + $result->colSizes = [1, 1]; + $result->rowSizes = [1]; + $result->width = ceil($maxWidth); + $result->height = ceil($height); + $result->tiles = [new ThumbTile(1, 1, $computedWidth, $height), new ThumbTile(1, 1, $computedWidth, $height)]; + } else /* next to each other, different ratios */ { + $w0 = ( + ($maxWidth - $marginWidth) / $ratios[1] / ( (1 / $ratios[0]) + (1 / $ratios[1]) ) + ); + $w1 = $maxWidth - $w0 - $marginWidth; + $h = min($maxHeight, min($w0 / $ratios[0], $w1 / $ratios[1])); + + $result->colSizes = [ceil($w0), ceil($w1)]; + $result->rowSizes = [1]; + $result->width = ceil($w0 + $w1 + $marginWidth); + $result->height = ceil($h); + $result->tiles = [new ThumbTile(1, 1, $w0, $h), new ThumbTile(1, 1, $w1, $h)]; + } + break; + case 3: + # Three wide photos, we will put two of them below and one on top + if($orients == [Makima::ORIENT_WIDE, Makima::ORIENT_WIDE, Makima::ORIENT_WIDE]) { + $hCover = min($maxWidth / $ratios[0], ($maxHeight - $marginHeight) * (2 / 3)); + $w2 = ($maxWidth - $marginWidth) / 2; + $h = min($maxHeight - $hCover - $marginHeight, min($w2 / $ratios[1], $w2 / $ratios[2])); + + $result->colSizes = [1, 1]; + $result->rowSizes = [ceil($hCover), ceil($h)]; + $result->width = ceil($maxWidth); + $result->height = ceil($marginHeight + $hCover + $h); + $result->tiles = [ + new ThumbTile(2, 1, $maxWidth, $hCover), + new ThumbTile(1, 1, $w2, $h), new ThumbTile(1, 1, $w2, $h), + ]; + } else /* Photos have different sizes or are not wide, so we will put one to left and two to the right */ { + $wCover = min($maxHeight * $ratios[0], ($maxWidth - $marginWidth) * (3 / 4)); + $h1 = ($ratios[1] * ($maxHeight - $marginHeight) / ($ratios[2] + $ratios[1])); + $h0 = $maxHeight - $marginHeight - $h1; + $w = min($maxWidth - $marginWidth - $wCover, min($h1 * $ratios[2], $h0 * $ratios[1])); + + $result->colSizes = [ceil($wCover), ceil($w)]; + $result->rowSizes = [ceil($h0), ceil($h1)]; + $result->width = ceil($w + $wCover + $marginWidth); + $result->height = ceil($maxHeight); + $result->tiles = [ + new ThumbTile(1, 2, $wCover, $maxHeight), new ThumbTile(1, 1, $w, $h0), + new ThumbTile(1, 1, $w, $h1), + ]; + } + break; + case 4: + # Four wide photos, we will put one to the top and rest below + if($orients == [Makima::ORIENT_WIDE, Makima::ORIENT_WIDE, Makima::ORIENT_WIDE, Makima::ORIENT_WIDE]) { + $hCover = min($maxWidth / $ratios[0], ($maxHeight - $marginHeight) / (2 / 3)); + $h = ($maxWidth - 2 * $marginWidth) / (array_sum($ratios) - $ratios[0]); + $w0 = $h * $ratios[1]; + $w1 = $h * $ratios[2]; + $w2 = $h * $ratios[3]; + $h = min($maxHeight - $marginHeight - $hCover, $h); + + $result->colSizes = [ceil($w0), ceil($w1), ceil($w2)]; + $result->rowSizes = [ceil($hCover), ceil($h)]; + $result->width = ceil($maxWidth); + $result->height = ceil($hCover + $marginHeight + $h); + $result->tiles = [ + new ThumbTile(3, 1, $maxWidth, $hCover), + new ThumbTile(1, 1, $w0, $h), new ThumbTile(1, 1, $w1, $h), new ThumbTile(1, 1, $w2, $h), + ]; + } else /* Four photos, we will put one to the left and rest to the right */ { + $wCover = min($maxHeight * $ratios[0], ($maxWidth - $marginWidth) * (2 / 3)); + $w = ($maxHeight - 2 * $marginHeight) / (1 / $ratios[1] + 1 / $ratios[2] + 1 / $ratios[3]); + $h0 = $w / $ratios[1]; + $h1 = $w / $ratios[2]; + $h2 = $w / $ratios[3] + $marginHeight; + $w = min($w, $maxWidth - $marginWidth - $wCover); + + $result->colSizes = [ceil($wCover), ceil($w)]; + $result->rowSizes = [ceil($h0), ceil($h1), ceil($h2)]; + $result->width = ceil($wCover + $marginWidth + $w); + $result->height = ceil($maxHeight); + $result->tiles = [ + new ThumbTile(1, 3, $wCover, $maxHeight), new ThumbTile(1, 1, $w, $h0), + new ThumbTile(1, 1, $w, $h1), + new ThumbTile(1, 1, $w, $h1), + ]; + } + break; + default: + // как лопать пузырики + $ratiosCropped = []; + if($avgRatio > 1.1) { + foreach($ratios as $ratio) + $ratiosCropped[] = max($ratio, 1.0); + } else { + foreach($ratios as $ratio) + $ratiosCropped[] = min($ratio, 1.0); + } + + $tries = []; + + $firstLine; + $secondLine; + $thirdLine; + + # Try one line: + $tries[$firstLine = $count] = [$this->calculateMultiThumbsHeight($ratiosCropped, $maxWidth, $marginWidth)]; + + # Try two lines: + for($firstLine = 1; $firstLine < ($count - 1); $firstLine++) { + $secondLine = $count - $firstLine; + $key = "$firstLine&$secondLine"; + $tries[$key] = [ + $this->calculateMultiThumbsHeight(array_slice($ratiosCropped, 0, $firstLine), $maxWidth, $marginWidth), + $this->calculateMultiThumbsHeight(array_slice($ratiosCropped, $firstLine), $maxWidth, $marginWidth), + ]; + } + + # Try three lines: + for($firstLine = 1; $firstLine < ($count - 2); $firstLine++) { + for($secondLine = 1; $secondLine < ($count - $firstLine - 1); $secondLine++) { + $thirdLine = $count - $firstLine - $secondLine; + $key = "$firstLine&$secondLine&$thirdLine"; + $tries[$key] = [ + $this->calculateMultiThumbsHeight(array_slice($ratiosCropped, 0, $firstLine), $maxWidth, $marginWidth), + $this->calculateMultiThumbsHeight($this->extractSubArr($ratiosCropped, $firstLine, $firstLine + $secondLine), $maxWidth, $marginWidth), + $this->calculateMultiThumbsHeight($this->extractSubArr($ratiosCropped, $firstLine + $secondLine, sizeof($ratiosCropped)), $maxWidth, $marginWidth), + ]; + } + } + + # Now let's find the most optimal configuration: + $optimalConfiguration = $optimalDifference = NULL; + foreach($tries as $config => $heights) { + $config = explode('&', (string) $config); # да да стринговые ключи пхп даже со стриктайпами автокастует к инту (см. 187) + $confH = $marginHeight * (sizeof($heights) - 1); + foreach($heights as $h) + $confH += $h; + + $confDiff = abs($confH - $maxHeight); + if(sizeof($config) > 1) + if($config[0] > $config[1] || sizeof($config) >= 2 && $config[1] > $config[2]) + $confDiff *= 1.1; + + if(!$optimalConfiguration || $confDigff < $optimalDifference) { + $optimalConfiguration = $config; + $optimalDifference = $confDiff; + } + } + + $thumbsRemain = $this->photos; + $ratiosRemain = $ratiosCropped; + $optHeights = $tries[implode('&', $optimalConfiguration)]; + $k = 0; + + $result->width = ceil($maxWidth); + $result->rowSizes = [sizeof($optHeights)]; + $result->tiles = []; + + $totalHeight = 0.0; + $gridLineOffsets = []; + $rowTiles = []; // vector> + + for($i = 0; $i < sizeof($optimalConfiguration); $i++) { + $lineChunksNum = $optimalConfiguration[$i]; + $lineThumbs = []; + for($j = 0; $j < $lineChunksNum; $j++) + $lineThumbs[] = array_shift($thumbsRemain); + + $lineHeight = $optHeights[$i]; + $totalHeight += $lineHeight; + + $result->rowSizes[$i] = ceil($lineHeight); + + $totalWidth = 0; + $row = []; + for($j = 0; $j < sizeof($lineThumbs); $j++) { + $thumbRatio = array_shift($ratiosRemain); + if($j == sizeof($lineThumbs) - 1) + $w = $maxWidth - $totalWidth; + else + $w = $thumbRatio * $lineHeight; + + $totalWidth += ceil($w); + if($j < (sizeof($lineThumbs) - 1) && !in_array($totalWidth, $gridLineOffsets)) + $gridLineOffsets[] = $totalWidth; + + $tile = new ThumbTile(1, 1, $w, $lineHeight); + $result->tiles[$k++] = $row[] = $tile; + } + + $rowTiles[] = $row; + } + + sort($gridLineOffsets, SORT_NUMERIC); + $gridLineOffsets[] = $maxWidth; + + $result->colSizes = [$gridLineOffsets[0]]; + for($i = sizeof($gridLineOffsets) - 1; $i > 0; $i--) + $result->colSizes[$i] = $gridLineOffsets[$i] - $gridLineOffsets[$i - 1]; + + foreach($rowTiles as $row) { + $columnOffset = 0; + foreach($row as $tile) { + $startColumn = $columnOffset; + $width = 0; + $tile->colSpan = 0; + for($i = $startColumn; $i < sizeof($result->colSizes); $i++) { + $width += $result->colSizes[$i]; + $tile->colSpan++; + if($width == $tile->width) + break; + } + + $columnOffset += $tile->colSpan; + } + } + + $result->height = ceil($totalHeight + $marginHeight * (sizeof($optHeights) - 1)); + break; + } + + return $result; + } +} diff --git a/Web/Util/Makima/MasonryLayout.php b/Web/Util/Makima/MasonryLayout.php new file mode 100644 index 00000000..b23aa483 --- /dev/null +++ b/Web/Util/Makima/MasonryLayout.php @@ -0,0 +1,10 @@ +width, $this->height, $this->rowSpan, $this->colSpan] = [ceil($w), ceil($h), $rs, $cs]; + } +} diff --git a/Web/routes.yml b/Web/routes.yml index effa68eb..dea8ddd5 100644 --- a/Web/routes.yml +++ b/Web/routes.yml @@ -129,6 +129,8 @@ routes: handler: "Wall->rss" - url: "/wall{num}/makePost" handler: "Wall->makePost" + - url: "/wall/edit" + handler: "Wall->edit" - url: "/wall{num}_{num}" handler: "Wall->post" - url: "/wall{num}_{num}/like" @@ -369,6 +371,8 @@ routes: handler: "About->humansTxt" - url: "/dev" handler: "About->dev" + - url: "/iapi/getPhotosFromPost/{num}_{num}" + handler: "InternalAPI->getPhotosFromPost" - url: "/tour" handler: "About->tour" - url: "/{?shortCode}" diff --git a/Web/static/css/main.css b/Web/static/css/main.css index 55484f13..47d67317 100644 --- a/Web/static/css/main.css +++ b/Web/static/css/main.css @@ -744,10 +744,14 @@ h4 { line-height: 130%; } -.post-content .attachments_b { +.post-content .attachments:first-of-type { margin-top: 8px; } +.post-content .attachments_m .attachment { + width: 98%; +} + .attachment .post { width: 102%; } @@ -757,6 +761,12 @@ h4 { image-rendering: -webkit-optimize-contrast; } +.post-content .media_makima { + width: calc(100% - 4px); + height: calc(100% - 4px); + object-fit: cover; +} + .post-signature { margin: 4px; margin-bottom: 2px; @@ -1466,6 +1476,12 @@ body.scrolled .toTop:hover { display: none; } +.post-has-videos { + margin-top: 11px; + margin-left: 3px; + color: #3c3c3c; +} + .post-upload::before, .post-has-poll::before, .post-has-note::before { content: " "; width: 8px; @@ -1477,6 +1493,28 @@ body.scrolled .toTop:hover { margin-left: 2px; } +.post-has-video { + padding-bottom: 4px; + cursor: pointer; +} + +.post-has-video:hover span { + text-decoration: underline; +} + +.post-has-video::before { + content: " "; + width: 14px; + height: 15px; + display: inline-block; + vertical-align: bottom; + background-image: url("/assets/packages/static/openvk/img/video.png"); + background-repeat: no-repeat; + margin: 3px; + margin-left: 2px; + margin-bottom: -1px; +} + .post-opts { margin-top: 10px; } @@ -2251,6 +2289,124 @@ a.poll-retract-vote { border-radius: 1px; } +.progress { + border: 1px solid #eee; + height: 15px; + background: linear-gradient(to bottom, #fefefe, #fafafa); +} + +.progress .progress-bar { + background: url('progress.png'); + background-repeat: repeat-x; + height: 15px; + animation-name: progress; + animation-duration: 1s; + animation-iteration-count: infinite; + animation-timing-function: linear; +} + +@keyframes progress { + from { + background-position: 0; + } + + to { + background-position: 20px; + } +} + +.upload { + margin-top: 8px; +} + +.upload .upload-item { + width: 75px; + height: 60px; + overflow: hidden; + display: inline-block; + margin-right: 3px; +} + +.upload-item .upload-delete { + position: absolute; + background: rgba(0,0,0,0.5); + padding: 2px 5px; + text-decoration: none; + color: #fff; + font-size: 11px; + margin-left: 57px; /* мне лень переделывать :DDDD */ + opacity: 0; + transition: 0.25s; +} + +.upload-item:hover > .upload-delete { + opacity: 1; +} + +.upload-item img { + width: 100%; + max-height: 60px; + object-fit: cover; + border-radius: 3px; +} + +/* https://imgur.com/a/ihB3JZ4 */ + +.ovk-photo-view-dimmer { + position: fixed; + left: 0px; + top: 0px; + right: 0px; + bottom: 0px; + overflow: auto; + padding-bottom: 20px; + z-index: 300; +} + +.ovk-photo-view { + position: relative; + z-index: 999; + background: #fff; + width: 610px; + padding: 20px; + padding-top: 15px; + padding-bottom: 10px; + box-shadow: 0px 0px 3px 1px #222; + margin: 15px auto 0 auto; +} + +.ovk-photo-details { + overflow: auto; +} + +.photo_com_title { + font-weight: bold; + padding-bottom: 20px; +} + +.photo_com_title div { + float: right; + font-weight: normal; +} + +.ovk-photo-slide-left { + left: 0; + width: 35%; + height: 100%; + max-height: 60vh; + position: absolute; + cursor: pointer; +} + +.ovk-photo-slide-right { + right: 0; + width: 35%; + height: 100%; + max-height: 60vh; + position: absolute; + cursor: pointer; +} + .client_app > img { top: 3px; position: relative; @@ -2700,4 +2856,121 @@ body.article .floating_sidebar, body.article .page_content { position: absolute; right: 22px; font-size: 12px; -} \ No newline at end of file +} + +.topGrayBlock { + background: #F0F0F0; + height: 37px; + border-bottom: 1px solid #C7C7C7; +} + +.edited { + color: #9b9b9b; +} + +.uploadedImage img { + max-height: 76px; + object-fit: cover; +} + +.lagged { + filter: opacity(0.5); + cursor: progress; + user-select: none; +} + +.editMenu.loading { + filter: opacity(0.5); + cursor: progress; + user-select: none; +} + +.editMenu.loading * { + pointer-events: none; +} + +.lagged * { + pointer-events: none; +} + +.button.dragged { + background: #c4c4c4 !important; + border-color: #c4c4c4 !important; + color: black !important; +} + +.whiteBox { + background: white; + width: 421px; + margin-left: auto; + margin-right: auto; + border: 1px solid #E8E8E8; + margin-top: 7%; + height: 231px; +} + +.boxContent { + padding: 24px 38px; +} + +.blueList { + list-style-type: none; +} + +.blueList li { + color: black; + font-size: 11px; + padding-top: 7px; +} + +.blueList li::before { + content: " "; + width: 5px; + height: 5px; + display: inline-block; + vertical-align: bottom; + background-color: #73889C; + margin: 3px; + margin-left: 2px; + margin-right: 7px; +} + +.insertedPhoto { + background: white; + border: 1px solid #E8E7EA; + padding: 10px; + height: 100px; + margin-top: 6px; +} + +.uploadedImage { + float: right; + display: flex; + flex-direction: column; +} + +.uploadedImageDescription { + width: 449px; +} + +.uploadedImageDescription textarea { + width: 84%; + height: 86px; +} + +.smallFrame { + border: 1px solid #E1E3E5; + background: #F0F0F0; + height: 33px; + text-align: center; + cursor: pointer; +} + +.smallFrame .smallBtn { + margin-top: 10px; +} + +.smallFrame:hover { + background: #E9F0F1 !important; +} + diff --git a/Web/static/css/microblog.css b/Web/static/css/microblog.css index bf5d0d53..503af42a 100644 --- a/Web/static/css/microblog.css +++ b/Web/static/css/microblog.css @@ -110,10 +110,24 @@ transition-duration: 0.3s; } +.post-author .edit { + float: right; + height: 16px; + width: 16px; + overflow: auto; + background: url("/assets/packages/static/openvk/img/edit.png") no-repeat 0 0; + opacity: 0.1; + transition-duration: 0.3s; +} + .post-author .pin:hover { opacity: 0.4; } +.post-author .edit:hover { + opacity: 0.4; +} + .expand_button { background-color: #eee; width: 100%; diff --git a/Web/static/img/edit.png b/Web/static/img/edit.png new file mode 100644 index 00000000..b3474d0f Binary files /dev/null and b/Web/static/img/edit.png differ diff --git a/Web/static/img/video.png b/Web/static/img/video.png new file mode 100644 index 00000000..5c115f1c Binary files /dev/null and b/Web/static/img/video.png differ diff --git a/Web/static/js/al_photos.js b/Web/static/js/al_photos.js new file mode 100644 index 00000000..b4632d60 --- /dev/null +++ b/Web/static/js/al_photos.js @@ -0,0 +1,198 @@ +$(document).on("change", "#uploadButton", (e) => { + let iterator = 0 + + if(e.currentTarget.files.length > 10) { + MessageBox(tr("error"), tr("too_many_pictures"), [tr("ok")], [() => {Function.noop}]) + return; + } + + for(const file of e.currentTarget.files) { + if(!file.type.startsWith('image/')) { + MessageBox(tr("error"), tr("only_images_accepted", escapeHtml(file.name)), [tr("ok")], [() => {Function.noop}]) + return; + } + + if(file.size > 5 * 1024 * 1024) { + MessageBox(tr("error"), tr("max_filesize", 5), [tr("ok")], [() => {Function.noop}]) + return; + } + } + + if(document.querySelector(".whiteBox").style.display == "block") { + document.querySelector(".whiteBox").style.display = "none" + document.querySelector(".insertThere").append(document.getElementById("fakeButton")); + } + + let photos = new FormData() + for(file of e.currentTarget.files) { + photos.append("photo_"+iterator, file) + iterator += 1 + } + + photos.append("count", e.currentTarget.files.length) + photos.append("hash", u("meta[name=csrf]").attr("value")) + + let xhr = new XMLHttpRequest() + xhr.open("POST", "/photos/upload?album="+document.getElementById("album").value) + + xhr.onloadstart = () => { + document.querySelector(".insertPhotos").insertAdjacentHTML("beforeend", ``) + } + + xhr.onload = () => { + let result = JSON.parse(xhr.responseText) + + if(result.success) { + u("#loader").remove() + let photosArr = result.photos + + for(photo of photosArr) { + let table = document.querySelector(".insertPhotos") + + table.insertAdjacentHTML("beforeend", ` +
+
+ ${tr("description")}: + +
+
+ + ${tr("delete")} + +
+
+ `) + } + + document.getElementById("endUploading").style.display = "block" + } else { + u("#loader").remove() + MessageBox(tr("error"), escapeHtml(result.flash.message) ?? tr("error_uploading_photo"), [tr("ok")], [() => {Function.noop}]) + } + } + + xhr.send(photos) +}) + +$(document).on("click", "#endUploading", (e) => { + let table = document.querySelector("#photos") + let data = new FormData() + let arr = new Map(); + for(el of table.querySelectorAll("div#photo")) { + arr.set(el.dataset.id, el.querySelector("textarea").value) + } + + data.append("photos", JSON.stringify(Object.fromEntries(arr))) + data.append("hash", u("meta[name=csrf]").attr("value")) + + let xhr = new XMLHttpRequest() + // в самом вк на каждое изменение описания отправляется свой запрос, но тут мы экономим запросы + xhr.open("POST", "/photos/upload?act=finish&album="+document.getElementById("album").value) + + xhr.onloadstart = () => { + e.currentTarget.setAttribute("disabled", "disabled") + } + + xhr.onerror = () => { + MessageBox(tr("error"), tr("error_uploading_photo"), [tr("ok")], [() => {Function.noop}]) + } + + xhr.onload = () => { + let result = JSON.parse(xhr.responseText) + + if(!result.success) { + MessageBox(tr("error"), escapeHtml(result.flash.message), [tr("ok")], [() => {Function.noop}]) + } else { + document.querySelector(".page_content .insertPhotos").innerHTML = "" + document.getElementById("endUploading").style.display = "none" + + NewNotification(tr("photos_successfully_uploaded"), tr("click_to_go_to_album"), null, () => {window.location.assign(`/album${result.owner}_${result.album}`)}) + + document.querySelector(".whiteBox").style.display = "block" + document.querySelector(".insertAgain").append(document.getElementById("fakeButton")) + } + + e.currentTarget.removeAttribute("disabled") + } + + xhr.send(data) +}) + +$(document).on("click", "#deletePhoto", (e) => { + let data = new FormData() + data.append("hash", u("meta[name=csrf]").attr("value")) + + let xhr = new XMLHttpRequest() + xhr.open("POST", `/photo${e.currentTarget.dataset.owner}_${e.currentTarget.dataset.id}/delete`) + + xhr.onloadstart = () => { + e.currentTarget.closest("div#photo").classList.add("lagged") + } + + xhr.onerror = () => { + MessageBox(tr("error"), tr("unknown_error"), [tr("ok")], [() => {Function.noop}]) + } + + xhr.onload = () => { + u(e.currentTarget.closest("div#photo")).remove() + + if(document.querySelectorAll("div#photo").length < 1) { + document.getElementById("endUploading").style.display = "none" + document.querySelector(".whiteBox").style.display = "block" + document.querySelector(".insertAgain").append(document.getElementById("fakeButton")) + } + } + + xhr.send(data) +}) + +$(document).on("dragover drop", (e) => { + e.preventDefault() + + return false; +}) + +$(".container_gray").on("dragover", (e) => { + e.preventDefault() + document.querySelector("#fakeButton").classList.add("dragged") + document.querySelector("#fakeButton").value = tr("drag_files_here") +}) + +$(".container_gray").on("dragleave", (e) => { + e.preventDefault() + document.querySelector("#fakeButton").classList.remove("dragged") + document.querySelector("#fakeButton").value = tr("upload_picts") +}) + +$(".container_gray").on("drop", (e) => { + e.originalEvent.dataTransfer.dropEffect = 'move'; + e.preventDefault() + + $(".container_gray").trigger("dragleave") + + let files = e.originalEvent.dataTransfer.files + + for(const file of files) { + if(!file.type.startsWith('image/')) { + MessageBox(tr("error"), tr("only_images_accepted", escapeHtml(file.name)), [tr("ok")], [() => {Function.noop}]) + return; + } + + if(file.size > 5 * 1024 * 1024) { + MessageBox(tr("error"), tr("max_filesize", 5), [tr("ok")], [() => {Function.noop}]) + return; + } + } + + document.getElementById("uploadButton").files = files + u("#uploadButton").trigger("change") +}) + +u(".container_gray").on("paste", (e) => { + if(e.clipboardData.files.length > 0 && e.clipboardData.files.length < 10) { + document.getElementById("uploadButton").files = e.clipboardData.files; + u("#uploadButton").trigger("change") + } +}) diff --git a/Web/static/js/al_wall.js b/Web/static/js/al_wall.js index bb349c14..39ee2199 100644 --- a/Web/static/js/al_wall.js +++ b/Web/static/js/al_wall.js @@ -22,37 +22,14 @@ function trim(string) { 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() { canvas.getImage({includeWatermark: false}).toBlob(blob => { let fName = "Graffiti-" + Math.ceil(performance.now()).toString() + ".jpeg"; let image = new File([blob], fName, {type: "image/jpeg", lastModified: new Date().getTime()}); - let trans = new DataTransfer(); - trans.items.add(image); - let fileSelect = document.querySelector("#post-buttons" + id + " input[name='_pic_attachment']"); - fileSelect.files = trans.files; - - u(fileSelect).trigger("change"); - u("#post-buttons" + id + " #write textarea").trigger("focusin"); + fastUploadImage(id, image) }, "image/jpeg", 0.92); canvas.teardown(); @@ -75,6 +52,79 @@ function initGraffiti(id) { }); } +function fastUploadImage(textareaId, file) { + // uploading images + + if(!file.type.startsWith('image/')) { + MessageBox(tr("error"), tr("only_images_accepted", escapeHtml(file.name)), [tr("ok")], [() => {Function.noop}]) + return; + } + + // 🤓🤓🤓 + if(file.size > 5 * 1024 * 1024) { + MessageBox(tr("error"), tr("max_filesize", 5), [tr("ok")], [() => {Function.noop}]) + return; + } + + let imagesCount = document.querySelector("#post-buttons" + textareaId + " input[name='photos']").value.split(",").length + + if(imagesCount > 10) { + MessageBox(tr("error"), tr("too_many_photos"), [tr("ok")], [() => {Function.noop}]) + return + } + + let xhr = new XMLHttpRequest + let data = new FormData + + data.append("photo_0", file) + data.append("count", 1) + data.append("hash", u("meta[name=csrf]").attr("value")) + + xhr.open("POST", "/photos/upload") + + xhr.onloadstart = () => { + document.querySelector("#post-buttons"+textareaId+" .upload").insertAdjacentHTML("beforeend", ``) + } + + xhr.onload = () => { + let response = JSON.parse(xhr.responseText) + + appendImage(response, textareaId) + } + + xhr.send(data) +} + +// append image after uploading via /photos/upload +function appendImage(response, textareaId) { + if(!response.success) { + MessageBox(tr("error"), (tr("error_uploading_photo") + response.flash.message), [tr("ok")], [() => {Function.noop}]) + } else { + let form = document.querySelector("#post-buttons"+textareaId) + let photosInput = form.querySelector("input[name='photos']") + let photosIndicator = form.querySelector(".upload") + + for(const phot of response.photos) { + let id = phot.owner + "_" + phot.vid + + photosInput.value += (id + ",") + + u(photosIndicator).append(u(` +
+ × + +
+ `)) + + u(photosIndicator.querySelector(`.upload #aP[data-id='${id}'] .upload-delete`)).on("click", () => { + photosInput.value = photosInput.value.replace(id + ",", "") + u(form.querySelector(`.upload #aP[data-id='${id}']`)).remove() + }) + } + } + u(`#post-buttons${textareaId} .upload #loader`).remove() +} + u(".post-like-button").on("click", function(e) { e.preventDefault(); @@ -97,11 +147,12 @@ u(".post-like-button").on("click", function(e) { function setupWallPostInputHandlers(id) { u("#wall-post-input" + id).on("paste", function(e) { + // Если вы находитесь на странице с постом с id 11, то копирование произойдёт джва раза. + // Оч ржачный баг, но вот как его исправить, я, если честно, не знаю. + if(e.clipboardData.files.length === 1) { - var input = u("#post-buttons" + id + " input[name=_pic_attachment]").nodes[0]; - input.files = e.clipboardData.files; - - u(input).trigger("change"); + fastUploadImage(id, e.clipboardData.files[0]) + return; } }); @@ -116,6 +167,183 @@ function setupWallPostInputHandlers(id) { // 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() + + // todo add animation + return; + }); + + $("#wall-post-input" + id).on("drop", function(e) { + e.originalEvent.dataTransfer.dropEffect = 'move'; + fastUploadImage(id, e.originalEvent.dataTransfer.files[0]) + return; + }); +} + +function OpenMiniature(e, photo, post, photo_id, type = "post") { + /* + костыли но смешные однако + */ + e.preventDefault(); + + if(u(".ovk-photo-view").length > 0) u(".ovk-photo-view-dimmer").remove(); + + // Значения для переключения фоток + + let json; + + let imagesCount = 0; + let imagesIndex = 0; + + let tempDetailsSection = []; + + let dialog = u( + `
+
+
+ + + + +
+
+
+
+ +
+
+ +
+
+
`); + u("body").addClass("dimmed").append(dialog); + document.querySelector("html").style.overflowY = "hidden" + + let button = u("#ovk-photo-close"); + + button.on("click", function(e) { + let __closeDialog = () => { + u("body").removeClass("dimmed"); + u(".ovk-photo-view-dimmer").remove(); + document.querySelector("html").style.overflowY = "scroll" + }; + + __closeDialog(); + }); + + function __reloadTitleBar() { + u("#photo_com_title_photos").last().innerHTML = imagesCount > 1 ? tr("photo_x_from_y", imagesIndex, imagesCount) : tr("photo"); + } + + function __loadDetails(photo_id, index) { + if(tempDetailsSection[index] == null) { + u(".ovk-photo-details").last().innerHTML = ''; + ky("/photo" + photo_id, { + hooks: { + afterResponse: [ + async (_request, _options, response) => { + let parser = new DOMParser(); + let body = parser.parseFromString(await response.text(), "text/html"); + + let element = u(body.getElementsByClassName("ovk-photo-details")).last(); + + tempDetailsSection[index] = element.innerHTML; + + if(index == imagesIndex) { + u(".ovk-photo-details").last().innerHTML = element.innerHTML; + } + + document.querySelectorAll(".ovk-photo-details .bsdn").forEach(bsdnInitElement) + document.querySelectorAll(".ovk-photo-details script").forEach(scr => { + // stolen from #953 + let newScr = document.createElement('script') + + if(scr.src) { + newScr.src = scr.src + } else { + newScr.textContent = scr.textContent + } + + document.querySelector(".ovk-photo-details").appendChild(newScr); + }) + } + ] + } + }); + } else { + u(".ovk-photo-details").last().innerHTML = tempDetailsSection[index]; + } + } + + function __slidePhoto(direction) { + /* direction = 1 - right + direction = 0 - left */ + if(json == undefined) { + console.log("Да подожди ты. Куда торопишься?"); + } else { + if(imagesIndex >= imagesCount && direction == 1) { + imagesIndex = 1; + } else if(imagesIndex <= 1 && direction == 0) { + imagesIndex = imagesCount; + } else if(direction == 1) { + imagesIndex++; + } else if(direction == 0) { + imagesIndex--; + } + + let photoURL = json.body[imagesIndex - 1].url; + + u("#ovk-photo-img").last().src = photoURL; + __reloadTitleBar(); + __loadDetails(json.body[imagesIndex - 1].id, imagesIndex); + } + } + + let slideLeft = u(".ovk-photo-slide-left"); + + slideLeft.on("click", (e) => { + __slidePhoto(0); + }); + + let slideRight = u(".ovk-photo-slide-right"); + + slideRight.on("click", (e) => { + __slidePhoto(1); + }); + + let data = new FormData() + data.append('parentType', type); + ky.post("/iapi/getPhotosFromPost/" + (type == "post" ? post : "1_"+post), { + hooks: { + afterResponse: [ + async (_request, _options, response) => { + json = await response.json(); + + imagesCount = json.body.length; + imagesIndex = 0; + // Это всё придётся правда на 1 прибавлять + + json.body.every(element => { + imagesIndex++; + if(element.id == photo_id) { + return false; + } else { + return true; + } + }); + + __reloadTitleBar(); + __loadDetails(json.body[imagesIndex - 1].id, imagesIndex); } + ] + }, + body: data + }); + + return u(".ovk-photo-view-dimmer"); } u("#write > form").on("keydown", function(event) { @@ -210,6 +438,7 @@ function addNote(textareaId, nid) u("body").removeClass("dimmed"); u(".ovk-diag-cont").remove(); + document.querySelector("html").style.overflowY = "scroll" } async function attachNote(id) @@ -262,4 +491,473 @@ async function showArticle(note_id) { u("#articleText").html(`

${note.title}

` + note.html); u("body").removeClass("dimmed"); u("body").addClass("article"); -} \ No newline at end of file +} + +$(document).on("click", "#videoAttachment", async (e) => { + e.preventDefault() + + let body = ` + + +
+ ` + + let form = e.currentTarget.closest("form") + + MessageBox(tr("selecting_video"), body, [tr("close")], [Function.noop]); + + // styles for messageboxx + document.querySelector(".ovk-diag-body").style.padding = "0" + document.querySelector(".ovk-diag-cont").style.width = "580px" + document.querySelector(".ovk-diag-body").style.height = "335px" + + async function insertVideos(page, query = "") { + document.querySelector(".videosInsert").insertAdjacentHTML("beforeend", ``) + + let vidoses + let noVideosText = tr("no_videos") + if(query == "") { + vidoses = await API.Wall.getVideos(page) + } else { + vidoses = await API.Wall.searchVideos(page, query) + noVideosText = tr("no_videos_results") + } + + if(vidoses.count < 1) { + document.querySelector(".videosInsert").innerHTML = `${noVideosText}` + } + + let pagesCount = Math.ceil(Number(vidoses.count) / 8) + u("#loader").remove() + let insert = document.querySelector(".videosInsert") + + for(const vid of vidoses.items) { + let isAttached = (form.querySelector("input[name='videos']").value.includes(`${vid.video.owner_id}_${vid.video.id},`)) + + insert.insertAdjacentHTML("beforeend", ` +
+ + + + + + + + +
+ +
+ ${escapeHtml(vid.video.title)} +
+
+
+ + + ${ovk_proc_strtr(escapeHtml(vid.video.title), 30)} + + +
+

+ ${ovk_proc_strtr(escapeHtml(vid.video.description ?? ""), 140)} +

+ ${escapeHtml(vid.video.author_name ?? "")} +
+
+ `) + } + + if(page < pagesCount) { + document.querySelector(".videosInsert").insertAdjacentHTML("beforeend", ` +
+ more... +
`) + } + } + + $(".videosInsert").on("click", "#showMoreVideos", (e) => { + u(e.currentTarget).remove() + insertVideos(Number(e.currentTarget.dataset.page), document.querySelector(".topGrayBlock #vquery").value) + }) + + $(".topGrayBlock #vquery").on("change", async (e) => { + await new Promise(r => setTimeout(r, 1000)); + + if(e.currentTarget.value === document.querySelector(".topGrayBlock #vquery").value) { + document.querySelector(".videosInsert").innerHTML = "" + insertVideos(1, e.currentTarget.value) + return; + } else { + console.info("skipping") + } + }) + + insertVideos(1) + + function insertAttachment(id) { + let videos = form.querySelector("input[name='videos']") + + if(!videos.value.includes(id + ",")) { + if(videos.value.split(",").length > 10) { + NewNotification(tr("error"), tr("max_attached_videos")) + return false + } + + form.querySelector("input[name='videos']").value += (id + ",") + + console.info(id + " attached") + return true + } else { + form.querySelector("input[name='videos']").value = form.querySelector("input[name='videos']").value.replace(id + ",", "") + + console.info(id + " detached") + return false + } + } + + $(".videosInsert").on("click", "#attachvid", (ev) => { + // откреплено от псто + if(!insertAttachment(ev.currentTarget.dataset.attachmentdata)) { + u(`.post-has-videos .post-has-video[data-id='${ev.currentTarget.dataset.attachmentdata}']`).remove() + ev.currentTarget.innerHTML = tr("attach") + } else { + ev.currentTarget.innerHTML = tr("detach") + + form.querySelector(".post-has-videos").insertAdjacentHTML("beforeend", ` +
+ ${tr("video")} "${ovk_proc_strtr(escapeHtml(ev.currentTarget.dataset.name), 20)}" +
+ `) + + u(`#unattachVideo[data-id='${ev.currentTarget.dataset.attachmentdata}']`).on("click", (e) => { + let id = ev.currentTarget.dataset.attachmentdata + form.querySelector("input[name='videos']").value = form.querySelector("input[name='videos']").value.replace(id + ",", "") + + console.info(id + " detached") + + u(e.currentTarget).remove() + }) + } + }) +}) + +$(document).on("click", "#editPost", (e) => { + let post = e.currentTarget.closest("table") + let content = post.querySelector(".text") + let text = content.querySelector(".really_text") + + if(content.querySelector("textarea") == null) { + content.insertAdjacentHTML("afterbegin", ` +
+
+ + + +
+ ${e.currentTarget.dataset.nsfw != null ? ` +
+ +
+ ` : ``} + ${e.currentTarget.dataset.fromgroup != null ? ` +
+ +
+ ` : ``} +
+ `) + + u(content.querySelector("#cancelEditing")).on("click", () => {post.querySelector("#editPost").click()}) + u(content.querySelector("#endEditing")).on("click", () => { + let nwcntnt = content.querySelector("#new_content").value + let type = "post" + + if(post.classList.contains("comment")) { + type = "comment" + } + + let xhr = new XMLHttpRequest() + xhr.open("POST", "/wall/edit") + + xhr.onloadstart = () => { + content.querySelector(".editMenu").classList.add("loading") + } + + xhr.onerror = () => { + MessageBox(tr("error"), "unknown error occured", [tr("ok")], [() => {Function.noop}]) + } + + xhr.ontimeout = () => { + MessageBox(tr("error"), "Try to refresh page", [tr("ok")], [() => {Function.noop}]) + } + + xhr.onload = () => { + let result = JSON.parse(xhr.responseText) + + if(result.error == "no") { + post.querySelector("#editPost").click() + content.querySelector(".really_text").innerHTML = result.new_content + + if(post.querySelector(".editedMark") == null) { + post.querySelector(".date").insertAdjacentHTML("beforeend", ` + (${tr("edited_short")}) + `) + } + + if(e.currentTarget.dataset.nsfw != null) { + e.currentTarget.setAttribute("data-nsfw", result.nsfw) + + if(result.nsfw == 0) { + post.classList.remove("post-nsfw") + } else { + post.classList.add("post-nsfw") + } + } + + if(e.currentTarget.dataset.fromgroup != null) { + e.currentTarget.setAttribute("data-fromgroup", result.from_group) + } + + post.querySelector(".post-avatar").setAttribute("src", result.author.avatar) + post.querySelector(".post-author-name").innerHTML = result.author.name + post.querySelector(".really_text").setAttribute("data-text", result.new_text) + } else { + MessageBox(tr("error"), result.error, [tr("ok")], [Function.noop]) + post.querySelector("#editPost").click() + } + } + + xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + xhr.send("postid="+e.currentTarget.dataset.id+ + "&newContent="+nwcntnt+ + "&hash="+encodeURIComponent(u("meta[name=csrf]").attr("value"))+ + "&type="+type+ + "&nsfw="+(content.querySelector("#nswfw") != null ? content.querySelector("#nswfw").checked : 0)+ + "&fromgroup="+(content.querySelector("#fromgroup") != null ? content.querySelector("#fromgroup").checked : 0)) + }) + + u(".editMenu").on("keydown", (e) => { + if(e.ctrlKey && e.keyCode === 13) + content.querySelector("#endEditing").click() + }); + + text.style.display = "none" + setupWallPostInputHandlers(999) + } else { + u(content.querySelector(".editMenu")).remove() + text.style.display = "block" + } +}) + +// copypaste from videos picker +$(document).on("click", "#photosAttachments", async (e) => { + let body = ` +
+
+ ${tr("upload_new_photo")}: + + + +
+
+ +
+

${tr("is_x_photos", 0)}

+
+
+ ` + + let form = e.currentTarget.closest("form") + + MessageBox(tr("select_photo"), body, [tr("close")], [Function.noop]); + + document.querySelector(".ovk-diag-body").style.padding = "0" + document.querySelector(".ovk-diag-cont").style.width = "630px" + document.querySelector(".ovk-diag-body").style.height = "335px" + + async function insertPhotos(page, album = 0) { + u("#loader").remove() + + let insertPlace = document.querySelector(".photosInsert .photosList") + document.querySelector(".photosInsert").insertAdjacentHTML("beforeend", ``) + + let photos; + + try { + photos = await API.Photos.getPhotos(page, Number(album)) + } catch(e) { + document.querySelector(".photosInsert h4").innerHTML = tr("is_x_photos", -1) + insertPlace.innerHTML = "Invalid album" + console.error(e) + u("#loader").remove() + return; + } + + document.querySelector(".photosInsert h4").innerHTML = tr("is_x_photos", photos.count) + + let pagesCount = Math.ceil(Number(photos.count) / 24) + u("#loader").remove() + + for(const photo of photos.items) { + let isAttached = (form.querySelector("input[name='photos']").value.includes(`${photo.owner_id}_${photo.id},`)) + + insertPlace.insertAdjacentHTML("beforeend", ` +
+ + ... + +
+ `) + } + + if(page < pagesCount) { + insertPlace.insertAdjacentHTML("beforeend", ` +
+ more... +
`) + } + } + + insertPhotos(1) + + let albums = await API.Photos.getAlbums(Number(e.currentTarget.dataset.club ?? 0)) + + for(const alb of albums.items) { + let sel = document.querySelector(".ovk-diag-body #albumSelect") + + sel.insertAdjacentHTML("beforeend", ``) + } + + $(".photosInsert").on("click", "#showMorePhotos", (e) => { + u(e.currentTarget).remove() + insertPhotos(Number(e.currentTarget.dataset.page), document.querySelector(".topGrayBlock #albumSelect").value) + }) + + $(".topGrayBlock #albumSelect").on("change", (evv) => { + document.querySelector(".photosInsert .photosList").innerHTML = "" + + insertPhotos(1, evv.currentTarget.value) + }) + + function insertAttachment(id) { + let photos = form.querySelector("input[name='photos']") + + if(!photos.value.includes(id + ",")) { + if(photos.value.split(",").length > 10) { + NewNotification(tr("error"), tr("max_attached_photos")) + return false + } + + form.querySelector("input[name='photos']").value += (id + ",") + + console.info(id + " attached") + return true + } else { + form.querySelector("input[name='photos']").value = form.querySelector("input[name='photos']").value.replace(id + ",", "") + + console.info(id + " detached") + return false + } + } + + $(".photosList").on("click", ".album-photo", (ev) => { + ev.preventDefault() + + if(!insertAttachment(ev.currentTarget.dataset.attachmentdata)) { + u(form.querySelector(`.upload #aP[data-id='${ev.currentTarget.dataset.attachmentdata}']`)).remove() + ev.currentTarget.querySelector("img").style.backgroundColor = "white" + } else { + ev.currentTarget.querySelector("img").style.backgroundColor = "#646464" + let id = ev.currentTarget.dataset.attachmentdata + + u(form.querySelector(`.upload`)).append(u(` +
+ × + +
+ `)); + + u(`.upload #aP[data-id='${ev.currentTarget.dataset.attachmentdata}'] .upload-delete`).on("click", () => { + form.querySelector("input[name='photos']").value = form.querySelector("input[name='photos']").value.replace(id + ",", "") + u(form.querySelector(`.upload #aP[data-id='${ev.currentTarget.dataset.attachmentdata}']`)).remove() + }) + } + }) + + u("#fastFotosUplod").on("change", (evn) => { + let xhr = new XMLHttpRequest() + xhr.open("POST", "/photos/upload") + + let formdata = new FormData() + let iterator = 0 + + for(const fille of evn.currentTarget.files) { + if(!fille.type.startsWith('image/')) { + continue; + } + + if(fille.size > 5 * 1024 * 1024) { + continue; + } + + if(evn.currentTarget.files.length >= 10) { + NewNotification(tr("error"), tr("max_attached_photos")) + return; + } + + formdata.append("photo_"+iterator, fille) + iterator += 1 + } + + xhr.onloadstart = () => { + evn.currentTarget.parentNode.insertAdjacentHTML("beforeend", ``) + } + + xhr.onload = () => { + let result = JSON.parse(xhr.responseText) + + u("#loader").remove() + if(result.success) { + for(const pht of result.photos) { + let id = pht.owner + "_" + pht.vid + + if(!insertAttachment(id)) { + return + } + + u(form.querySelector(`.upload`)).append(u(` +
+ × + +
+ `)); + + u(`.upload #aP[data-id='${pht.owner + "_" + pht.vid}'] .upload-delete`).on("click", () => { + form.querySelector("input[name='photos']").value = form.querySelector("input[name='photos']").value.replace(id + ",", "") + u(form.querySelector(`.upload #aP[data-id='${id}']`)).remove() + }) + } + + u("body").removeClass("dimmed"); + u(".ovk-diag-cont").remove(); + document.querySelector("html").style.overflowY = "scroll" + } else { + // todo: https://vk.com/wall-32295218_78593 + alert(result.flash.message) + } + } + + formdata.append("hash", u("meta[name=csrf]").attr("value")) + formdata.append("count", iterator) + + xhr.send(formdata) + }) +}) diff --git a/Web/static/js/messagebox.js b/Web/static/js/messagebox.js index 45791fd3..e56c720f 100644 --- a/Web/static/js/messagebox.js +++ b/Web/static/js/messagebox.js @@ -3,6 +3,7 @@ Function.noop = () => {}; function MessageBox(title, body, buttons, callbacks) { if(u(".ovk-diag-cont").length > 0) return false; + document.querySelector("html").style.overflowY = "hidden" let dialog = u( `
@@ -19,7 +20,11 @@ function MessageBox(title, body, buttons, callbacks) { button.on("click", function(e) { let __closeDialog = () => { - u("body").removeClass("dimmed"); + if(document.querySelector(".ovk-photo-view-dimmer") == null) { + u("body").removeClass("dimmed"); + document.querySelector("html").style.overflowY = "scroll" + } + u(".ovk-diag-cont").remove(); }; diff --git a/Web/static/js/openvk.cls.js b/Web/static/js/openvk.cls.js index 22144cfc..b131bfa0 100644 --- a/Web/static/js/openvk.cls.js +++ b/Web/static/js/openvk.cls.js @@ -68,7 +68,7 @@ function toggleMenu(id) { } document.addEventListener("DOMContentLoaded", function() { //BEGIN - u("#_photoDelete").on("click", function(e) { + $(document).on("click", "#_photoDelete", function(e) { var formHtml = ""; formHtml += ""; formHtml += ""; diff --git a/install/automated/common/10-openvk.conf b/install/automated/common/10-openvk.conf index d842eefd..b272f5ac 100644 --- a/install/automated/common/10-openvk.conf +++ b/install/automated/common/10-openvk.conf @@ -8,6 +8,5 @@ Require all granted - ErrorLog /var/log/openvk/error.log - CustomLog /var/log/openvk/access.log combinedio + LogFormat combinedio diff --git a/install/automated/docker/acl_handler.sh b/install/automated/docker/acl_handler.sh index 8ef23239..e23176a3 100755 --- a/install/automated/docker/acl_handler.sh +++ b/install/automated/docker/acl_handler.sh @@ -8,7 +8,6 @@ do chown -R 33:33 /opt/chandler/extensions/available/openvk/tmp/api-storage/audios chown -R 33:33 /opt/chandler/extensions/available/openvk/tmp/api-storage/photos chown -R 33:33 /opt/chandler/extensions/available/openvk/tmp/api-storage/videos - chown -R 33:33 /var/log/openvk sleep 600 done \ No newline at end of file diff --git a/install/automated/docker/base-php-apache.Dockerfile b/install/automated/docker/base-php-apache.Dockerfile index de24d34f..3ead71f0 100644 --- a/install/automated/docker/base-php-apache.Dockerfile +++ b/install/automated/docker/base-php-apache.Dockerfile @@ -18,5 +18,6 @@ RUN apt update; \ yaml \ pdo_mysql \ rdkafka \ + imagick \ && \ rm -rf /var/lib/apt/lists/* \ No newline at end of file diff --git a/install/automated/docker/docker-compose.yml b/install/automated/docker/docker-compose.yml index cbd697a4..685ab713 100644 --- a/install/automated/docker/docker-compose.yml +++ b/install/automated/docker/docker-compose.yml @@ -12,7 +12,6 @@ services: - openvk-audios:/opt/chandler/extensions/available/openvk/tmp/api-storage/audios - openvk-photos:/opt/chandler/extensions/available/openvk/tmp/api-storage/photos - openvk-videos:/opt/chandler/extensions/available/openvk/tmp/api-storage/videos - - openvk-logs:/var/log/openvk - ./openvk.yml:/opt/chandler/extensions/available/openvk/openvk.yml:ro - ./chandler.yml:/opt/chandler/chandler.yml:ro depends_on: @@ -32,7 +31,6 @@ services: - openvk-audios:/opt/chandler/extensions/available/openvk/tmp/api-storage/audios - openvk-photos:/opt/chandler/extensions/available/openvk/tmp/api-storage/photos - openvk-videos:/opt/chandler/extensions/available/openvk/tmp/api-storage/videos - - openvk-logs:/var/log/openvk - ./acl_handler.sh:/bin/acl_handler.sh:ro mariadb-primary: @@ -76,6 +74,7 @@ services: - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092 - KAFKA_BROKER_ID=1 - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@127.0.0.1:9093 + - KAFKA_CFG_NODE_ID=1 phpmyadmin: image: docker.io/phpmyadmin:5 diff --git a/install/automated/docker/openvk.Dockerfile b/install/automated/docker/openvk.Dockerfile index 47f63b77..389f7443 100644 --- a/install/automated/docker/openvk.Dockerfile +++ b/install/automated/docker/openvk.Dockerfile @@ -48,7 +48,6 @@ RUN ln -s /opt/chandler/extensions/available/commitcaptcha/ /opt/chandler/extens ln -s /opt/chandler/extensions/available/openvk/install/automated/common/10-openvk.conf /etc/apache2/sites-enabled/10-openvk.conf && \ a2enmod rewrite -VOLUME [ "/var/log/openvk" ] VOLUME [ "/opt/chandler/extensions/available/openvk/storage" ] VOLUME [ "/opt/chandler/extensions/available/openvk/tmp/api-storage/audios" ] VOLUME [ "/opt/chandler/extensions/available/openvk/tmp/api-storage/photos" ] diff --git a/install/automated/kubernetes/manifests/003-deployment.yaml b/install/automated/kubernetes/manifests/003-deployment.yaml index e15d3ccd..edbc40f9 100644 --- a/install/automated/kubernetes/manifests/003-deployment.yaml +++ b/install/automated/kubernetes/manifests/003-deployment.yaml @@ -38,13 +38,10 @@ items: chown -R 33:33 /opt/chandler/extensions/available/openvk/tmp/api-storage/audios chown -R 33:33 /opt/chandler/extensions/available/openvk/tmp/api-storage/photos chown -R 33:33 /opt/chandler/extensions/available/openvk/tmp/api-storage/videos - chown -R 33:33 /var/log/openvk sleep 600 done volumeMounts: - - mountPath: /var/log/openvk - name: openvk-logs - mountPath: /opt/chandler/extensions/available/openvk/storage name: openvk-storage - mountPath: /opt/chandler/extensions/available/openvk/tmp/api-storage/audios @@ -66,8 +63,6 @@ items: cpu: 100m memory: 512Mi volumeMounts: - - mountPath: /var/log/openvk - name: openvk-logs - mountPath: /opt/chandler/extensions/available/openvk/openvk.yml name: openvk-config subPath: openvk.yml diff --git a/install/sqls/00002-support-aliases.sql b/install/sqls/00002-support-aliases.sql index b586a1dd..e3d17ca0 100644 --- a/install/sqls/00002-support-aliases.sql +++ b/install/sqls/00002-support-aliases.sql @@ -1 +1,7 @@ -CREATE TABLE `support_names` ( `agent` BIGINT UNSIGNED NOT NULL , `name` VARCHAR(512) NOT NULL , `icon` VARCHAR(1024) NULL DEFAULT NULL , `numerate` BOOLEAN NOT NULL DEFAULT FALSE , PRIMARY KEY (`agent`)) ENGINE = InnoDB; \ No newline at end of file +CREATE TABLE `support_names` ( + `agent` BIGINT UNSIGNED NOT NULL, + `name` VARCHAR(512) NOT NULL, + `icon` VARCHAR(1024) NULL DEFAULT NULL, + `numerate` BOOLEAN NOT NULL DEFAULT FALSE, + PRIMARY KEY (`agent`) +) ENGINE = InnoDB; \ No newline at end of file diff --git a/install/sqls/00018-reports.sql b/install/sqls/00019-reports.sql similarity index 100% rename from install/sqls/00018-reports.sql rename to install/sqls/00019-reports.sql diff --git a/install/sqls/00019-block-in-support.sql b/install/sqls/00020-block-in-support.sql similarity index 100% rename from install/sqls/00019-block-in-support.sql rename to install/sqls/00020-block-in-support.sql diff --git a/install/sqls/00020-image-sizes.sql b/install/sqls/00021-image-sizes.sql similarity index 100% rename from install/sqls/00020-image-sizes.sql rename to install/sqls/00021-image-sizes.sql diff --git a/install/sqls/00021-video-processing.sql b/install/sqls/00022-video-processing.sql similarity index 100% rename from install/sqls/00021-video-processing.sql rename to install/sqls/00022-video-processing.sql diff --git a/install/sqls/00022-group-alerts.sql b/install/sqls/00023-group-alerts.sql similarity index 100% rename from install/sqls/00022-group-alerts.sql rename to install/sqls/00023-group-alerts.sql diff --git a/install/sqls/00023-email-change.sql b/install/sqls/00024-email-change.sql similarity index 100% rename from install/sqls/00023-email-change.sql rename to install/sqls/00024-email-change.sql diff --git a/install/sqls/00024-main-page-setting.sql b/install/sqls/00025-main-page-setting.sql similarity index 100% rename from install/sqls/00024-main-page-setting.sql rename to install/sqls/00025-main-page-setting.sql diff --git a/install/sqls/00025-toncoin-fetching.sql b/install/sqls/00026-toncoin-fetching.sql similarity index 100% rename from install/sqls/00025-toncoin-fetching.sql rename to install/sqls/00026-toncoin-fetching.sql diff --git a/install/sqls/00026-better-birthdays.sql b/install/sqls/00027-better-birthdays.sql similarity index 100% rename from install/sqls/00026-better-birthdays.sql rename to install/sqls/00027-better-birthdays.sql diff --git a/install/sqls/00027-rating.sql b/install/sqls/00028-rating.sql similarity index 100% rename from install/sqls/00027-rating.sql rename to install/sqls/00028-rating.sql diff --git a/install/sqls/00028-deactivation.sql b/install/sqls/00029-deactivation.sql similarity index 100% rename from install/sqls/00028-deactivation.sql rename to install/sqls/00029-deactivation.sql diff --git a/install/sqls/00029-hashtag-search.sql b/install/sqls/00030-hashtag-search.sql similarity index 100% rename from install/sqls/00029-hashtag-search.sql rename to install/sqls/00030-hashtag-search.sql diff --git a/install/sqls/00030-apps.sql b/install/sqls/00031-apps.sql similarity index 100% rename from install/sqls/00030-apps.sql rename to install/sqls/00031-apps.sql diff --git a/install/sqls/00032-agent-card.sql b/install/sqls/00032-agent-card.sql deleted file mode 100644 index a8354c80..00000000 --- a/install/sqls/00032-agent-card.sql +++ /dev/null @@ -1,15 +0,0 @@ - -CREATE TABLE `support_names` ( - `id` bigint(20) UNSIGNED NOT NULL, - `agent` bigint(20) UNSIGNED NOT NULL, - `name` varchar(512) COLLATE utf8mb4_unicode_ci NOT NULL, - `icon` varchar(1024) COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `numerate` tinyint(1) NOT NULL DEFAULT 0 -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - -ALTER TABLE `support_names` - ADD PRIMARY KEY (`id`); - -ALTER TABLE `support_names` - MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT; -COMMIT; \ No newline at end of file diff --git a/install/sqls/00031-ban-page-until.sql b/install/sqls/00032-ban-page-until.sql similarity index 100% rename from install/sqls/00031-ban-page-until.sql rename to install/sqls/00032-ban-page-until.sql diff --git a/install/sqls/00033-agent-card.sql b/install/sqls/00033-agent-card.sql new file mode 100644 index 00000000..0f82460e --- /dev/null +++ b/install/sqls/00033-agent-card.sql @@ -0,0 +1,9 @@ + +ALTER TABLE `support_names` ADD `id` bigint(20) UNSIGNED NOT NULL FIRST; + +ALTER TABLE `support_names` + ADD UNIQUE KEY `id` (`id`); + +ALTER TABLE `support_names` + MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT; +COMMIT; \ No newline at end of file diff --git a/install/sqls/00032-banned-urls.sql b/install/sqls/00034-banned-urls.sql similarity index 100% rename from install/sqls/00032-banned-urls.sql rename to install/sqls/00034-banned-urls.sql diff --git a/install/sqls/00032-better-reports.sql b/install/sqls/00035-better-reports.sql similarity index 100% rename from install/sqls/00032-better-reports.sql rename to install/sqls/00035-better-reports.sql diff --git a/install/sqls/00033-shortcode-aliases.sql b/install/sqls/00036-shortcode-aliases.sql similarity index 100% rename from install/sqls/00033-shortcode-aliases.sql rename to install/sqls/00036-shortcode-aliases.sql diff --git a/install/sqls/00037-agent-card-profilefix.sql b/install/sqls/00037-agent-card-profilefix.sql deleted file mode 100644 index e7ebc230..00000000 --- a/install/sqls/00037-agent-card-profilefix.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE `support_names` - ADD COLUMN `id` bigint(20) NOT NULL AUTO_INCREMENT UNIQUE FIRST; diff --git a/install/sqls/00034-polls.sql b/install/sqls/00037-polls.sql similarity index 100% rename from install/sqls/00034-polls.sql rename to install/sqls/00037-polls.sql diff --git a/install/sqls/00035-backdrops.sql b/install/sqls/00038-backdrops.sql similarity index 100% rename from install/sqls/00035-backdrops.sql rename to install/sqls/00038-backdrops.sql diff --git a/install/sqls/00036-platforms.sql b/install/sqls/00039-platforms.sql similarity index 100% rename from install/sqls/00036-platforms.sql rename to install/sqls/00039-platforms.sql diff --git a/install/sqls/00038-noSpam-templates.sql b/install/sqls/00040-noSpam-templates.sql similarity index 100% rename from install/sqls/00038-noSpam-templates.sql rename to install/sqls/00040-noSpam-templates.sql diff --git a/locales/en.strings b/locales/en.strings index 487d2156..b1d211d1 100644 --- a/locales/en.strings +++ b/locales/en.strings @@ -149,6 +149,10 @@ "user_banned" = "Unfortunately, we had to block the $1 user page."; "user_banned_comment" = "Moderator's comment:"; +"verified_page" = "Verified page"; +"user_is_blocked" = "User is blocked"; +"before" = "before"; +"forever" = "forever"; /* Wall */ @@ -202,6 +206,7 @@ "nsfw_warning" = "This post may have NSFW-content"; "report" = "Report"; "attach" = "Attach"; +"detach" = "Detach"; "attach_photo" = "Attach photo"; "attach_video" = "Attach video"; "draw_graffiti" = "Draw graffiti"; @@ -213,6 +218,9 @@ "graffiti" = "Graffiti"; "reply" = "Reply"; +"post_is_ad" = "This post is sponsored."; + +"edited_short" = "edited"; /* Friends */ @@ -337,9 +345,15 @@ "create" = "Create"; "albums" = "Albums"; +"album" = "Album"; +"photos" = "photos"; +"photo" = "Photo"; "create_album" = "Create album"; "edit_album" = "Edit album"; +"edit_photo" = "Edit photo"; "creating_album" = "Creating album"; +"delete_photo" = "Delete photo"; +"sure_deleting_photo" = "Do you really want to delete this picture?"; "upload_photo" = "Upload photo"; "photo" = "Photo"; "upload_button" = "Upload"; @@ -381,6 +395,39 @@ "upd_f" = "updated her profile picture"; "upd_g" = "updated group's picture"; +"add_photos" = "Add photos"; +"upload_picts" = "Upload photos"; +"end_uploading" = "Finish uploading"; +"photos_successfully_uploaded" = "Photos successfully uploaded"; +"click_to_go_to_album" = "Click here to go to album."; +"error_uploading_photo" = "Error when uploading photo"; +"too_many_pictures" = "No more than 10 pictures"; + +"drag_files_here" = "Drag files here"; +"only_images_accepted" = "File \"$1\" is not an image"; +"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."; +"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"; + +"is_x_photos_zero" = "Just zero photos."; +"is_x_photos_one" = "Just one photo."; +"is_x_photos_few" = "Just $1 photos."; +"is_x_photos_many" = "Just $1 photos."; +"is_x_photos_other" = "Just $1 photos."; + +"all_photos" = "All photos"; +"error_uploading_photo" = "Error when uploading photo. Error text: "; +"too_many_photos" = "Too many photos."; + +"photo_x_from_y" = "Photo $1 from $2"; + /* Notes */ "notes" = "Notes"; @@ -415,6 +462,8 @@ "notes_closed" = "You can't attach note to post, because only you can see them.
You can change it in settings."; "do_not_attach_note" = "Do not attach note"; +"something" = "Something"; +"supports_xhtml" = "from (X)HTML supported."; /* Notes: Article Viewer */ "aw_legacy_ui" = "Legacy interface"; @@ -630,6 +679,9 @@ "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."; +"viewing_backup_codes" = "View backup codes"; +"disable_2fa" = "Disable 2FA"; +"viewing" = "View"; /* Sorting */ @@ -657,6 +709,16 @@ "view_video" = "View"; +"selecting_video" = "Selecting videos"; +"upload_new_video" = "Upload new video"; +"max_attached_videos" = "Max is 10 videos"; +"max_attached_photos" = "Max is 10 photos"; +"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."; + /* Notifications */ "feedback" = "Feedback"; @@ -695,6 +757,7 @@ "nt_mention_in_video" = "in discussion of this video"; "nt_mention_in_note" = "in discussion of this note"; "nt_mention_in_topic" = "in the discussion"; +"nt_sent_gift" = "sent you a gift"; /* Time */ @@ -909,6 +972,20 @@ "text_of_the_post" = "Text of the post"; "today" = "today"; +"will_be_watched" = "It will be reviewed by the moderators soon"; + +"report_question" = "Report?"; +"report_question_text" = "What exactly do you find unacceptable about this material?"; +"report_reason" = "Report reason"; +"reason" = "Reason"; +"going_to_report_app" = "You are about to report this application."; +"going_to_report_club" = "You are about to report this club."; +"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_post" = "You are about to report this post."; +"going_to_report_comment" = "You are about to report this comment."; + "comment" = "Comment"; "sender" = "Sender"; @@ -1066,6 +1143,7 @@ "error_data_too_big" = "Attribute '$1' must be at most $2 $3 long"; "forbidden" = "Access error"; +"unknown_error" = "Unknown error"; "forbidden_comment" = "This user's privacy settings do not allow you to look at his page."; "changes_saved" = "Changes saved"; @@ -1118,21 +1196,116 @@ "post_is_empty_or_too_big" = "The post is empty or too big."; "post_is_too_big" = "The post is too big."; +"error_sending_report" = "Failed to make a report..."; + +"error_when_saving_gift" = "Error when saving gift"; +"error_when_saving_gift_bad_image" = "Gift image is crooked."; +"error_when_saving_gift_no_image" = "Please, upload gift's image."; +"video_uploads_disabled" = "Video uploads are disabled by the system administrator."; + +"error_when_publishing_comment" = "Error when publishing comment"; +"error_when_publishing_comment_description" = "Image is corrupted, too big or one side is many times larger than the other."; +"error_comment_empty" = "Comment is empty or too big."; +"error_comment_too_big" = "Comment is too big."; +"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."; + +"error_access_denied_short" = "Access denied"; +"error_access_denied" = "You don't have rights to edit this resource"; +"success" = "Success"; +"comment_will_not_appear" = "This comment will no longer appear."; + +"error_when_gifting" = "Failed to gift"; +"error_user_not_exists" = "User or pack does not exist."; +"error_no_rights_gifts" = "Failed to check rights on gift."; +"error_no_more_gifts" = "You no longer have such gifts."; +"error_no_money" = "Shout out to a beggar."; + +"gift_sent" = "Gift sent"; +"gift_sent_desc" = "You sent a gift to $1 for $2 votes"; + +"error_on_server_side" = "An error occurred on the server side. Contact your system administrator."; +"error_no_group_name" = "You did not enter a group name."; + +"success_action" = "Action successful"; +"connection_error" = "Connection error"; +"connection_error_desc" = "Failed to connect to telemetry service."; + +"error_when_uploading_photo" = "Failed to save photo"; + +"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)"; +"x_no_more_admin" = "$1 no longer an administrator."; +"x_is_admin" = "$1 appointed as an administrator."; + +"x_is_now_hidden" = "Now $1 will be shown as a normal subscriber to everyone except other admins"; +"x_is_now_showed" = "Now everyone will know that $1 is an administrator."; + +"note_is_deleted" = "Note was deleted"; +"note_x_is_now_deleted" = "Note \"$1\" was successfully deleted."; +"new_data_accepted" = "New data accepted."; + +"album_is_deleted" = "Album was deleted"; +"album_x_is_deleted" = "Album $1 was successfully deleted."; + +"error_adding_to_deleted" = "Failed to save photo to DELETED."; +"error_adding_to_x" = "Failed to save photo to $1."; +"no_photo" = "No photo"; + +"select_file" = "Select file"; +"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."; + +"no_video" = "No video"; +"no_video_desc" = "Select a file or provide a link."; +"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."; + +"new_data_video" = "The updated description will appear on the video page."; +"error_deleting_video" = "Failed to delete video"; +"login_please" = "You are not signed in."; +"invalid_code" = "Failed to verify phone number: Invalid code."; + +"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)"; +"death" = "Death..."; +"nehay" = "Live long!"; +"user_successfully_banned" = "User was successfully banned."; + +"content_is_deleted" = "The content has been removed and the user has received a warning."; +"report_is_ignored" = "Report was ignored."; +"group_owner_is_banned" = "Group's owner was successfully banned"; +"group_is_banned" = "Group was successfully banned"; + +"description_too_long" = "Description is too long."; + /* Admin actions */ "login_as" = "Login as $1"; "manage_user_action" = "Manage user"; "manage_group_action" = "Manage group"; "ban_user_action" = "Ban user"; +"blocks" = "Blocks"; +"last_actions" = "Last actions"; "unban_user_action" = "Unban user"; "warn_user_action" = "Warn user"; "ban_in_support_user_action" = "Ban in support"; "unban_in_support_user_action" = "Unban in support"; +"changes_history" = "Editing history"; /* Admin panel */ "admin" = "Admin panel"; +"sandbox_for_developers" = "Sandbox for developers"; "admin_ownerid" = "Owner ID"; "admin_author" = "Author"; "admin_name" = "Name"; @@ -1218,6 +1391,11 @@ "admin_banned_link_not_specified" = "The link is not specified"; "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."; + +"logs" = "Logs"; +"logs_anything" = "Anything"; "logs_adding" = "Creation"; "logs_editing" = "Editing"; "logs_removing" = "Deletion"; @@ -1226,6 +1404,28 @@ "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_user" = "User"; +"logs_object" = "Object"; +"logs_type" = "Type"; +"logs_changes" = "Changes"; +"logs_time" = "Time"; + +"bans_history" = "Blocks history"; +"bans_history_blocked" = "Blocked"; +"bans_history_initiator" = "Initiator"; +"bans_history_start" = "Start"; +"bans_history_end" = "End"; +"bans_history_time" = "Time"; +"bans_history_reason" = "Reason"; +"bans_history_start" = "Start"; +"bans_history_removed" = "Removed"; +"bans_history_active" = "Active block"; /* Paginator (deprecated) */ @@ -1273,6 +1473,8 @@ "warning" = "Warning"; "question_confirm" = "This action can't be undone. Do you really wanna do it?"; +"confirm_m" = "Confirm"; +"action_successfully" = "Success"; /* User alerts */ @@ -1286,6 +1488,8 @@ /* 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"; @@ -1550,6 +1754,41 @@ "no_results" = "No results"; +/* BadBrowser */ + +"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 */ + +"coverage" = "Coverage"; +"coverage_this_week" = "This graph shows the coverage over the last 7 days."; +"views" = "Views"; +"views_this_week" = "This graph shows the views of community posts over the last 7 days."; + +"full_coverage" = "Full coverage"; +"all_views" = "All views"; + +"subs_coverage" = "Subscribers coverage"; +"subs_views" = "Subscribers views"; + +"viral_coverage" = "Viral coverage"; +"viral_views" = "Viral views"; + +/* Sudo */ + +"you_entered_as" = "You logged 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"; + +/* Phone number */ + +"verify_phone_number" = "Confirm phone number"; +"we_sended_first" = "We sended SMS with code on number"; +"we_sended_end" = "enter it here"; + /* Mobile */ "mobile_friends" = "Friends"; "mobile_photos" = "Photos"; @@ -1565,3 +1804,40 @@ "mobile_like" = "Like"; "mobile_user_info_hide" = "Hide"; "mobile_user_info_show_details" = "Show details"; + +/* Moderation */ + +"section" = "Section"; +"template_ban" = "Ban by template"; +"active_templates" = "Active templates"; +"users_reports" = "Users reports"; +"substring" = "Substring"; +"n_user" = "User"; +"time_before" = "Time earlier than"; +"time_after" = "Time later than"; +"where_for_search" = "WHERE for search by section"; +"block_params" = "Block params"; +"only_rollback" = "Only rollback"; +"only_block" = "Only blocking"; +"rollback_and_block" = "Rollback and blocking"; +"subm" = "Apply"; + +"select_section_for_start" = "Choose a section to get started"; +"results_will_be_there" = "Search results will be displayed here"; +"search_results" = "Search results"; +"cnt" = "pcs"; + +"link_to_page" = "Link on page"; +"or_subnet" = "or subnet"; +"error_when_searching" = "Error while executing request"; +"no_found" = "No found"; +"operation_successfully" = "Operation completed successfully"; + +"unknown_error" = "Unknown error"; +"templates" = "Template"; +"type" = "Type"; +"count" = "Count"; +"time" = "Time"; + +"roll_back" = "rollback"; +"roll_backed" = "rollbacked"; diff --git a/locales/ru.strings b/locales/ru.strings index dc101e2b..49871399 100644 --- a/locales/ru.strings +++ b/locales/ru.strings @@ -132,6 +132,10 @@ "updated_at" = "Обновлено $1"; "user_banned" = "К сожалению, нам пришлось заблокировать страницу пользователя $1."; "user_banned_comment" = "Комментарий модератора:"; +"verified_page" = "Подтверждённая страница"; +"user_is_blocked" = "Пользователь заблокирован"; +"before" = "до"; +"forever" = "навсегда"; /* Wall */ @@ -182,6 +186,7 @@ "nsfw_warning" = "Данный пост может содержать 18+ контент"; "report" = "Пожаловаться"; "attach" = "Прикрепить"; +"detach" = "Открепить"; "attach_photo" = "Прикрепить фото"; "attach_video" = "Прикрепить видео"; "draw_graffiti" = "Нарисовать граффити"; @@ -191,6 +196,8 @@ "version_incompatibility" = "Не удалось отобразить это вложение. Возможно, база данных несовместима с текущей версией OpenVK."; "graffiti" = "Граффити"; "reply" = "Ответить"; +"post_is_ad" = "Этот пост был размещён за взятку."; +"edited_short" = "ред."; /* Friends */ @@ -319,10 +326,16 @@ /* Albums */ "create" = "Создать"; +"album" = "Альбом"; "albums" = "Альбомы"; +"photos" = "фотографий"; +"photo" = "Фотография"; "create_album" = "Создать альбом"; "edit_album" = "Редактировать альбом"; +"edit_photo" = "Изменить фотографию"; "creating_album" = "Создание альбома"; +"delete_photo" = "Удалить фотографию"; +"sure_deleting_photo" = "Вы уверены, что хотите удалить эту фотографию?"; "upload_photo" = "Загрузить фотографию"; "photo" = "Фотография"; "upload_button" = "Загрузить"; @@ -364,6 +377,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 при выборе файлов в ОС Windows или клавишу CMD в Mac OS."; +"album_poster" = "Обложка альбома"; +"select_photo" = "Выберите фотографию"; +"upload_new_photo" = "Загрузить новую фотографию"; + +"is_x_photos_zero" = "Всего ноль фотографий."; +"is_x_photos_one" = "Всего одна фотография."; +"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" = "Заметки"; @@ -399,6 +445,8 @@ "notes_closed" = "Вы не можете прикрепить заметку к записи, так как ваши заметки видны только вам.

Вы можете поменять это в настройках."; "do_not_attach_note" = "Не прикреплять заметку"; +"something" = "Кое-что"; +"supports_xhtml" = "из (X)HTML поддерживается."; /* Notes: Article Viewer */ "aw_legacy_ui" = "Старый интерфейс"; @@ -589,6 +637,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 */ @@ -614,6 +665,15 @@ "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 фотографий"; +"no_videos" = "У вас нет видео."; +"no_videos_results" = "Нет результатов."; /* Notifications */ @@ -650,6 +710,7 @@ "nt_mention_in_video" = "в обсуждении видеозаписи"; "nt_mention_in_note" = "в обсуждении заметки"; "nt_mention_in_topic" = "в обсуждении"; +"nt_sent_gift" = "отправил вам подарок"; /* Time */ @@ -843,6 +904,20 @@ "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" = "Автор"; @@ -982,6 +1057,7 @@ "error_repost_fail" = "Не удалось поделиться записью"; "error_data_too_big" = "Аттрибут '$1' не может быть длиннее $2 $3"; "forbidden" = "Ошибка доступа"; +"unknown_error" = "Неизвестная ошибка"; "forbidden_comment" = "Настройки приватности этого пользователя не разрешают вам смотреть на его страницу."; "changes_saved" = "Изменения сохранены"; "changes_saved_comment" = "Новые данные появятся на вашей странице"; @@ -1017,6 +1093,94 @@ "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" = "Ору нищ не пук."; + +"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" = "Выберите файл или укажите ссылку."; +"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" = "Сообщество успешно забанено"; +"description_too_long" = "Описание слишком длинное."; /* Admin actions */ @@ -1024,14 +1188,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" = "Имя"; @@ -1106,6 +1274,12 @@ "admin_banned_link_initiator" = "Инициатор"; "admin_banned_link_not_specified" = "Ссылка не указана"; "admin_banned_link_not_found" = "Ссылка не найдена"; + +"admin_gift_moved_successfully" = "Подарок успешно перемещён"; +"admin_gift_moved_to_recycle" = "Теперь подарок находится в корзине."; + +"logs" = "Логи"; +"logs_anything" = "Любое"; "logs_adding" = "Создание"; "logs_editing" = "Редактирование"; "logs_removing" = "Удаление"; @@ -1114,6 +1288,28 @@ "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) */ @@ -1163,6 +1359,8 @@ "close" = "Закрыть"; "warning" = "Внимание"; "question_confirm" = "Это действие нельзя отменить. Вы действительно уверены в том что хотите сделать?"; +"confirm_m" = "Подтвердить"; +"action_successfully" = "Операция успешна"; /* User alerts */ @@ -1176,6 +1374,8 @@ /* Away */ +"transition_is_blocked" = "Переход по ссылке заблокирован"; +"caution" = "Предупреждение"; "url_is_banned" = "Переход невозможен"; "url_is_banned_comment" = "Администрация $1 не рекомендует переходить по этой ссылке."; "url_is_banned_comment_r" = "Администрация $1 не рекомендует переходить по этой ссылке.

Причина: $2"; @@ -1442,6 +1642,41 @@ "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" = "Фотографии"; @@ -1457,3 +1692,40 @@ "mobile_like" = "Нравится"; "mobile_user_info_hide" = "Скрыть"; "mobile_user_info_show_details" = "Показать подробнее"; + +/* 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" = "откачено";