[WIP] Textarea: Upload multiple pictures (#800)

* VKAPI: Fix bug when DELETED user appear if there is no user_ids

* Textarea: Make multiple attachments

* постмодернистское искусство

* Use only attachPic for grabbing pic attachments

TODO throw flashFail on bruh moment with pic attachments

* draft masonry picture layout in posts xddd

где мои опиаты???

* fix funny typos in computeMasonryLayout

* Fix video bruh moment in textarea

* Posts: add multiple kakahi for microblog

* Photo: Add minimal implementation of миниатюра открывашка

Co-authored-by: Daniel <60743585+myslivets@users.noreply.github.com>

* Photo: Add ability to slide trough photos in one post

This also gives ability to easily implement comments and actions

* Photo: The Fxck Is This implementation of comments under photo in viewer

* FloatingPhotoViewer: Better CSS

- Fix that details background issue
- Make slide buttons slightly shorter by height

* FloatingPhotoViewer: Refactor, and make it better

- Now you can actually check the comments under EVERY photo
- Fix for textarea. Now you can publish comments

* Fix funny typos xddd

* Kinda fix poll display in non-microblog posts

* Posts: Fix poll display in microblog posts

* Add photos picker (#986)

* early implementation of photos pickir

Добавлен пикер фоточек и быстрая загрузка фото. Так же пофикшен просмотрщик фото в группах. Но, правда, я сломал копипейст, но это ладн.

* Fiks fotos viver four coments.

* Add picking photos from clubs albums

Копипейст и граффити так и не пофикшены

* Fix graffiti and copypaste

Какого-то хуя копипаста у постов срабатывает два раза.

* some fixesx

* dragon drop

* Fix PHP 8 compatibility

* 5 (#988)

---------

Co-authored-by: celestora <kitsuruko@gmail.com>
Co-authored-by: Daniel <60743585+myslivets@users.noreply.github.com>
Co-authored-by: lalka2016 <99399973+lalka2016@users.noreply.github.com>
Co-authored-by: Alexander Minkin <weryskok@gmail.com>
This commit is contained in:
Vladimir Barinov 2023-10-03 19:40:13 +03:00 committed by GitHub
parent 6632d070f5
commit a859fa13a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1268 additions and 113 deletions

92
ServiceAPI/Photos.php Normal file
View file

@ -0,0 +1,92 @@
<?php declare(strict_types=1);
namespace openvk\ServiceAPI;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\{Photos as PhotosRepo, Albums, Clubs};
class Photos implements Handler
{
protected $user;
protected $photos;
function __construct(?User $user)
{
$this->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);
}
}

View file

@ -11,7 +11,7 @@ class Comment extends Post
function getPrettyId(): string function getPrettyId(): string
{ {
return $this->getRecord()->id; return (string)$this->getRecord()->id;
} }
function getVirtualId(): int function getVirtualId(): int

View file

@ -105,7 +105,7 @@ class IP extends RowModel
$this->stateChanges("ip", $ip); $this->stateChanges("ip", $ip);
} }
function save($log): void function save(?bool $log = false): void
{ {
if(is_null($this->getRecord())) if(is_null($this->getRecord()))
$this->stateChanges("first_seen", time()); $this->stateChanges("first_seen", time());

View file

@ -121,14 +121,14 @@ abstract class Media extends Postable
$this->stateChanges("hash", $hash); $this->stateChanges("hash", $hash);
} }
function save(): void function save(?bool $log = false): void
{ {
if(!is_null($this->processingPlaceholder) && is_null($this->getRecord())) { if(!is_null($this->processingPlaceholder) && is_null($this->getRecord())) {
$this->stateChanges("processed", 0); $this->stateChanges("processed", 0);
$this->stateChanges("last_checked", time()); $this->stateChanges("last_checked", time());
} }
parent::save(); parent::save($log);
} }
function delete(bool $softly = true): void function delete(bool $softly = true): void

View file

@ -152,7 +152,7 @@ abstract class Postable extends Attachable
throw new ISE("Setting virtual id manually is forbidden"); throw new ISE("Setting virtual id manually is forbidden");
} }
function save(): void function save(?bool $log = false): void
{ {
$vref = $this->upperNodeReferenceColumnName; $vref = $this->upperNodeReferenceColumnName;
@ -171,7 +171,7 @@ abstract class Postable extends Attachable
$this->stateChanges("edited", time()); $this->stateChanges("edited", time());
}*/ }*/
parent::save(); parent::save($log);
} }
use Traits\TAttachmentHost; use Traits\TAttachmentHost;

View file

@ -1,6 +1,7 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Models\Entities\Traits; namespace openvk\Web\Models\Entities\Traits;
use openvk\Web\Models\Entities\Attachable; use openvk\Web\Models\Entities\{Attachable, Photo};
use openvk\Web\Util\Makima\Makima;
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
trait TAttachmentHost trait TAttachmentHost
@ -30,6 +31,46 @@ trait TAttachmentHost
} }
} }
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 function attach(Attachable $attachment): void
{ {
DatabaseConnection::i()->getContext() DatabaseConnection::i()->getContext()

View file

@ -33,14 +33,26 @@ class Photos
return new Photo($photo); 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([ $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); yield new Photo($photo);
} }
} }
function getUserPhotosCount(User $user)
{
$photos = $this->photos->where([
"owner" => $user->getId(),
"deleted" => 0
]);
return sizeof($photos);
}
} }

View file

@ -2,7 +2,7 @@
namespace openvk\Web\Presenters; namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{Comment, Notifications\MentionNotification, Photo, Video, User, Topic, Post}; use openvk\Web\Models\Entities\{Comment, Notifications\MentionNotification, Photo, Video, User, Topic, Post};
use openvk\Web\Models\Entities\Notifications\CommentNotification; use openvk\Web\Models\Entities\Notifications\CommentNotification;
use openvk\Web\Models\Repositories\{Comments, Clubs, Videos}; use openvk\Web\Models\Repositories\{Comments, Clubs, Videos, Photos};
final class CommentPresenter extends OpenVKPresenter final class CommentPresenter extends OpenVKPresenter
{ {
@ -54,9 +54,6 @@ final class CommentPresenter extends OpenVKPresenter
if ($entity instanceof Post && $entity->getWallOwner()->isBanned()) if ($entity instanceof Post && $entity->getWallOwner()->isBanned())
$this->flashFail("err", tr("error"), tr("forbidden")); $this->flashFail("err", tr("error"), tr("forbidden"));
if($_FILES["_vid_attachment"] && OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading'])
$this->flashFail("err", tr("error"), tr("video_uploads_disabled"));
$flags = 0; $flags = 0;
if($this->postParam("as_group") === "on" && !is_null($club) && $club->canBeModifiedBy($this->user->identity)) if($this->postParam("as_group") === "on" && !is_null($club) && $club->canBeModifiedBy($this->user->identity))
$flags |= 0b10000000; $flags |= 0b10000000;
@ -70,18 +67,22 @@ final class CommentPresenter extends OpenVKPresenter
} }
} }
# TODO move to trait $photos = [];
try { if(!empty($this->postParam("photos"))) {
$photo = NULL; $un = rtrim($this->postParam("photos"), ",");
if($_FILES["_pic_attachment"]["error"] === UPLOAD_ERR_OK) { $arr = explode(",", $un);
$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); 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;
}
} }
} catch(ISE $ex) {
$this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_file_too_big"));
} }
$videos = []; $videos = [];
@ -103,7 +104,7 @@ final class CommentPresenter extends OpenVKPresenter
} }
} }
if(empty($this->postParam("text")) && !$photo && !$video) if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1)
$this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_empty")); $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_empty"));
try { try {
@ -119,8 +120,8 @@ final class CommentPresenter extends OpenVKPresenter
$this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_too_big")); $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_too_big"));
} }
if(!is_null($photo)) foreach($photos as $photo)
$comment->attach($photo); $comment->attach($photo);
if(sizeof($videos) > 0) if(sizeof($videos) > 0)
foreach($videos as $vid) foreach($videos as $vid)

View file

@ -1,5 +1,6 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Presenters; namespace openvk\Web\Presenters;
use openvk\Web\Models\Repositories\{Posts, Comments};
use MessagePack\MessagePack; use MessagePack\MessagePack;
use Chandler\Session\Session; use Chandler\Session\Session;
@ -95,4 +96,41 @@ final class InternalAPIPresenter extends OpenVKPresenter
]); ]);
} }
} }
function renderGetPhotosFromPost(int $owner_id, int $post_id) {
if($_SERVER["REQUEST_METHOD"] !== "POST") {
header("HTTP/1.1 405 Method Not Allowed");
exit("иди нахуй заебал");
}
if($this->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
]);
}
}
} }

View file

@ -223,14 +223,19 @@ final class PhotosPresenter extends OpenVKPresenter
$this->assertUserLoggedIn(); $this->assertUserLoggedIn();
$this->willExecuteWriteAction(true); $this->willExecuteWriteAction(true);
if(is_null($this->queryParam("album"))) if(is_null($this->queryParam("album"))) {
$this->flashFail("err", tr("error"), tr("error_adding_to_deleted"), 500, true); $album = $this->albums->getUserWallAlbum($this->user->identity);
} else {
[$owner, $id] = explode("_", $this->queryParam("album"));
$album = $this->albums->get((int) $id);
}
[$owner, $id] = explode("_", $this->queryParam("album"));
$album = $this->albums->get((int) $id);
if(!$album) if(!$album)
$this->flashFail("err", tr("error"), tr("error_adding_to_deleted"), 500, true); $this->flashFail("err", tr("error"), tr("error_adding_to_deleted"), 500, true);
if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity))
# Для быстрой загрузки фоток из пикера фотографий нужен альбом, но юзер не может загружать фото
# в системные альбомы, так что так.
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); $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"), 500, true);
if($_SERVER["REQUEST_METHOD"] === "POST") { if($_SERVER["REQUEST_METHOD"] === "POST") {
@ -261,6 +266,9 @@ final class PhotosPresenter extends OpenVKPresenter
$this->flashFail("err", tr("no_photo"), tr("select_file"), 500, true); $this->flashFail("err", tr("no_photo"), tr("select_file"), 500, true);
$photos = []; $photos = [];
if((int)$this->postParam("count") > 10)
$this->flashFail("err", tr("no_photo"), "ты еблан", 500, true);
for($i = 0; $i < $this->postParam("count"); $i++) { for($i = 0; $i < $this->postParam("count"); $i++) {
try { try {
$photo = new Photo; $photo = new Photo;

View file

@ -3,7 +3,7 @@ namespace openvk\Web\Presenters;
use openvk\Web\Models\Exceptions\TooMuchOptionsException; use openvk\Web\Models\Exceptions\TooMuchOptionsException;
use openvk\Web\Models\Entities\{Poll, Post, Photo, Video, Club, User}; use openvk\Web\Models\Entities\{Poll, Post, Photo, Video, Club, User};
use openvk\Web\Models\Entities\Notifications\{MentionNotification, RepostNotification, WallPostNotification}; use openvk\Web\Models\Entities\Notifications\{MentionNotification, RepostNotification, WallPostNotification};
use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums, Notes, Videos, Comments}; use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums, Notes, Videos, Comments, Photos};
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
use Nette\InvalidStateException as ISE; use Nette\InvalidStateException as ISE;
use Bhaktaraz\RSSGenerator\Item; use Bhaktaraz\RSSGenerator\Item;
@ -249,23 +249,23 @@ final class WallPresenter extends OpenVKPresenter
if($this->postParam("force_sign") === "on") if($this->postParam("force_sign") === "on")
$flags |= 0b01000000; $flags |= 0b01000000;
try { $photos = [];
$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); 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 { try {
@ -312,7 +312,7 @@ final class WallPresenter extends OpenVKPresenter
} }
} }
if(empty($this->postParam("text")) && !$photo && sizeof($videos) < 1 && !$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")); $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big"));
try { try {
@ -329,8 +329,8 @@ final class WallPresenter extends OpenVKPresenter
$this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_too_big")); $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_too_big"));
} }
if(!is_null($photo)) foreach($photos as $photo)
$post->attach($photo); $post->attach($photo);
if(sizeof($videos) > 0) if(sizeof($videos) > 0)
foreach($videos as $vid) foreach($videos as $vid)

View file

@ -41,7 +41,7 @@
</a> </a>
<a href="/photo{$photo->getPrettyId()}?from=album{$album->getId()}"> <a href="/photo{$photo->getPrettyId()}?from=album{$album->getId()}">
<img class="album-photo--image" src="{$photo->getURL()}" alt="{$photo->getDescription()}" /> <img class="album-photo--image" src="{$photo->getURLBySizeId('tinier')}" alt="{$photo->getDescription()}" />
</a> </a>
</div> </div>
{/foreach} {/foreach}

View file

@ -26,11 +26,11 @@
<hr/> <hr/>
<div style="width: 100%; min-height: 100px;"> <div style="width: 100%; min-height: 100px;" class="ovk-photo-details">
<div style="float: left; min-height: 100px; width: 70%;"> <div style="float: left; min-height: 100px; width: 68%;margin-left: 3px;">
{include "../components/comments.xml", comments => $comments, count => $cCount, page => $cPage, model => "photos", parent => $photo} {include "../components/comments.xml", comments => $comments, count => $cCount, page => $cPage, model => "photos", parent => $photo, custom_id => 999}
</div> </div>
<div style="float: left; min-height: 100px; width: 30%;"> <div style="float:right;min-height: 100px;width: 30%;margin-left: 1px;">
<div> <div>
<h4>{_information}</h4> <h4>{_information}</h4>
<span style="color: grey;">{_info_description}:</span> <span style="color: grey;">{_info_description}:</span>
@ -42,7 +42,7 @@
</div> </div>
<br/> <br/>
<h4>{_actions}</h4> <h4>{_actions}</h4>
{if $thisUser->getId() != $photo->getOwner()->getId()} {if isset($thisUser) && $thisUser->getId() != $photo->getOwner()->getId()}
{var canReport = true} {var canReport = true}
{/if} {/if}
<div n:if="isset($thisUser) && $thisUser->getId() === $photo->getOwner()->getId()"> <div n:if="isset($thisUser) && $thisUser->getId() === $photo->getOwner()->getId()">

View file

@ -6,6 +6,8 @@
{/block} {/block}
{block content} {block content}
{php $GLOBALS["_bigWall"] = 1}
<div class="tabs"> <div class="tabs">
<div n:attr="id => (isset($globalFeed) ? 'ki' : 'activetabs')" class="tab"> <div n:attr="id => (isset($globalFeed) ? 'ki' : 'activetabs')" class="tab">
<a n:attr="id => (isset($globalFeed) ? 'ki' : 'act_tab_a')" href="/feed">{_my_news}</a> <a n:attr="id => (isset($globalFeed) ? 'ki' : 'act_tab_a')" href="/feed">{_my_news}</a>

View file

@ -35,7 +35,7 @@
<a n:if="$canDelete ?? false" class="profile_link" style="display:block;width:96%;" href="/wall{$post->getPrettyId()}/delete">{_delete}</a> <a n:if="$canDelete ?? false" class="profile_link" style="display:block;width:96%;" href="/wall{$post->getPrettyId()}/delete">{_delete}</a>
<a <a
n:if="$thisUser->getChandlerUser()->can('access')->model('admin')->whichBelongsTo(NULL) AND $post->getEditTime()" n:if="isset($thisUser) && $thisUser->getChandlerUser()->can('access')->model('admin')->whichBelongsTo(NULL) AND $post->getEditTime()"
style="display:block;width:96%;" style="display:block;width:96%;"
class="profile_link" class="profile_link"
href="/admin/logs?type=1&obj_type=Post&obj_id={$post->getId()}" href="/admin/logs?type=1&obj_type=Post&obj_id={$post->getId()}"

View file

@ -1,7 +1,8 @@
{if $attachment instanceof \openvk\Web\Models\Entities\Photo} {if $attachment instanceof \openvk\Web\Models\Entities\Photo}
{if !$attachment->isDeleted()} {if !$attachment->isDeleted()}
<a href="{$attachment->getPageUrl()}"> {var $link = "/photo" . ($attachment->isAnonymous() ? ("s/" . base_convert((string) $attachment->getId(), 10, 32)) : $attachment->getPrettyId())}
<img class="media" src="{$attachment->getURLBySizeId('normal')}" alt="{$attachment->getDescription()}" /> <a href="{$link}" onclick="OpenMiniature(event, {$attachment->getURLBySizeId('normal')}, {$parent->getPrettyId()}, {$attachment->getPrettyId()}, {$parentType})">
<img class="media media_makima" src="{$attachment->getURLBySizeId('normal')}" alt="{$attachment->getDescription()}" />
</a> </a>
{else} {else}
<a href="javascript:alert('{_attach_no_longer_available}');"> <a href="javascript:alert('{_attach_no_longer_available}');">

View file

@ -22,9 +22,16 @@
<div class="text" id="text{$comment->getId()}"> <div class="text" id="text{$comment->getId()}">
<span data-text="{$comment->getText(false)}" class="really_text">{$comment->getText()|noescape}</span> <span data-text="{$comment->getText(false)}" class="really_text">{$comment->getText()|noescape}</span>
<div n:ifcontent class="attachments_b"> {var $attachmentsLayout = $comment->getChildrenWithLayout(288)}
<div class="attachment" n:foreach="$comment->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}"> <div n:ifcontent class="attachments attachments_b" style="height: {$attachmentsLayout->height|noescape}; width: {$attachmentsLayout->width|noescape};">
{include "attachment.xml", attachment => $attachment} <div class="attachment" n:foreach="$attachmentsLayout->tiles as $attachment" style="float: {$attachment[3]|noescape}; width: {$attachment[0]|noescape}; height: {$attachment[1]|noescape};" data-localized-nsfw-text="{_nsfw_warning}">
{include "attachment.xml", attachment => $attachment[2], parent => $comment, parentType => "comment"}
</div>
</div>
<div n:ifcontent class="attachments attachments_m">
<div class="attachment" n:foreach="$attachmentsLayout->extras as $attachment">
{include "attachment.xml", attachment => $attachment, post => $comment}
</div> </div>
</div> </div>
</div> </div>

View file

@ -4,7 +4,7 @@
{var $commentsURL = "/al_comments/create/$model/" . $parent->getId()} {var $commentsURL = "/al_comments/create/$model/" . $parent->getId()}
{var $club = $parent instanceof \openvk\Web\Models\Entities\Post && $parent->getTargetWall() < 0 ? (new openvk\Web\Models\Repositories\Clubs)->get(abs($parent->getTargetWall())) : $club} {var $club = $parent instanceof \openvk\Web\Models\Entities\Post && $parent->getTargetWall() < 0 ? (new openvk\Web\Models\Repositories\Clubs)->get(abs($parent->getTargetWall())) : $club}
{if !$readOnly} {if !$readOnly}
{include "textArea.xml", route => $commentsURL, postOpts => false, graffiti => (bool) ovkGetQuirk("comments.allow-graffiti"), club => $club} {include "textArea.xml", route => $commentsURL, postOpts => false, graffiti => (bool) ovkGetQuirk("comments.allow-graffiti"), club => $club, custom_id => $custom_id}
{/if} {/if}
</div> </div>

View file

@ -74,9 +74,20 @@
<div class="text"> <div class="text">
<span data-text="{$post->getText(false)}" class="really_text">{$post->getText()|noescape}</span> <span data-text="{$post->getText(false)}" class="really_text">{$post->getText()|noescape}</span>
<div n:ifcontent class="attachments_b"> {var $width = ($GLOBALS["_bigWall"] ?? false) ? 550 : 320}
<div class="attachment" n:foreach="$post->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}"> {if isset($GLOBALS["_nesAttGloCou"])}
{include "../attachment.xml", attachment => $attachment} {var $width = $width - 70 * $GLOBALS["_nesAttGloCou"]}
{/if}
{var $attachmentsLayout = $post->getChildrenWithLayout($width)}
<div n:ifcontent class="attachments attachments_b" style="height: {$attachmentsLayout->height|noescape}; width: {$attachmentsLayout->width|noescape};">
<div class="attachment" n:foreach="$attachmentsLayout->tiles as $attachment" style="float: {$attachment[3]|noescape}; width: {$attachment[0]|noescape}; height: {$attachment[1]|noescape};" data-localized-nsfw-text="{_nsfw_warning}">
{include "../attachment.xml", attachment => $attachment[2], parent => $post, parentType => "post"}
</div>
</div>
<div n:ifcontent class="attachments attachments_m">
<div class="attachment" n:foreach="$attachmentsLayout->extras as $attachment">
{include "../attachment.xml", attachment => $attachment, post => $post}
</div> </div>
</div> </div>
</div> </div>

View file

@ -65,8 +65,19 @@
<span data-text="{$post->getText(false)}" class="really_text">{$post->getText()|noescape}</span> <span data-text="{$post->getText(false)}" class="really_text">{$post->getText()|noescape}</span>
<div n:ifcontent class="attachments_b"> {var $width = ($GLOBALS["_bigWall"] ?? false) ? 550 : 320}
<div class="attachment" n:foreach="$post->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}"> {if isset($GLOBALS["_nesAttGloCou"])}
{var $width = $width - 70 * $GLOBALS["_nesAttGloCou"]}
{/if}
{var $attachmentsLayout = $post->getChildrenWithLayout($width)}
<div n:ifcontent class="attachments attachments_b" style="height: {$attachmentsLayout->height|noescape}; width: {$attachmentsLayout->width|noescape};">
<div class="attachment" n:foreach="$attachmentsLayout->tiles as $attachment" style="float: {$attachment[3]|noescape}; width: {$attachment[0]|noescape}; height: {$attachment[1]|noescape};" data-localized-nsfw-text="{_nsfw_warning}">
{include "../attachment.xml", attachment => $attachment[2], parent => $post, parentType => "post"}
</div>
</div>
<div n:ifcontent class="attachments attachments_m">
<div class="attachment" n:foreach="$attachmentsLayout->extras as $attachment">
{include "../attachment.xml", attachment => $attachment} {include "../attachment.xml", attachment => $attachment}
</div> </div>
</div> </div>

View file

@ -1,5 +1,6 @@
{php if(!isset($GLOBALS["textAreaCtr"])) $GLOBALS["textAreaCtr"] = 10;} {php if(!isset($GLOBALS["textAreaCtr"])) $GLOBALS["textAreaCtr"] = 10;}
{var $textAreaId = ($post ?? NULL) === NULL ? (++$GLOBALS["textAreaCtr"]) : $post->getId()} {var $textAreaId = ($post ?? NULL) === NULL ? (++$GLOBALS["textAreaCtr"]) : $post->getId()}
{var $textAreaId = ($custom_id ?? NULL) === NULL ? $textAreaId : $custom_id}
<div id="write" style="padding: 5px 0;" onfocusin="expand_wall_textarea({$textAreaId});"> <div id="write" style="padding: 5px 0;" onfocusin="expand_wall_textarea({$textAreaId});">
<form action="{$route}" method="post" enctype="multipart/form-data" style="margin:0;"> <form action="{$route}" method="post" enctype="multipart/form-data" style="margin:0;">
@ -8,8 +9,11 @@
<!-- padding to fix <br/> bug --> <!-- padding to fix <br/> bug -->
</div> </div>
<div id="post-buttons{$textAreaId}" style="display: none;"> <div id="post-buttons{$textAreaId}" style="display: none;">
<div class="upload">
</div>
<div class="post-upload"> <div class="post-upload">
{_attachment}: <span>(unknown)</span> <span style="color: inherit;"></span>
</div> </div>
<div class="post-has-poll"> <div class="post-has-poll">
{_poll} {_poll}
@ -56,7 +60,7 @@
<input type="checkbox" name="as_group" /> {_comment_as_group} <input type="checkbox" name="as_group" /> {_comment_as_group}
</label> </label>
</div> </div>
<input type="file" class="postFileSel" id="postFilePic" name="_pic_attachment" accept="image/*" style="display:none;" /> <input type="hidden" name="photos" value="" />
<input type="hidden" name="videos" value="" /> <input type="hidden" name="videos" value="" />
<input type="hidden" name="poll" value="none" /> <input type="hidden" name="poll" value="none" />
<input type="hidden" id="note" name="note" value="none" /> <input type="hidden" id="note" name="note" value="none" />
@ -73,7 +77,7 @@
<a class="header" href="javascript:toggleMenu({$textAreaId});"> <a class="header" href="javascript:toggleMenu({$textAreaId});">
{_attach} {_attach}
</a> </a>
<a href="javascript:void(document.querySelector('#post-buttons{$textAreaId} input[name=_pic_attachment]').click());"> <a id="photosAttachments" {if !is_null($club ?? NULL) && $club->canBeModifiedBy($thisUser)}data-club="{$club->getId()}"{/if}>
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/mimetypes/application-x-egon.png" /> <img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/mimetypes/application-x-egon.png" />
{_photo} {_photo}
</a> </a>
@ -101,14 +105,11 @@
<script> <script>
$(document).ready(() => { $(document).ready(() => {
u("#post-buttons{$textAreaId} .postFileSel").on("change", function() {
handleUpload.bind(this, {$textAreaId})();
});
setupWallPostInputHandlers({$textAreaId}); setupWallPostInputHandlers({$textAreaId});
}); });
u("#post-buttons{$textAreaId} input[name='videos']")["nodes"].at(0).value = "" u("#post-buttons{$textAreaId} input[name='videos']")["nodes"].at(0).value = ""
u("#post-buttons{$textAreaId} input[name='photos']")["nodes"].at(0).value = ""
</script> </script>
{if $graffiti} {if $graffiti}

305
Web/Util/Makima/Makima.php Normal file
View file

@ -0,0 +1,305 @@
<?php declare(strict_types=1);
namespace openvk\Web\Util\Makima;
use openvk\Web\Models\Entities\Photo;
class Makima
{
private $photos;
const ORIENT_WIDE = 0;
const ORIENT_REGULAR = 1;
const ORIENT_SLIM = 2;
function __construct(array $photos)
{
if(sizeof($photos) < 2)
throw new \LogicException("Minimum attachment count for tiled layout is 2");
$this->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<vector<ThumbTile>>
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;
}
}

View file

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace openvk\Web\Util\Makima;
class MasonryLayout {
public $colSizes;
public $rowSizes;
public $tiles;
public $width;
public $height;
}

View file

@ -0,0 +1,14 @@
<?php declare(strict_types=1);
namespace openvk\Web\Util\Makima;
class ThumbTile {
public $width;
public $height;
public $rowSpan;
public $colSpan;
function __construct(int $rs, int $cs, float $w, float $h)
{
[$this->width, $this->height, $this->rowSpan, $this->colSpan] = [ceil($w), ceil($h), $rs, $cs];
}
}

View file

@ -371,6 +371,8 @@ routes:
handler: "About->humansTxt" handler: "About->humansTxt"
- url: "/dev" - url: "/dev"
handler: "About->dev" handler: "About->dev"
- url: "/iapi/getPhotosFromPost/{num}_{num}"
handler: "InternalAPI->getPhotosFromPost"
- url: "/tour" - url: "/tour"
handler: "About->tour" handler: "About->tour"
- url: "/{?shortCode}" - url: "/{?shortCode}"

View file

@ -744,10 +744,14 @@ h4 {
line-height: 130%; line-height: 130%;
} }
.post-content .attachments_b { .post-content .attachments:first-of-type {
margin-top: 8px; margin-top: 8px;
} }
.post-content .attachments_m .attachment {
width: 98%;
}
.attachment .post { .attachment .post {
width: 102%; width: 102%;
} }
@ -757,6 +761,12 @@ h4 {
image-rendering: -webkit-optimize-contrast; image-rendering: -webkit-optimize-contrast;
} }
.post-content .media_makima {
width: calc(100% - 4px);
height: calc(100% - 4px);
object-fit: cover;
}
.post-signature { .post-signature {
margin: 4px; margin: 4px;
margin-bottom: 2px; margin-bottom: 2px;
@ -2279,6 +2289,124 @@ a.poll-retract-vote {
border-radius: 1px; 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 { .client_app > img {
top: 3px; top: 3px;
position: relative; position: relative;

View file

@ -22,37 +22,14 @@ function trim(string) {
return newStr; return newStr;
} }
function handleUpload(id) {
console.warn("блять...");
u("#post-buttons" + id + " .postFileSel").not("#" + this.id).each(input => input.value = null);
var indicator = u("#post-buttons" + id + " .post-upload");
var file = this.files[0];
if(typeof file === "undefined") {
indicator.attr("style", "display: none;");
} else {
u("span", indicator.nodes[0]).text(trim(file.name) + " (" + humanFileSize(file.size, false) + ")");
indicator.attr("style", "display: block;");
}
document.querySelector("#post-buttons" + id + " #wallAttachmentMenu").classList.add("hidden");
}
function initGraffiti(id) { function initGraffiti(id) {
let canvas = null; let canvas = null;
let msgbox = MessageBox(tr("draw_graffiti"), "<div id='ovkDraw'></div>", [tr("save"), tr("cancel")], [function() { let msgbox = MessageBox(tr("draw_graffiti"), "<div id='ovkDraw'></div>", [tr("save"), tr("cancel")], [function() {
canvas.getImage({includeWatermark: false}).toBlob(blob => { canvas.getImage({includeWatermark: false}).toBlob(blob => {
let fName = "Graffiti-" + Math.ceil(performance.now()).toString() + ".jpeg"; let fName = "Graffiti-" + Math.ceil(performance.now()).toString() + ".jpeg";
let image = new File([blob], fName, {type: "image/jpeg", lastModified: new Date().getTime()}); 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']"); fastUploadImage(id, image)
fileSelect.files = trans.files;
u(fileSelect).trigger("change");
u("#post-buttons" + id + " #write textarea").trigger("focusin");
}, "image/jpeg", 0.92); }, "image/jpeg", 0.92);
canvas.teardown(); 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", `<img id="loader" src="/assets/packages/static/openvk/img/loading_mini.gif">`)
}
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(`
<div class="upload-item" id="aP" data-id="${id}">
<a class="upload-delete">×</a>
<img src="${phot.url}">
</div>
`))
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) { u(".post-like-button").on("click", function(e) {
e.preventDefault(); e.preventDefault();
@ -97,11 +147,12 @@ u(".post-like-button").on("click", function(e) {
function setupWallPostInputHandlers(id) { function setupWallPostInputHandlers(id) {
u("#wall-post-input" + id).on("paste", function(e) { u("#wall-post-input" + id).on("paste", function(e) {
if(e.clipboardData.files.length === 1) { // Если вы находитесь на странице с постом с id 11, то копирование произойдёт джва раза.
var input = u("#post-buttons" + id + " input[name=_pic_attachment]").nodes[0]; // Оч ржачный баг, но вот как его исправить, я, если честно, не знаю.
input.files = e.clipboardData.files;
u(input).trigger("change"); if(e.clipboardData.files.length === 1) {
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) // revert to original size if it is larger (possibly changed by user)
// textArea.style.height = (newHeight > originalHeight ? (newHeight + boost) : originalHeight) + "px"; // 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(
`<div class="ovk-photo-view-dimmer">
<div class="ovk-photo-view">
<div class="photo_com_title">
<text id="photo_com_title_photos">
<img src="/assets/packages/static/openvk/img/loading_mini.gif">
</text>
<div>
<a id="ovk-photo-close">${tr("close")}</a>
</div>
</div>
<center style="margin-bottom: 8pt;">
<div class="ovk-photo-slide-left"></div>
<div class="ovk-photo-slide-right"></div>
<img src="${photo}" style="max-width: 100%; max-height: 60vh; user-select:none;" id="ovk-photo-img">
</center>
<div class="ovk-photo-details">
<img src="/assets/packages/static/openvk/img/loading_mini.gif">
</div>
</div>
</div>`);
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 = '<img src="/assets/packages/static/openvk/img/loading_mini.gif">';
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) { u("#write > form").on("keydown", function(event) {
@ -210,6 +438,7 @@ function addNote(textareaId, nid)
u("body").removeClass("dimmed"); u("body").removeClass("dimmed");
u(".ovk-diag-cont").remove(); u(".ovk-diag-cont").remove();
document.querySelector("html").style.overflowY = "scroll"
} }
async function attachNote(id) async function attachNote(id)
@ -525,3 +754,210 @@ $(document).on("click", "#editPost", (e) => {
text.style.display = "block" text.style.display = "block"
} }
}) })
// copypaste from videos picker
$(document).on("click", "#photosAttachments", async (e) => {
let body = `
<div class="topGrayBlock">
<div style="padding-top: 7px;padding-left: 12px;">
${tr("upload_new_photo")}:
<input type="file" multiple accept="image/*" id="fastFotosUplod" style="display:none">
<input type="button" class="button" value="${tr("upload_button")}" onclick="fastFotosUplod.click()">
<select id="albumSelect" style="width: 154px;float: right;margin-right: 17px;">
<option value="0">${tr("all_photos")}</option>
</select>
</div>
</div>
<div class="photosInsert" style="padding: 5px;height: 287px;overflow-y: scroll;">
<div style="position: fixed;z-index: 1007;width: 92%;background: white;margin-top: -5px;padding-top: 6px;"><h4>${tr("is_x_photos", 0)}</h4></div>
<div class="photosList album-flex" style="margin-top: 20px;"></div>
</div>
`
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", `<img id="loader" style="max-height: 8px;max-width: 36px;" src="/assets/packages/static/openvk/img/loading_mini.gif">`)
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", `
<div style="width: 14%;margin-bottom: 7px;margin-left: 13px;" class="album-photo" data-attachmentdata="${photo.owner_id}_${photo.id}" data-preview="${photo.photo_130}">
<a href="/photo${photo.owner_id}_${photo.id}">
<img class="album-photo--image" src="${photo.photo_130}" alt="..." style="${isAttached ? "background-color: #646464" : ""}">
</a>
</div>
`)
}
if(page < pagesCount) {
insertPlace.insertAdjacentHTML("beforeend", `
<div id="showMorePhotos" data-pagesCount="${pagesCount}" data-page="${page + 1}" style="width: 100%;text-align: center;background: #f0f0f0;height: 22px;padding-top: 9px;cursor:pointer;">
<span>more...</span>
</div>`)
}
}
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", `<option value="${alb.id}">${ovk_proc_strtr(escapeHtml(alb.name), 20)}</option>`)
}
$(".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(`
<div class="upload-item" id="aP" data-id="${ev.currentTarget.dataset.attachmentdata}">
<a class="upload-delete">×</a>
<img src="${ev.currentTarget.dataset.preview}">
</div>
`));
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", `<img id="loader" style="max-height: 8px;max-width: 36px;" src="/assets/packages/static/openvk/img/loading_mini.gif">`)
}
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(`
<div class="upload-item" id="aP" data-id="${pht.owner + "_" + pht.vid}">
<a class="upload-delete">×</a>
<img src="${pht.url}">
</div>
`));
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)
})
})

View file

@ -20,9 +20,12 @@ function MessageBox(title, body, buttons, callbacks) {
button.on("click", function(e) { button.on("click", function(e) {
let __closeDialog = () => { 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(); u(".ovk-diag-cont").remove();
document.querySelector("html").style.overflowY = "scroll"
}; };
Reflect.apply(callbacks[callback], { Reflect.apply(callbacks[callback], {

View file

@ -68,7 +68,7 @@ function toggleMenu(id) {
} }
document.addEventListener("DOMContentLoaded", function() { //BEGIN document.addEventListener("DOMContentLoaded", function() { //BEGIN
u("#_photoDelete").on("click", function(e) { $(document).on("click", "#_photoDelete", function(e) {
var formHtml = "<form id='tmpPhDelF' action='" + u(this).attr("href") + "' >"; var formHtml = "<form id='tmpPhDelF' action='" + u(this).attr("href") + "' >";
formHtml += "<input type='hidden' name='hash' value='" + u("meta[name=csrf]").attr("value") + "' />"; formHtml += "<input type='hidden' name='hash' value='" + u("meta[name=csrf]").attr("value") + "' />";
formHtml += "</form>"; formHtml += "</form>";

View file

@ -347,6 +347,7 @@
"albums" = "Albums"; "albums" = "Albums";
"album" = "Album"; "album" = "Album";
"photos" = "photos"; "photos" = "photos";
"photo" = "Photo";
"create_album" = "Create album"; "create_album" = "Create album";
"edit_album" = "Edit album"; "edit_album" = "Edit album";
"edit_photo" = "Edit photo"; "edit_photo" = "Edit photo";
@ -412,6 +413,20 @@
"tip" = "Tip"; "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."; "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"; "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 */
@ -697,6 +712,7 @@
"selecting_video" = "Selecting videos"; "selecting_video" = "Selecting videos";
"upload_new_video" = "Upload new video"; "upload_new_video" = "Upload new video";
"max_attached_videos" = "Max is 10 videos"; "max_attached_videos" = "Max is 10 videos";
"max_attached_photos" = "Max is 10 photos";
"no_videos" = "You don't have uploaded videos."; "no_videos" = "You don't have uploaded videos.";
"no_videos_results" = "No results."; "no_videos_results" = "No results.";

View file

@ -329,6 +329,7 @@
"album" = "Альбом"; "album" = "Альбом";
"albums" = "Альбомы"; "albums" = "Альбомы";
"photos" = "фотографий"; "photos" = "фотографий";
"photo" = "Фотография";
"create_album" = "Создать альбом"; "create_album" = "Создать альбом";
"edit_album" = "Редактировать альбом"; "edit_album" = "Редактировать альбом";
"edit_photo" = "Изменить фотографию"; "edit_photo" = "Изменить фотографию";
@ -394,6 +395,20 @@
"tip" = "Подсказка"; "tip" = "Подсказка";
"tip_ctrl" = "для того, чтобы выбрать сразу несколько фотографий, удерживайте клавишу Ctrl при выборе файлов в ОС Windows или клавишу CMD в Mac OS."; "tip_ctrl" = "для того, чтобы выбрать сразу несколько фотографий, удерживайте клавишу Ctrl при выборе файлов в ОС Windows или клавишу CMD в Mac OS.";
"album_poster" = "Обложка альбома"; "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 */
@ -656,6 +671,7 @@
"selecting_video" = "Выбор видеозаписей"; "selecting_video" = "Выбор видеозаписей";
"upload_new_video" = "Загрузить новое видео"; "upload_new_video" = "Загрузить новое видео";
"max_attached_videos" = "Максимум 10 видеозаписей"; "max_attached_videos" = "Максимум 10 видеозаписей";
"max_attached_photos" = "Максимум 10 фотографий";
"no_videos" = "У вас нет видео."; "no_videos" = "У вас нет видео.";
"no_videos_results" = "Нет результатов."; "no_videos_results" = "Нет результатов.";