From bddfbdc3682a1c08235537e5a3a52da3db66e0e9 Mon Sep 17 00:00:00 2001 From: veselcraft <veselcraft@icloud.com> Date: Tue, 15 Aug 2023 00:59:57 +0300 Subject: [PATCH 01/26] Reports: Fix 500 error while trying to delete any non-text publication --- Web/Models/Entities/Report.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Web/Models/Entities/Report.php b/Web/Models/Entities/Report.php index 4070952e..09c838ef 100644 --- a/Web/Models/Entities/Report.php +++ b/Web/Models/Entities/Report.php @@ -96,8 +96,13 @@ class Report extends RowModel { if ($this->getContentType() !== "user") { $pubTime = $this->getContentObject()->getPublicationTime(); - $name = $this->getContentObject()->getName(); - $this->getAuthor()->adminNotify("Ваш контент, который вы опубликовали $pubTime ($name) был удалён модераторами инстанса. За повторные или серьёзные нарушения вас могут заблокировать."); + if (method_exists($this->getContentObject(), "getName")) { + $name = $this->getContentObject()->getName(); + $placeholder = "$pubTime ($name)"; + } else { + $placeholder = "$pubTime"; + } + $this->getAuthor()->adminNotify("Ваш контент, который вы опубликовали $placeholder был удалён модераторами инстанса. За повторные или серьёзные нарушения вас могут заблокировать."); $this->getContentObject()->delete($this->getContentType() !== "app"); } From a2c5896fa1702476bcee31958e01821c61aec546 Mon Sep 17 00:00:00 2001 From: veselcraft <veselcraft@icloud.com> Date: Tue, 15 Aug 2023 01:10:49 +0300 Subject: [PATCH 02/26] Reports: Fix 500 error while trying to delete group's publication --- Web/Models/Entities/Report.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Web/Models/Entities/Report.php b/Web/Models/Entities/Report.php index 09c838ef..d449a2e8 100644 --- a/Web/Models/Entities/Report.php +++ b/Web/Models/Entities/Report.php @@ -3,6 +3,7 @@ namespace openvk\Web\Models\Entities; use openvk\Web\Util\DateTime; use Nette\Database\Table\ActiveRow; use openvk\Web\Models\RowModel; +use openvk\Web\Models\Entities\Club; use Chandler\Database\DatabaseConnection; use openvk\Web\Models\Repositories\{Applications, Comments, Notes, Reports, Users, Posts, Photos, Videos, Clubs}; use Chandler\Database\DatabaseConnection as DB; @@ -102,7 +103,13 @@ class Report extends RowModel } else { $placeholder = "$pubTime"; } - $this->getAuthor()->adminNotify("Ваш контент, который вы опубликовали $placeholder был удалён модераторами инстанса. За повторные или серьёзные нарушения вас могут заблокировать."); + + if ($this->getAuthor() instanceof Club) { + $name = $this->getAuthor()->getName(); + $this->getAuthor()->getOwner()->adminNotify("Ваш контент, который опубликовали $placeholder в созданной вами группе \"$name\" был удалён модераторами инстанса. За повторные или серьёзные нарушения группу могут заблокировать."); + } else { + $this->getAuthor()->adminNotify("Ваш контент, который вы опубликовали $placeholder был удалён модераторами инстанса. За повторные или серьёзные нарушения вас могут заблокировать."); + } $this->getContentObject()->delete($this->getContentType() !== "app"); } From 1174ddfa4f7112519c27ba73b86175b6b738babd Mon Sep 17 00:00:00 2001 From: n1rwana <aydashkin@vk.com> Date: Tue, 15 Aug 2023 02:12:48 +0300 Subject: [PATCH 03/26] =?UTF-8?q?[Reports]=20=D0=92=D0=BE=D0=B7=D0=BC?= =?UTF-8?q?=D0=BE=D0=B6=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B9=D1=82=D0=B8=20=D0=BA=20=D0=BF=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D1=83=20=D0=B8=D0=B7=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=D1=80=D0=B8=D1=8F=20(#965)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ПОРНО --- Web/Presenters/templates/Report/ViewContent.xml | 2 +- Web/Presenters/templates/components/comment.xml | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Web/Presenters/templates/Report/ViewContent.xml b/Web/Presenters/templates/Report/ViewContent.xml index 3928495f..249e6b38 100644 --- a/Web/Presenters/templates/Report/ViewContent.xml +++ b/Web/Presenters/templates/Report/ViewContent.xml @@ -16,7 +16,7 @@ {elseif $type == "group" || $type == "user"} {include "../components/group.xml", group => $object, isUser => $type == "user"} {elseif $type == "comment"} - {include "../components/comment.xml", comment => $object, timeOnly => true} + {include "../components/comment.xml", comment => $object, timeOnly => true, linkWithPost => true} {elseif $type == "note"} {include "./content/note.xml", note => $object} {elseif $type == "app"} diff --git a/Web/Presenters/templates/components/comment.xml b/Web/Presenters/templates/components/comment.xml index 5067c6c8..714893d1 100644 --- a/Web/Presenters/templates/components/comment.xml +++ b/Web/Presenters/templates/components/comment.xml @@ -29,7 +29,12 @@ </div> </div> <div n:if="isset($thisUser) &&! ($compact ?? false)" class="post-menu"> - <a href="#_comment{$comment->getId()}" class="date">{$comment->getPublicationTime()}</a> + <a + href="{=$linkWithPost && get_class($comment->getTarget()) == 'openvk\Web\Models\Entities\Post' ? '/wall' . $comment->getTarget()->getPrettyId() : ''}#_comment{$comment->getId()}" + class="date" + > + {$comment->getPublicationTime()} + </a> {if !$timeOnly} | {if $comment->canBeDeletedBy($thisUser)} From c2b6db1b8a3103720ad9217041999f7eb759ba29 Mon Sep 17 00:00:00 2001 From: veselcraft <veselcraft@icloud.com> Date: Tue, 15 Aug 2023 02:39:48 +0300 Subject: [PATCH 04/26] Global: Add underline while cursor hovering to the clickable counters in the left menu --- Web/Presenters/templates/@layout.xml | 2 +- Web/static/css/main.css | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml index c3d561b2..f20169d6 100644 --- a/Web/Presenters/templates/@layout.xml +++ b/Web/Presenters/templates/@layout.xml @@ -176,7 +176,7 @@ <a href="{$thisUser->getURL()}" class="link" title="{_my_page} [Alt+Shift+.]" accesskey=".">{_my_page}</a> <a href="/friends{$thisUser->getId()}" class="link">{_my_friends} <object type="internal/link" n:if="$thisUser->getFollowersCount() > 0"> - <a href="/friends{$thisUser->getId()}?act=incoming"> + <a href="/friends{$thisUser->getId()}?act=incoming" class="linkunderline"> (<b>{$thisUser->getFollowersCount()}</b>) </a> </object> diff --git a/Web/static/css/main.css b/Web/static/css/main.css index c6d22a04..55484f13 100644 --- a/Web/static/css/main.css +++ b/Web/static/css/main.css @@ -29,6 +29,10 @@ a { cursor: pointer; } +.linkunderline:hover { + text-decoration: underline; +} + p { margin: 5px 0; } From 69d0739ef155c7a959b1e7f361e42bf04533167e Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Sun, 20 Aug 2023 13:55:41 +0300 Subject: [PATCH 05/26] dghnryjtyj (#972) --- VKAPI/Handlers/Notes.php | 2 +- Web/Models/Entities/Note.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VKAPI/Handlers/Notes.php b/VKAPI/Handlers/Notes.php index ce26baae..7c9c9fec 100644 --- a/VKAPI/Handlers/Notes.php +++ b/VKAPI/Handlers/Notes.php @@ -211,7 +211,7 @@ final class Notes extends VKAPIRequestHandler $items = []; $note = (new NotesRepo)->getNoteById((int)$id[0], (int)$id[1]); - if($note) { + if($note && !$note->isDeleted()) { $nodez->notes[] = $note->toVkApiStruct(); } } diff --git a/Web/Models/Entities/Note.php b/Web/Models/Entities/Note.php index 37d9ac29..83082bf3 100644 --- a/Web/Models/Entities/Note.php +++ b/Web/Models/Entities/Note.php @@ -124,7 +124,7 @@ class Note extends Postable $res = (object) []; $res->type = "note"; - $res->id = $this->getId(); + $res->id = $this->getVirtualId(); $res->owner_id = $this->getOwner()->getId(); $res->title = $this->getName(); $res->text = $this->getText(); From 0b7a2e1eda9e1b88935418680ff7afad7d82289e Mon Sep 17 00:00:00 2001 From: lvl <90154880+apeeh@users.noreply.github.com> Date: Mon, 21 Aug 2023 13:44:56 +0400 Subject: [PATCH 06/26] Update hy.strings (#973) it's been a while --- locales/hy.strings | 573 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 459 insertions(+), 114 deletions(-) diff --git a/locales/hy.strings b/locales/hy.strings index de11783f..09a8e344 100644 --- a/locales/hy.strings +++ b/locales/hy.strings @@ -17,6 +17,7 @@ "password" = "Գաղտնաբառ"; "registration" = "Գրանցում"; "forgot_password" = "Մոռացե՞լ եք գաղտնաբառը"; + "checkbox_in_registration" = "Ես համաձայն եմ <a href='/privacy'>կոնֆիդենցիալության քաղաքականությանն</a> ու <a href='/terms'>կայքի կանոնադրությանը</a>։"; "checkbox_in_registration_unchecked" = "Դուք պետք է համաձայնվեք պայմանների հետ նախքան գրանցվելը։"; @@ -54,8 +55,11 @@ "register_meta_desc" = "Գրանցվեք $1 -ում հենց հիմա՛"; "register_referer_meta_title" = "$1 -ն հրավիրում է ձեզ դեպի $2"; "register_referer_meta_desc" = "Միացե՛ք $1 -ին և բազմաթիվ օգտատերերին $2 -ու՛մ"; +"registration_welcome_1" = "յուրահատուկ գործընկերների որոնման գործիք է, հիմնված ՎԿոնտակտե–ի կառուցվաշքի վրա։"; +"registration_welcome_2" = "Մենք ցանկանում ենք, որպեսզի Ձեր ընկերները, դասարանցիները, հարևանները և գործընկերները միշտ մնան կապի մեջ։"; "users" = "Օգտատերեր"; +"other_fields" = "Այլ դաշտեր"; /* Profile information */ @@ -75,18 +79,20 @@ "female" = "իգական"; "description" = "Նկարագրություն"; "save" = "Պահպանել"; -"main_information" = "Հիմնական ինֆորմացիա"; +"main_information" = "Հիմնական տեղեկություն"; +"additional_information" = "Հավելյալ տեղեկություն"; "nickname" = "Մականուն"; "online" = "Օնլայն"; "was_online" = "եղել է ցանցում"; "was_online_m" = "եղել է ցանցում"; "was_online_f" = "եղել է ցանցում"; "all_title" = "Բոլորը"; -"information" = "Ինֆորմացիա"; +"information" = "Տեղեկություն"; "status" = "Կարգավիճակ"; -"no_information_provided" = "Ինֆորմացիան բացակայում է"; +"no_information_provided" = "Տեղեկությունը բացակայում է"; "deceased_person" = "Վախճանված"; "none" = "բացակայում է"; +"desc_none" = "առանց նկարագրության"; "send" = "ուղարկել"; "years_zero" = "Զրո տարեկան"; @@ -98,7 +104,7 @@ "show_my_birthday" = "Ցույց տալ ծննդյան օրը"; "show_only_month_and_day" = "Ցուցադրել միայն ամիսն ու օրը"; -"relationship" = "Ընտանեկան դրություն"; +"relationship" = "Կարգավիճակ"; "relationship_0" = "Ընտրված չէ"; "relationship_1" = "Չամուսնացած եմ"; @@ -128,7 +134,7 @@ "politViews_8" = "Ուլտրակոնսերվատիվ"; "politViews_9" = "Լիբերտարիանական"; -"contact_information" = "Կոնտակտային ինֆորմացիա"; +"contact_information" = "Կոնտակտային տեղեկատվություն"; "email" = "Էլեկտրոնային հասցե"; "phone" = "Հեռախոս"; @@ -137,7 +143,7 @@ "city" = "Քաղաք"; "address" = "Հասցե"; -"personal_information" = "Անձնական ինֆորմացիա"; +"personal_information" = "Անձնական տեղեկատվություն"; "interests" = "Հետաքրքրություններ"; "favorite_music" = "Սիրված երգ"; @@ -150,7 +156,7 @@ "updated_at" = "Թարմացված է $1"; "user_banned" = "Ցավո՛ք, մենք ստիպված կասեցրել ենք <b>$1</b>-ի էջը։"; -"user_banned_comment" = "Մոդերատորի մեկնաբանությունը․"; +"user_banned_comment" = "Մոդերատորի մեկնաբանությունը."; /* Wall */ @@ -163,6 +169,9 @@ "post_deact_f" = "ջնջել է էջը հետևյալ բառերով."; "post_deact_silent_m" = "սուս ու փուս ջնջել է էջը։"; "post_deact_silent_f" = "սուս ու փուս ջնջել է էջը։"; +"post_on_your_wall" = "Ձեր պատին"; +"post_on_group_wall" = "$1 –ին"; +"post_on_user_wall" = "$1 –ի պատին"; "wall" = "Պատ"; "post" = "Գրություն"; "write" = "Գրել"; @@ -194,9 +203,9 @@ "attachment" = "Հավելում"; "post_as_group" = "Խմբի անունից"; "comment_as_group" = "Մեկնաբանել խմբի անունից"; -"add_signature" = "Հեղինակի ստորագրություն"; +"add_signature" = "Ավելացնել ստորագրություն"; "contains_nsfw" = "Պարունակում է NSFW մատերիալ"; -"nsfw_warning" = "Այս պոստը կարող է պարունակել 18+ մատերիալ"; +"nsfw_warning" = "Այս գրությունը կարող է պարունակել 18+ մատերիալ"; "report" = "Բողոքարկել"; "attach" = "Ամրացնել"; "attach_photo" = "Ամրացնել նկար"; @@ -214,47 +223,37 @@ /* Friends */ "friends" = "Ընկերներ"; -"followers" = "Բաժանորդներ"; -"follower" = "Բաժանորդ"; +"followers" = "Հետևորդներ"; +"follower" = "Հետևորդ"; "friends_add" = "Ավելացնել դեպի ընկերներ"; "friends_delete" = "Հեռացնել ընկերներից"; "friends_reject" = "Չեղարկել հայտը"; "friends_accept" = "Ընդունել հայտը"; "send_message" = "Ուղարկել նամակ"; -"incoming_req" = "Բաժանորդներ"; -"outcoming_req" = "Հայցեր"; +"incoming_req" = "Սպասվող"; +"outcoming_req" = "Արտագնա"; "req" = "Հայցեր"; "friends_online" = "Ընկերները ցանցում"; "all_friends" = "Բոլոր ընկերները"; "req_zero" = "Ոչ մի հայտ չի գտնվել..."; "req_one" = "Գտնվեց մեկ հայտ"; -"req_few" = "Գտնվեց $1 հայտ"; -"req_many" = "Գտնվեց $1 հայտ"; "req_other" = "Գտնվեց $1 հայտ"; "friends_zero" = "Ոչ մի ընկեր չկա"; "friends_one" = "$1 ընկեր"; -"friends_few" = "$1 ընկեր"; -"friends_many" = "$1 հատ ընկեր"; "friends_other" = "$1 հատ ընկեր"; "friends_list_zero" = "Դուք դեռ չունեք ընկերներ"; "friends_list_one" = "Դուք ունեք մեկ ընկեր"; -"friends_list_few" = "Դուք ունեք $1 ընկեր"; -"friends_list_many" = "Դուք ունեք $1 ընկեր"; "friends_list_other" = "Դուք ունեք $1 ընկեր"; -"followers_zero" = "Ոչ մի բաժանորդ չունեք"; -"followers_one" = "$1 բաժանորդ"; -"followers_few" = "$1 բաժանորդ"; -"followers_many" = "$1 բաժանորդ"; -"followers_other" = "$1 բաժանորդ"; +"followers_zero" = "Ոչ մի հետևորդ չունեք"; +"followers_one" = "$1 հետևորդ"; +"followers_other" = "$1 հետևորդ"; "subscriptions_zero" = "Ոչ մեկի վրա չեք բաժանորդագրվել"; "subscriptions_one" = "$1 բաժանորդագրություն"; -"subscriptions_few" = "$1 բաժանորդագրություն"; -"subscriptions_many" = "$1 բաժանորդագրություն"; "subscriptions_other" = "$1 բաժանորդագրություն"; "friends_list_online_zero" = "Դուք դեռ չունեք ցանցի մեջ գտնվող ընկերներ"; @@ -269,7 +268,7 @@ "subscribe" = "Բաժանորդագրվել"; "unsubscribe" = "Հետ բաժանորդագրվել"; "subscriptions" = "Բաժանորդագրություններ"; -"join_community" = "Մտնել խումբ"; +"join_community" = "Միանալ խմբին"; "leave_community" = "Լքել խումբը"; "check_community" = "Դիտել խումբը"; "min_6_community" = "Անվանումը չպետք է լինի 6 նշից պակաս"; @@ -279,20 +278,19 @@ "create_group" = "Ստեղծել խումբ"; "group_managers" = "Ղեկավարություն"; "group_type" = "Խմբի տեսակ"; -"group_type_open" = "Սա բաց խումբ է․ այստեղ ամեն ոք կարող է մտնել։"; +"group_type_open" = "Սա բաց խումբ է․ ամեն ոք կարող է միանալ իրեն։"; "group_type_closed" = "Սա փակ խումբ է․ այստեղ միանալու համար անհրաժեշտ է հայտ թողնել։"; -"creator" = "Ստեղծող"; +"creator" = "Հեղինակ"; "administrators" = "Ադմինիստրատորներ"; -"add_to_left_menu" = "Ավելացնել դեպի ձախ մենյու"; -"remove_from_left_menu" = "Ջնջել ձախ մենյուից"; -"all_followers" = "Բոլոր բաժանորդները"; +"add_to_left_menu" = "Ավելացնել ձախ մենյույում"; +"remove_from_left_menu" = "Հեռացնել ձախ մենյուից"; +"all_followers" = "Բոլոր հետևորդները"; "only_administrators" = "Միայն ադմինիստրատորները"; -"website" = "Վեբկայք"; +"website" = "Կայք"; "managed" = "Կառավարվում է"; "size" = "Չափ"; "administrators_one" = "$1 ադմինիստրատոր"; -"administrators_few" = "$1 ադմինիստրատոր"; "administrators_other" = "$1 ադմինիստրատոր"; "role" = "Դեր"; @@ -301,30 +299,26 @@ "promote_to_owner" = "Դարձնել տեր"; "devote" = "Հետ բողոքարկել"; "set_comment" = "Փոփոխել մեկնաբանությունը"; -"hidden_yes" = "Թաքցված է"; -"hidden_no" = "Թաքցված չէ"; +"hidden_yes" = "Թաքնված է"; +"hidden_no" = "Թաքնված չէ"; "group_allow_post_for_everyone" = "Թույլատրել հրապարակել բոլորին"; "group_hide_from_global_feed" = "Չցույց տալ հրապարակությունները ընդհանուր լրահոսում։"; -"statistics" = "Ստատիստիկա"; +"statistics" = "Վիճակագրություն"; "group_administrators_list" = "Ադմինների ցուցակ"; -"group_display_only_creator" = "Ցույց տալ միայն խմբի ստեղծողին"; +"group_display_only_creator" = "Ցուցադրել միայն խմբի ստեղծողին"; "group_display_all_administrators" = "Ցուցադրել բոլոր ադմինիստրատորներին"; "group_dont_display_administrators_list" = "Ոչ մեկին ցույց չտալ"; -"group_changeowner_modal_title" = "Օգտատերի իրավասությունների փոխանցում"; -"group_changeowner_modal_text" = "<b>Ուշադրությու՛ն։</b> Դուք փոխանցում եք խմբի բոլոր իրավունքները $1-ին։ Այս գործողությունը անդառնալի է։ Դուք էլի կմնաք ադմինիստրատոր, բայց հեշտությամբ դա ձեզնից կարող են խլել։"; -"group_owner_setted" = "Նոր տերը ($1) նշանակված է $2 միությունում։ Ձեզ տրված են ադմինիստրատորի իրավասություններ։ Եթե ուզում եք հետ բերել իրավասությունները, <a href='/support?act=new'>գրե՛ք կայքի տեխնիկական աջակցությանը</a>։"; +"group_changeowner_modal_title" = "Սեփականատիրոջ իրավասությունների փոխանցում"; +"group_changeowner_modal_text" = "<b>Ուշադրությու՛ն։</b> Դուք փոխանցում եք խմբի բոլոր իրավունքները $1-ին։ Այս գործողությունը անդառնալի է։ Դուք էլի կմնաք ադմինիստրատոր, բայց հեշտությամբ այդ դերը ձեզնից կարող են խլել։"; +"group_owner_setted" = "Նոր տերը ($1) նշանակված է $2 հանրությունում։ Ձեզ տրված են ադմինիստրատորի իրավասություններ։ Եթե ուզում եք հետ բերել իրավասությունները, <a href='/support?act=new'>գրե՛ք կայքի տեխնիկական աջակցությանը</a>։"; "participants_zero" = "Ոչ մի մասնակից"; "participants_one" = "Միայն մեկ մասնակից"; -"participants_few" = "$1 մասնակից"; -"participants_many" = "$1 հատ մասնակից"; "participants_other" = "$1 հատ մասնակից"; "groups_zero" = "Ոչ մի խումբ"; "groups_one" = "Մեկ խումբ"; -"groups_few" = "$1 խումբ"; -"groups_many" = "$1 խումբ"; "groups_other" = "$1 խումբ"; "groups_list_zero" = "Դուք չկաք գեթ ոչ մի խմբում"; @@ -333,12 +327,10 @@ "meetings_zero" = "Ոչ մի հանդիպում"; "meetings_one" = "Մեկ հանդիպում"; -"meetings_few" = "$1 հանդիպում"; -"meetings_many" = "$1 հանդիպում"; "meetings_other" = "$1 հանդիպում"; "open_new_group" = "Նոր խումբ բացել"; -"open_group_desc" = "Չե՞ք կարող խումբ գտնել, բացեք ձերը․․․"; +"open_group_desc" = "Չե՞ք կարողանում գտնել ճիշտ խումբը, բացե՛ք ձերը․․․"; "search_group" = "Խմբի որոնում"; "search_by_groups" = "Որոնում ըստ խմբերի"; "search_group_desc" = "Այստեղ դուք կարող եք փնտրել խմբեր և ընտրել ձեզ ամենահարմարը․․․"; @@ -361,16 +353,36 @@ "albums_zero" = "Ոչ մի ալբոմ չկա"; "albums_one" = "Մեկ ալբոմ"; -"albums_few" = "$1 ալբոմ"; -"albums_many" = "$1 ալբոմ"; "albums_other" = "$1 ալբոմ"; "albums_list_zero" = "Դուք ոչ մի ալբոմ չունեք"; "albums_list_one" = "Դուք ունեք մեկ ալբոմ"; -"albums_list_few" = "Դուք ունեք $1 ալբոմ"; -"albums_list_many" = "Դուք ունեք $1 ալբոմ"; "albums_list_other" = "Դուք ունեք $1 ալբոմ"; +"add_image" = "Ավելացնել պատկեր"; +"add_image_group" = "Վերբեռնել պատկեր"; +"upload_new_picture" = "Ավելացնել նոր պատկեր"; +"uploading_new_image" = "Վերբեռնվում է նոր պատկերը․․․"; +"friends_avatar" = "Այն ավելի կհեշտացնի ձեր ընկերներին ճանաչել Ձեզ, եթե տեղադրեք ձեր իրական լուսանկարը։"; +"groups_avatar" = "Լավ պատկերը կարող է Ձեր խումբը ավելի ճանաչելի դարձնել։"; +"formats_avatar" = "Դուք կարող եք վերբեռնել պատկեր միայն JPG, GIF կամ PNG ֆորմատով։"; +"troubles_avatar" = "Եթե դժվարանում եք տեղադրելուց, փորձե՛ք ընտրել ավելի փոքր նկար։"; +"webcam_avatar" = "Եթե Ձեր համակարգիչը ունի վեբ–տեսախցիկ, Դուք կարող եք <a href='javascript:'>վերցնել նկար</a>։"; + +"update_avatar_notification" = "Պրոֆիլի պատկերը փոփոխված է"; +"update_avatar_description" = "Սեղմեք դիտելու համար"; + +"deleting_avatar" = "Պատկերը ջնջվում է"; +"deleting_avatar_sure" = "Դուք վստա՞հ եք որ ցանկանում եք ջնջել պատկերը։"; + +"deleted_avatar_notification" = "Պատկերը հաջողությամբ ջնջվել է"; + +"save_changes" = "Պահպանել փոփոխությունները"; + +"upd_m" = "թարմացրել է իր պրոֆիլի պատկերը"; +"upd_f" = "թարմացրել է իր պրոֆիլի պատկերը"; +"upd_g" = "թարմացրել է իր խմբի պատկերը"; + /* Notes */ "notes" = "Նշումներ"; @@ -380,26 +392,35 @@ "create_note" = "Ստեղծել նշում"; "edit_note" = "Խմբագրել նշումը"; "actions" = "Գործողություններ"; + +"edited" = "Խմբագրված է"; + +"notes_zero" = "Ոչ մի նշում"; +"notes_one" = "Մեկ նշում"; +"notes_other" = "$1 նշում"; "notes_start_screen" = "Նշումների շնորհիվ Դուք կարող եք կիսվել ընկերների հետ տարբեր իրադարձություններով, և իմանալ թե ինչ է կատարվում իրենց մոտ։"; "note_preview" = "Նախադիտում"; "note_preview_warn" = "Սա ընդամենը նախադիտում է"; "note_preview_warn_details" = "Պահպանելուց հետո նշումները կարող են այլ տեսք ունենալ։ Ու մեկ էլ, այդքան հաճախ նախադիտում մի արեք։"; "note_preview_empty_err" = "Ինչու՞ նախադիտել նշումը առանց վերնագրի կամ բովանդակության։"; -"edited" = "Խմբագրված է"; - -"notes_zero" = "Ոչ մի նշում"; -"notes_one" = "Մեկ նշում"; -"notes_few" = "$1 նշում"; -"notes_many" = "$1 նշում"; -"notes_other" = "$1 նշում"; - "notes_list_zero" = "Ոչ մի նշում չի գտնվել"; "notes_list_one" = "Գտնվեց մեկ նշում"; -"notes_list_few" = "Գտնվեց $1 նշում"; -"notes_list_many" = "Գտնվեց $1 նշում"; "notes_list_other" = "Գտնվեց $1 նշում"; +"select_note" = "Ընտրվում է նշումը"; +"no_notes" = "Դուք չունե՛ք ոչ մի նշում"; + +"error_attaching_note" = "Խնդիր առաջացավ նշումը ընտրելիս"; + +"select_or_create_new" = "Ընտրե՛ք առկա նշումներից, կամ <a href='/notes/create'>ստեղծե՛ք նորը</a>"; + +"notes_closed" = "Դուք չե՛ք կարող ամրացնել նշումը հրապարակությանը, քանզի միայն Դուք եք տեսնում նրանք։<br> Դուք կարող եք փոխել դա <a href=\"/settings?act=privacy\">կարգավորումներում</a>։"; +"do_not_attach_note" = "Չամրացնել նշում"; + +/* Notes: Article Viewer */ +"aw_legacy_ui" = "Հին ինտերֆեյս"; + /* Menus */ "edit_button" = "խմբ."; @@ -415,8 +436,10 @@ "my_settings" = "Իմ կարգավորումները"; "bug_tracker" = "Բագ–թրեքեր"; +"menu_settings" = "Կարգավորումներ"; "menu_login" = "Մուտք"; "menu_registration" = "Գրանցում"; + "menu_help" = "Օգնություն"; "menu_logout" = "Դուրս գալ"; @@ -436,7 +459,7 @@ "left_menu_donate" = "Աջակցել"; "footer_about_instance" = "հոսքի մասին"; -"footer_rules" = "կանոնները"; +"footer_rules" = "կանոններ"; "footer_blog" = "բլոգ"; "footer_help" = "օգնություն"; "footer_developers" = "մշակողներին"; @@ -452,9 +475,9 @@ "interface" = "Արտաքին տեսք"; "security" = "Անվտանգություն"; -"profile_picture" = "Էջի նկար"; +"profile_picture" = "Էջի պատկեր"; -"picture" = "Նկար"; +"picture" = "Պատկեր"; "change_password" = "Փոխել գաղտնաբառը"; "old_password" = "Հին գաղտնաբառը"; @@ -473,13 +496,17 @@ "apply_style_for_this_device" = "Հաստատել տեսքը միայն այս սարքի համար"; "search_for_groups" = "Խմբերի որոնում"; -"search_for_people" = "Մարդկանց որոնում"; +"search_for_users" = "Մարդկանց որոնում"; +"search_for_posts" = "Հրապարակումների որոնում"; +"search_for_comments" = "Մեկնաբանությունների որոնում"; +"search_for_videos" = "Վիդեոների որոնում"; +"search_for_apps" = "Հավելվածների որոնում"; +"search_for_notes" = "Նշումների որոնում"; +"search_for_audios" = "Երաժշտության որոնում"; "search_button" = "Որոնել"; "search_placeholder" = "Գրեք ցանկացած անուն, անվանում կամ բառ"; "results_zero" = "Ոչ մի արդյունք"; "results_one" = "Մեկ արդյունք"; -"results_few" = "$1 արդյունք"; -"results_many" = "$1 արդյունք"; "results_other" = "$1 արդյունք"; "privacy_setting_access_page" = "Ով կարող է տեսնել ինտերնետում իմ էջը"; @@ -504,9 +531,9 @@ "your_email_address" = "Ձեր էլեկտրոնային հասցեն"; "your_page_address" = "Ձեր էջի հասցեն"; "page_address" = "Էջի հասցեն"; -"current_email_address" = "Ներկայիս հասցեն"; -"new_email_address" = "Նոր հասցեն"; -"save_email_address" = "Պահպանել հասցեն"; +"current_email_address" = "Ներկայիս էլեկտրոնային հասցեն"; +"new_email_address" = "Նոր էլեկտրոնային հասցեն"; +"save_email_address" = "Պահպանել էլեկտրոնային հասցեն"; "page_id" = "Էջի ID–ն"; "you_can_also" = "Դուք նաև կարող եք"; "delete_your_page" = "ջնջել ձեր էջը"; @@ -518,13 +545,14 @@ "ui_settings_rating_show" = "Ցուցադրել"; "ui_settings_rating_hide" = "Թաքցնել"; "ui_settings_nsfw_content" = "NSFW-կոնտենտ"; -"ui_settings_nsfw_content_dont_show" = "Ցույց չտա՛լ գլոբալ ժապավենում"; +"ui_settings_nsfw_content_dont_show" = "Ցույց չտա՛լ ընդհանուր ժապավենում"; "ui_settings_nsfw_content_blur" = "Միայն ներծծել"; "ui_settings_nsfw_content_show" = "Ցույց տալ"; -"ui_settings_view_of_posts" = "Փոսթերի տեսակ"; +"ui_settings_view_of_posts" = "Հրապարակումների տեսք"; "ui_settings_view_of_posts_old" = "Հին"; "ui_settings_view_of_posts_microblog" = "Միկրոբլոգ"; "ui_settings_main_page" = "Գլխավոր էջ"; +"ui_settings_sessions" = "Այցելություններ"; "additional_links" = "Հավելյալ հղումներ"; "ad_poster" = "Գովազդային վահանակ"; @@ -547,7 +575,7 @@ "profile_deactivate_reason_5_text" = "Ինձ այստեղ շան տեղ դնող չկա ու ես տխրում եմ։ Դուք կզղջաք որ ես հեռացա..."; "profile_deactivate_reason_6" = "Այլ պատճառ"; -"profile_deactivated_msg" = "Ձեր էջը <b>ջնջված է</b>։<br/><br/>Եթե Դուք ուզենաք նորից օգտվել Ձեր էջով, կարող եք <a href='/settings/reactivate'>ապաակտիվացնել այն</a> մինչև $1:"; +"profile_deactivated_msg" = "Ձեր էջը <b>ջնջված է</b>։<br/><br/>Եթե Դուք ուզենաք նորից օգտվել կայքով, ապա կարող եք <a href='/settings/reactivate'>ապաակտիվացնել այն</a> մինչև $1:"; "profile_deactivated_status" = "Էջը ջնջված է"; "profile_deactivated_info" = "Օգտատիրոջ էջը հեռացվել է։<br/>Մանրամասն տեղեկատվությունը բացակայում է։"; @@ -557,6 +585,18 @@ "end_all_sessions_description" = "Եթե ցանկանում եք դուրս գալ $1–ից ամեն դեվայսից, սեղմե՛ք ներքևի կոճակը"; "end_all_sessions_done" = "Բոլոր սեսսիաները նետված են, ներառյալ բջջային հավելվածները"; +"backdrop_short" = "Ետնապատկեր"; +"backdrop" = "Էջի ետնապատկեր"; +"backdrop_desc" = "Դուք կարող եք տեղադրել երկու նկար, որպես Ձեր պրոֆիլի կամ խմբի նախանկար։ Նրանք կցուցադրվեն էջի ձախ և աջ ծայրերում։ Այս հարմարանքի շնորհիվ Դուք կարող եք ավելացնել հավելյալ անհատականություն Ձեր պրոֆիլին։"; +"backdrop_warn" = "Նկարները կկազմակերպվեն ըստ վերևի դասակարգման։ Իրենց բարձրությունը ավտոմատ կընդլայնվի, ու նրանք կզբաղեցնեն էկրանի բարձրության 100%-ը, նաև կավելացվի մեջտեղում լղոզում։ Այն անհնար կլինի փոխարինել փոխարինել ետնապատկերը OpenVK-ի հիմնական ինտերֆեյսով կամ ավելացնել աուդիո։"; +"backdrop_about_adding" = "Դուք կարող եք ավելացնել միայն մեկ նկար, կախված դիզայնից, վերջնական արդյունքը կարող է տգեղ տեսք ունենալ։ Դուք նաև կարող եք փոխել միայն մեկ նկար. եթե արդեն ունեք երկու տեղադրված նկար և ուզում եք փոխել մեկը – վերբեռնեք միայն մեկ անգամ, ուսի մյուսները չեն ջնջվի։ Որպեսզի ջնջեք երկու նկարները, սեղմե՛ք ներքևի կոճակը, դուք չե՛ք կարող ջնջել նկարները առանձին։"; +"backdrop_save" = "Պահպանել ետնապակներները"; +"backdrop_remove" = "Ջնջել բոլոր ետնապակներները"; +"backdrop_error_title" = "Խնդիր առաջացավ ՝ ետնապակներները պահպանելիս"; +"backdrop_error_no_media" = "Նկարները վնասված են կամ լիարժեք չեն տեղադրվել"; +"backdrop_succ" = "Ետնապատկերի կարգավորումները պահպանված են"; +"backdrop_succ_rem" = "Ետնապատկերները ջնջվեցին"; +"backdrop_succ_desc" = "Օգտատերերը կտեսնեն փոփոխությունները 5 րոպեյվա ընթացքում"; "browse" = "Վերանայում"; /* Two-factor authentication */ @@ -612,11 +652,9 @@ "videos_zero" = "Ոչ մի տեսանյութ չկա"; "videos_one" = "Մեկ տեսանյութ"; -"videos_few" = "$1 տեսանյութ"; -"videos_many" = "$1 տեսանյութ"; "videos_other" = "$1 տեսանյութ"; -"view_video" = "Դիտում"; +"view_video" = "Դիտել"; /* Notifications */ @@ -649,26 +687,34 @@ "nt_photo_instrumental" = "նկարով"; "nt_topic_instrumental" = "թեմայով"; +"nt_you_were_mentioned_u" = "Ձեզ նշել է օգտատերը"; +"nt_you_were_mentioned_g" = "Ձեզ նշել է խումբը"; +"nt_mention_in_post_or_comms" = "իր քննարկման թեմաներից մեկում"; +"nt_mention_in_photo" = "այս նկարի քննարկմանը"; +"nt_mention_in_video" = "այս վիդեոյի քննարկմանը"; +"nt_mention_in_note" = "այս նշման քննարկմանը"; +"nt_mention_in_topic" = "այս քննարկմանը"; + /* Time */ -"time_at_sp" = " -ում "; +"time_at_sp" = " ՝ "; "time_just_now" = "հենց նոր"; "time_exactly_five_minutes_ago" = "ուղիղ հինգ րոպե առաջ"; "time_minutes_ago" = "$1 րոպե առաջ"; "time_today" = "այսօր"; "time_yesterday" = "երեկ"; -"points" = "Ձայն"; +"points" = "Ձայներ"; "points_count" = "ձայն"; "on_your_account" = "ձեր հաշվում"; -"top_up_your_account" = "Լիցքավորել բալանսը"; +"top_up_your_account" = "Ստանալ ավելին"; "you_still_have_x_points" = "Դուք ունեք <b>$1</b> չօգտագործված ձայն։"; "vouchers" = "Վաուչերներ"; "have_voucher" = "Կա վաուչեր"; "voucher_token" = "Վաուչերի կոդ"; "voucher_activators" = "Օգտագործվածները"; -"voucher_explanation" = "Գրե՛ք վաուչերի սերիական համարը։ Սովորաբար այն նշված է լինում կտրոնի կամ նամակի մեջ։"; +"voucher_explanation" = "Ներմուծե՛ք վաուչերի սերիական համարը։ Սովորաբար այն նշված է լինում կտրոնի կամ նամակի մեջ։"; "voucher_explanation_ex" = "Ուշադրություն դարձրե՛ք, որ վաուչերները կարող են սպառվել և օգտագործվել միայն մեկ անգամ։"; "invalid_voucher" = "Անվավեր վաուչեր"; "voucher_bad" = "Հնարավոր է, դուք ներմուծել եք սխալ վաուչեր, արդեն օգտագործել եք այն կամ էլ այն սպառվել է։"; @@ -677,14 +723,12 @@ "redeem" = "Ակտիվացնել վաուչերը"; "deactivate" = "Դեակտիվացնել"; "usages_total" = "Օգտագործումների քանակ"; -"usages_left" = "Մնացին օգտագործումներ"; +"usages_left" = "Մնաց օգտագործելու"; "points_transfer_dialog_header_1" = "Դուք կարող եք ուղարկել ձայները և նվերների մի մասը այլ մարդուն։"; -"points_transfer_dialog_header_2" = "Ձեր ներկայիս բալանսը․ "; +"points_transfer_dialog_header_2" = "Ձեր ներկայիս բալանսը."; "points_amount_one" = "Մեկ ձայն"; -"points_amount_few" = "$1 ձայն"; -"points_amount_many" = "$1 ձայն"; "points_amount_other" = "$1 ձայն"; "transfer_poins" = "Ձայների փոխանցում"; @@ -732,13 +776,9 @@ "gifts" = "Նվերներ"; "gifts_zero" = "Նվերներ չկան"; "gifts_one" = "Մեկ նվեր"; -"gifts_few" = "$1 նվեր"; -"gifts_many" = "$1 նվեր"; "gifts_other" = "$1 նվեր"; "gifts_left" = "Մնաց $1 նվեր"; "gifts_left_one" = "Մնաց մեկ նվեր"; -"gifts_left_few" = "$1 նվեր մնաց"; -"gifts_left_many" = "$1 նվեր մնաց"; "gifts_left_other" = "$1 նվեր մնաց"; "send_gift" = "Ուղարկել նվեր"; @@ -753,8 +793,6 @@ "coins" = "Ձայներ"; "coins_zero" = "0 ձայն"; "coins_one" = "Մեկ ձայն"; -"coins_few" = "$1 ձայն"; -"coins_many" = "$1 ձայն"; "coins_other" = "$1 ձայն"; "users_gifts" = "Նվերներ"; @@ -838,7 +876,7 @@ "support_status_0" = "Հարցը դիտարկման տակ է"; "support_status_1" = "Կա պատասխան"; "support_status_2" = "Փակ է"; -"support_greeting_hi" = "Բարև ձեզ, $1!"; +"support_greeting_hi" = "Բարև՜ Ձեզ, $1"; "support_greeting_regards" = "Հարգանքով, <br/>$1 -ի աջակցման թիմ։"; "support_faq" = "Հաճախ տրվող հարցեր"; @@ -860,6 +898,12 @@ "fast_answers" = "Արագ պատասխաններ"; +"ignore_report" = "Արհամարել զեկույցը"; +"report_number" = "Զեկույց #"; +"list_of_reports" = "Զեկույցների ցանկ"; +"text_of_the_post" = "Հրապարակման տեքստ"; +"today" = "այսօր"; + "comment" = "Մեկնաբանություն"; "sender" = "Ուղարկող"; @@ -874,6 +918,13 @@ "banned_in_support_1" = "Կներե՛ք, <b>$1</b>, բայց հիմա Ձեզ թույլատրված չէ դիմումներ ստեղծել։"; "banned_in_support_2" = "Դրա պատճառաբանությունը սա է․ <b>$1</b>։ Ցավո՛ք, այդ հնարավորությունը մենք Ձեզնից վերցրել ենք առհավետ։"; +"you_can_close_this_ticket_1" = "Եթե չունեք այլատիպ հարցեր, ապա կարող եք "; +"you_can_close_this_ticket_2" = "փակել այս դիմումը"; +"agent_profile_created_1" = "Պրոֆիլը ստեղծված է"; +"agent_profile_created_2" = "Հիմա օգտատերերը կարող են տեսնել Ձեր անհատականեցված անունը և ավատարը ՝ սովորականների փոխարեն։"; +"agent_profile_edited" = "Պրոֆիլը ստեղծված է"; +"agent_profile" = "Իմ Գործակալի քարտը"; + /* Invite */ "invite" = "Հրավիրել"; @@ -914,19 +965,47 @@ "messages_error_1" = "Նամակը չի ուղարկվել"; "messages_error_1_description" = "Այս նամակը ուղարկելու ժամանակ տեղի է ունեցել ընդհանրացված սխալ..."; +/* Polls */ +"poll" = "Քվեարկություն"; +"create_poll" = "Ստեղծել քվեարկություն"; +"poll_title" = "Հարց տալ"; +"poll_add_option" = "Ավելացնել ընտրություն..."; +"poll_anonymous" = "Գաղտնի քվեարկումներ"; +"poll_multiple" = "Տարբեր պատասխաններ"; +"poll_locked" = "Վիկտորինայի ռեժիմ (առանց վերանայման)"; +"poll_edit_expires" = "Սպառվում է. "; +"poll_edit_expires_days" = "օր"; +"poll_editor_tips" = "Backspace խփելը դատարկ ընտրությունը ջնջելու է։ Օգտագործե՛ք Tab/Enter դատարկ ընտրությանը, որպեսզի այն ավելի արագ ստեղծեք։"; +"poll_embed" = "Ներկառուցված կոդ"; + +"poll_voter_count_zero" = "Եղեք <b>առաջի՛ն</b> քվեարկողը"; +"poll_voter_count_one" = "<b>Միայն մեկ</b> օգտատեր է քվեարկել"; +"poll_voter_count_few" = "Քվեարկեց <b>$1</b> հոգի"; +"poll_voter_count_many" = "Քվեարկեց <b>$1</b> հոգի"; +"poll_voter_count_other" = "Քվեարկեց <b>$1</b> հոգի"; + +"poll_voters_list" = "Քվեարկողներ"; + +"poll_anon" = "Գաղտնի"; +"poll_public" = "Հանրային"; +"poll_multi" = "տարբեր ընտրություններ"; +"poll_lock" = "առանց հետ կանչելու"; +"poll_until" = "մինչև $1"; + +"poll_err_to_much_options" = "Չափից շատ տարբերակ է տրված։"; +"poll_err_anonymous" = "Չի լինում տեսնել քվեարկողների ցանկը. քվեարկությունը գաղտնի է"; +"cast_vote" = "Քվեարկե՛լ"; +"retract_vote" = "Չեղարկել իմ քվեարկումը"; + /* Discussions */ "discussions" = "Քննարկումներ"; "messages_one" = "Մեկ նամակ"; -"messages_few" = "$1 նամակ"; -"messages_many" = "$1 նամակ"; "messages_other" = "$1 նամակ"; "topic_messages_count_zero" = "Թեմայում նամակ չկա"; "topic_messages_count_one" = "Թեմայում մեկ նամակ է"; -"topic_messages_count_few" = "Թեմայում $1 նամակ կա"; -"topic_messages_count_many" = "Թեմայում $1 նամակ կա"; "topic_messages_count_other" = "Թեմայում $1 նամակ կա"; "replied" = "պատասխանել է"; @@ -945,11 +1024,9 @@ "delete_topic" = "Ջնջել թեման"; "topics_one" = "Մեկ թեմա"; -"topics_few" = "$1 թեմա"; -"topics_many" = "$1 թեմա"; "topics_other" = "$1 թեմա"; -"created" = "Ստեղծված է"; +"created" = "Ստեղծվել է"; "everyone_can_create_topics" = "Բոլորը կարող են թեմաներ սարքել"; "display_list_of_topics_above_wall" = "Ցուցադրել պատի տակ թեմաների ցուցակը"; @@ -978,8 +1055,10 @@ "error_upload_failed" = "Չհաջողվեց վերբեռնել նկարը"; "error_old_password" = "Հին գաղտնաբառը չի համընկնում"; "error_new_password" = "Նոր գաղտնաբառերը չեն համընկնում"; +"error_weak_password" = "Գաղտնաբառը այդքան էլ խիստ չէ։ Այն առնվազն պետք է պարունակի 8 նիշ, մեկ մեծատառ տառ և մեկ թիվ։"; "error_shorturl_incorrect" = "Կարճ հասցեն ունի սխալ ֆորմատ"; -"error_repost_fail" = "Չհաջողվեց կիսվել գրության հետ"; +"error_repost_fail" = "Չհաջողվեց կիսվել գրությունով"; +"error_data_too_big" = "'$1' ատտրիբուտը պետք է առնվազն լինի $2 –ը $3 –ի չափ երկար"; "forbidden" = "Հասանելիության սխալ"; "forbidden_comment" = "Այս օգտատիրոջ գաղտնիության կարգավորումները ձեզ թույլ չեն տալիս դիտել օգտատերի էջը։"; @@ -1015,7 +1094,7 @@ "suspicious_registration_attempt_comment" = "Դուք մի տեսակ փորձել եք սխալ տեղից գրանցվել։"; "rate_limit_error" = "Հե՛յ, կարող ա՞ խառնել ես։"; -"rate_limit_error_comment" = "Ա՛յ $1, չի՛ կարելի այսքան հաճախ սպամ հրապարակել։ Հո դու Կառլենը չե՞ս։ Բացառության կոդ․ $2։"; +"rate_limit_error_comment" = "Ա՛յ $1 ջան, չի՛ կարելի այսքան հաճախ սպամել։ Հո դու Գրիգորիսը չե՞ս։ Բացառության կոդ․ $2։"; "not_enough_permissions" = "Այդքան իրավասություն չկա"; "not_enough_permissions_comment" = "Դուք բավական իրավասություն չունեք այս գործողությունը կատարելու համար։"; @@ -1047,7 +1126,7 @@ /* Admin panel */ -"admin" = "Ադմին-վահանակ"; +"admin" = "Ադմինի վահանակ"; "admin_ownerid" = "Օգտատիրոջ ID"; "admin_author" = "Հեղինակ"; @@ -1124,7 +1203,7 @@ "admin_banned_links" = "Արգելափակված հղումներ"; "admin_banned_link" = "Հղում"; "admin_banned_domain" = "Դոմեն"; -"admin_banned_link_description" = "Պրոտոկոլով (https://example.com/)"; +"admin_banned_link_description" = "Պրոտոկոլով (https://example.am/)"; "admin_banned_link_regexp" = "Ռեգուլյար արտահայտություն"; "admin_banned_link_regexp_description" = "Տեղադրվում է վերոնշյալ դոմենից հետո։ Մի լրացրե՛ք, եթե ցանկանում եք արգելափակել ամբողջ դոմենը"; "admin_banned_link_reason" = "Պատճառ"; @@ -1132,6 +1211,15 @@ "admin_banned_link_not_specified" = "Հղումը նշված չէ"; "admin_banned_link_not_found" = "Հղումը չի գտնվել"; +"logs_adding" = "Ստեղծում"; +"logs_editing" = "Խմբագրում"; +"logs_removing" = "Ջնջում"; +"logs_restoring" = "Վերականգնում"; +"logs_added" = "ստեղծվել է"; +"logs_edited" = "խմբագրվել է"; +"logs_removed" = "ջնջվել է"; +"logs_restored" = "վերականգնվել է"; + /* Paginator (deprecated) */ "paginator_back" = "Հետ"; @@ -1150,29 +1238,20 @@ "instance_links" = "Հոսքերի հղումներ․"; "about_users_one" = "<b>Մեկ</b> օգտատեր"; -"about_users_few" = "<b>$1</b> օգտատեր"; -"about_users_many" = "<b>$1</b> օգտատեր"; "about_users_other" = "<b>$1</b> օգտատեր"; "about_online_users_one" = "<b>Մեկ</b> օգտատեր է ցանցի մեջ"; -"about_online_users_few" = "<b>$1</b> օգտատեր է ցանցի մեջ"; -"about_online_users_many" = "<b>$1</b> օգտատեր է ցանցի մեջ"; "about_online_users_other" = "<b>$1</b> օգտատեր է ցանցի մեջ"; "about_active_users_one" = "<b>Մեկ</b> ակտիվ օգտատեր"; -"about_active_users_few" = "<b>$1</b> ակտիվ օգտատեր"; -"about_active_users_many" = "<b>$1</b> ակտիվ օգտատեր"; "about_active_users_other" = "<b>$1</b> ակտիվ օգտատեր"; "about_groups_one" = "<b>Մեկ</b> խումբ"; -"about_groups_few" = "<b>$1</b> խումբ"; -"about_groups_many" = "<b>$1</b> խումբ"; "about_groups_other" = "<b>$1</b> խումբ"; "about_wall_posts_one" = "<b>Մեկ</b> գրություն պատերի վրա"; -"about_wall_posts_few" = "<b>$1</b> գրություն պատերի վրա"; -"about_wall_posts_many" = "<b>$1</b> գրություն պատերի վրա"; "about_wall_posts_other" = "<b>$1</b> գրություն պատերի վրա"; + "about_watch_rules" = "տես <a href='$1'>այստեղ</a>․"; /* Dialogs */ @@ -1191,10 +1270,11 @@ /* User alerts */ "user_alert_scam" = "Այս հաշվի վրա բազմաթիվ բողոքներ են եկել խարդախության հետ կապված։ Խնդրվում է զգույշ լինել, հատկապես եթե Ձեզնից փորձեն գումար խնդրել և շորթել։"; +"user_may_not_reply" = "Այս օգտատերը կարող է Ձեզ չպատասխանել, ձեր անվտանգության կարգավորումների պատճառով։ <a href='/settings?act=privacy'>Բացել անվտանգության կարգավորումները</a>"; /* Cookies pop-up */ -"cookies_popup_content" = "Cookie բառը անգլերենից նշանակում է թխվածքաբլիթ, իսկ թխվածքաբլիթը համեղ է։ Մեր կայքը չի ուտում թխվածք, բայց օգտագործում է այն ուղղակի սեսսիան կողմնորոշելու համար։ Ավելի մանրամասն կարող եք ծանոթանալ մեր <a href='/privacy'>գաղտնիության քաղաքականությանը</a> հավելյալ ինֆորմացիայի համար։"; +"cookies_popup_content" = "Cookie բառը թարգմանաբար նշանակում է թխվածքաբլիթ, իսկ թխվածքաբլիթը լավ բան է։ Մեր կայքը չի ուտում թխվածք, բայց օգտագործում է այն ՝ այցելությունը կողմնորոշելու համար։ Ավելի մանրամասն կարող եք ծանոթանալ մեր <a href='/privacy'>գաղտնիության քաղաքականությանը</a> հավելյալ ինֆորմացիայի համար։"; "cookies_popup_agree" = "Համաձայն եմ"; /* Away */ @@ -1206,6 +1286,42 @@ "url_is_banned_title" = "Հղում դեպի կասկածելի կայք"; "url_is_banned_proceed" = "Անցնել հղումով"; +"recently" = "Վերջերս"; + +/* Helpdesk */ + +"helpdesk" = "Աջակցում"; +"helpdesk_agent" = "Աջակցման գործակալ"; +"helpdesk_agent_card" = "Գործակալի քարտ"; +"helpdesk_positive_answers" = "դրական պատասխաններ"; +"helpdesk_negative_answers" = "բացասական պատասխաններ"; +"helpdesk_all_answers" = "բոլոր պատասխանները"; +"helpdesk_showing_name" = "Ցուցադրվող անունը"; +"helpdesk_show_number" = "Ցույց տալ թիվը"; +"helpdesk_avatar_url" = "Ավատարի հղումը"; + +/* Chandler */ + +"c_user_removed_from_group" = "Այս օգտատերը հեռացվել է խմբից"; +"c_permission_removed_from_group" = "Թույլտվությունը հեռացվել է խմբից"; +"c_group_removed" = "Խումբը ջնջվել է"; +"c_groups" = "Chandler–ի Խմբեր"; +"c_users" = "Chandler–ի Օգտատերեր"; +"c_group_permissions" = "Իրավասություններ"; +"c_group_members" = "Մասնակիցներ"; +"c_model" = "Մոդել"; +"c_permission" = "Իրավասություն"; +"c_permissions" = "Իրավասություններ"; +"c_color" = "Գույն"; +"add" = "Ավելացնել"; +"c_edit_groups" = "Խմբագրել Խմբերը"; +"c_user_is_not_in_group" = "Օգտատիրոջ և խմբի հանդեպ հարաբերությունները չգտնվեցին։"; +"c_permission_not_found" = "Իրավասության և խմբի հանդեպ հարաբերությունները չգտնվեցին։"; +"c_group_not_found" = "Խումբը չգտնվե՛ց։"; +"c_user_is_already_in_group" = "Այս օգտատերը արդեն խմբի անդամ է։"; +"c_add_to_group" = "Ավելացնել խմբին"; +"c_remove_from_group" = "Հեռացնել խմբից"; + /* Maintenance */ "global_maintenance" = "Տեխնիկական աշխատանքներ"; @@ -1214,3 +1330,232 @@ "undergoing_section_maintenance" = "Ցավոք սրտի, <b>$1</b> բաժինը ժամանակավորապես անհասանելի է։ Մենք արդեն աշխատում ենք խնդիրները շտկելու ուղղությամբ։ Խնդրում ենք այցելել մի քիչ ուշ։"; "topics" = "Թեմաներ"; + + +/* Tutorial */ + +"tour_title" = "Կայքի ճամփորդություն"; +"reg_title" = "Գրանցում"; +"ifnotlike_title" = " "Ի՞նչ եթե ինձ այս կայքը դուր չի գալիս։" "; +"tour_promo" = "Ինչ է Ձեզ սպասվում գրանցումից հետո"; + +"reg_text" = "<a href='/reg'>Օգտատիրոջ գրանցումը</a> լրիվ անվճար է և տևում է երկու րոպեյից ոչ ավել։"; +"ifnotlike_text" = "Դուք միշտ կարող եք ջնջել Ձեր հաշիվը"; + + +"tour_next" = "Հաջորդը →"; +"tour_reg" = "Գրանցում →"; + + +"tour_section_1" = "Սկիզբ"; +"tour_section_2" = "Պրոֆիլ"; +"tour_section_3" = "Նկարներ"; +"tour_section_4" = "Որոնում"; +"tour_section_5" = "Վիդեոներ"; +"tour_section_6" = "Աուդիոներ"; +"tour_section_7" = "Հիմնական լուրերի ժապավեն"; +"tour_section_8" = "Ընդհանուր լուրերի ժապավեն"; +"tour_section_9" = "Խմբեր"; +"tour_section_10" = "Իրադարձություններ"; +"tour_section_11" = "Թեմաներ"; +"tour_section_12" = "Անհատականացում"; +"tour_section_13" = "Պրոմոկոդեր"; +"tour_section_14" = "Հեռախոսի տարբերակ"; + + +"tour_section_1_title_1" = "Որտեղի՞ց սկսել"; +"tour_section_1_text_1" = "Օգտատիրոջ գրանցումը ամենաառաջին քայլն է այստեղ Ձեր ճանապարհը սկսելու համար։"; +"tour_section_1_text_2" = "Գրանցվելու համար պետք է ունենալ էլ. հասցե և գաղտնաբառ։"; +"tour_section_1_text_3" = "<b>Հիշե՛ք.</b> Ձեր էլ. հասցեն կօգտագործվի կայք մուտք գործելու համար։ Դուք նաև կունենաք լիիրավ իրավունք չնշել Ձեր ազգանունը գրանցվելիս։ Եթե հանկարծ կորցնեք մուտք գործելու գաղտնաբառը, միշտ կարող եք օգտվել <a href='/restore'>վերականգնման էջից</a>։"; +"tour_section_1_bottom_text_1" = "Գրանցվելով այս կայքում, Դուք համաձայնվում եք <a href='/terms'>կայքի կանոններին</a> և <a href='/privacy'>գաղտնիության քաղաքականությանը</a>։"; + +"tour_section_2_title_1" = "Ձեր պրոֆիլը"; +"tour_section_2_text_1_1" = "Գրանցվելուց հետո Դուք ավտոմատ կերպով կվերահղվեք դեպի <b>ձեր</b> էջը:"; +"tour_section_2_text_1_2" = "Դուք կարող եք խմբագրել այն որտեղ և երբ ցանկանաք։"; +"tour_section_2_text_1_3" = "<b>Ակնարկ.</b> Որպեսզի Ձեր պրոֆիլը թույն ու ներկայանալի տեսք ունենա, կարող եք այն լրացնել տեղեկությամբ, կամ էլ տեղադրել լուսանկար, որը օրինակի համար ցույց է տալիս Ձեր վերաբերմունքը Կարգին Հաղորդմանը։"; +"tour_section_2_bottom_text_1" = "Դուք եք միայն որոշում ինչքան տեղեկություն կարող են իմանալ Ձեր մասին ընկերները։"; +"tour_section_2_title_2" = "Տեղադրել սեփական անվտանգության կարգավորումները։"; +"tour_section_2_text_2_1" = "Դուք կարող եք սահմանել թե ինչ տեսակի տեղեկություն կարող է երևալ Ձեր էջում։"; +"tour_section_2_text_2_2" = "Դուք իրավունք ունեք բլոկավորել հսանելիությունը Ձեր էջին որոնողական համակարգերից ու չգրանցված օգտատերերից։"; +"tour_section_2_text_2_3" = "<b>Հիշե՛ք.</b> հետագայում անվտանգության կարգավորումները կընդլայնվեն։"; +"tour_section_2_title_3" = "Պրոֆիլի URL"; +"tour_section_2_text_3_1" = "էջը գրանցելուն պես Դուք ստանում եք անձնական ID, ասենք ՝ <b>@id12345</b>"; +"tour_section_2_text_3_2" = "<b>Սովորական ID-ն</b>, որը ստացվում է գրանցումից հետո, <b>մնում է անփոփոխ</b>"; +"tour_section_2_text_3_3" = "Բայց Ձեր էջի կարգավորումներում Դուք կարող եք տեղադրել անձնական հասցեն, և այն <b>կարող է փոխվել</b> երբ կամենաք"; +"tour_section_2_text_3_4" = "<b>Ակնարկ.</b> Դուք կարող եք վերցնել ցանկացած հասցե, որը առնվազն 5 նիշանի է։ Փորձե՛ք վերցնել թույն URL :Ճ"; +"tour_section_2_bottom_text_2" = "<i>Ցանկացած կարճ հասցե լատինատառ փոքրատառ տառերով սպասարկվում է; այն կարող է պարունակել թվեր (ոչ սկզբում), կետեր և ընդգծումնր (ոչ սկզբում կամ վերջում)</i>"; +"tour_section_2_title_4" = "Պատ"; + + +"tour_section_3_title_1" = "Կիսվե՛ք Ձեր կյանքի պահերով"; +"tour_section_3_text_1" = ""Լուսանկարների" բաժինը հասանելի է Ձեզ անմիջապես գրանցումից հետո"; +"tour_section_3_text_2" = "Դուք կարող եք դիտել օգտատիրոջ ալբոմները կամ էլ ստեղծել ձերը"; +"tour_section_3_text_3" = "Հասանելիություն տալ բոլոր ալբոմներին ուրիշներին, ինչը կառավարվում է Ձեր էջի անվտանգության կարգավորումներով"; +"tour_section_3_bottom_text_1" = "Դուք կարող եք ստեղծել անսահմանափակ քանակությամբ ալբոմներ, ճամփորդության կամ հանգստի համար, կամ էլ զուտ մեմերի համար"; + + +"tour_section_4_title_1" = "Որոնում"; +"tour_section_4_text_1" = ""Որոնման" բաժինը թույլ է տալիս փնտրել մարդկանց և խմբերը։"; +"tour_section_4_text_2" = "Կայքի հենց այս բաժինը ժամանակի ընթացքում ընդլայնվում է"; +"tour_section_4_text_3" = "Որպեսզի սկսեք որոնելը, Դուք պետք է իմանաք օգտատիրոջ անունը (կամ ազգանունը), և եթե Ձեզ հետաքրքիր ա խումբ ճարելը, ապա պետք է ճշտել իր անունը։"; +"tour_section_4_title_2" = "Արագ որոնում"; +"tour_section_4_text_4" = "Եթե ուզում եք խնայել ժամանակ, որոնման բարը միշտ հասանելի է կայքի վերնամասում"; + + +"tour_section_5_title_1" = "Վերբեռնե՛ք և կիսվե՛ք վիդեոներով ընկերների հետ"; +"tour_section_5_text_1" = "Դուք կարող եք տեղադրել անսահմանափակ վիդեոներ և կարճ հոլովակներ"; +"tour_section_5_text_2" = ""Վիդեոների" բաժինը ղեկավարվում է անվտանգության կարգավորումներով"; +"tour_section_5_bottom_text_1" = "Վիդեոները կարող են տեղադրվել անցնելով "Վիդեոների" բաժինը, ուղղակի կցելով դրանք պատին."; +"tour_section_5_title_2" = "YouTube-ից վիդեոների ներկրում"; +"tour_section_5_text_3" = "Ուղիղ տեղադրումից բացի, նաև կարող եք ամրացնել Ձեր սիրելի YouTube–յան վիդեոների հղումները"; + + +"tour_section_6_title_1" = "Աուդիոների բաժինը, որը հլը չկա բհահահսհդ xDDD հորս արևևև"; +"tour_section_6_text_1" = "Ինչպես ասվում էր Կարգին Հաղորդումում. «ապե մի քիչ էլ պահի ստե բան չի երևում»։ Վատ չէր լինի պատմել այս բաժնի մասին, բայց Վրիսկա ախպերը բեսամթ ալարել ա էս սարքել (նենց լավ ա էլի :Ճ):"; + + +"tour_section_7_title_1" = "Հետևե՛ք թե ինչ են գրում Ձեր ընկերներ"; +"tour_section_7_text_1" = ""Լուրերի" բաժինը բաժանվում է երկու տիպի. հիմնական և ընդհանուր ժապավենների"; +"tour_section_7_text_2" = "Հիմնական ժապավենը ցուցադրում է Ձեր ընկերների ու խմբերի նորությունները"; +"tour_section_7_bottom_text_1" = "Մենք չենք սարքում Ձեր լուրերի ժապավենը։ <b>Դուք եք ստեղծում այն</b>։"; + + +"tour_section_8_title_1" = "Հետևե՛ք այստեղ քննարկվող թեմաներին"; +"tour_section_8_text_1" = "Ընդհանուր ժապավենը ցուցադրում է բոլոր օգտատերերի և խմբերի թեմաները"; +"tour_section_8_text_2" = "Այս բաժինը խորհուրդ չի տրվում դիտել նյարդայիններին, հղիներին ու Վարդան Ղուկասյանին լսողներին"; +"tour_section_8_bottom_text_1" = "Ընդհանուր ժապավենի տեսքը չի տարբերվում հիմնականից"; +"tour_section_8_bottom_text_2" = "Ժապավենը ունի տարատեսակ կոնտենտ, սովորական նկարներից և վիդեոներից մինչև գաղտնի գրառումներ և քվեարկություններ"; + + +"tour_section_9_title_1" = "Ստեղծե՛ք խմբե՛ր"; +"tour_section_9_text_1" = "Կայքը արդեն վաղուց ունի հազարավոր խմբեր նվիրված տարբեր թեմաներին և երկրպագումներին"; +"tour_section_9_text_2" = "Դուք կարող եք միանալ ցանկացած խմբին, եթե չեք գտնում Ձեր ուզածը, կարող եք ստեղծել այն"; +"tour_section_9_text_3" = "Ամեն խումբ ունի իր վիքի էջերը, ալբոմները, հղումները և քննարկումները"; +"tour_section_9_title_2" = "Կառավարեք խումբը ընկերների հետ"; +"tour_section_9_text_2_1" = "Կառավարեք խումբը "Խմբագրել Խումբը" բաժնում հանրության ավատարի ներքո"; +"tour_section_9_text_2_2" = "Ստեղծեք Ձեր ադմինիստրատորների ու մոդերատորների թիմը, ում Դուք վստահում եք"; +"tour_section_9_text_2_3" = "Դուք կարող եք թաքցնել ադմինիստրատորին, և նա չի երևա Ձեր խմբի ոչ մի անկյունում"; +"tour_section_9_bottom_text_1" = ""Իմ Խմբերը" բաժինը կայքի ձախ մենյույում է գտնվում"; +"tour_section_9_bottom_text_2" = "Խմբի օրինակ"; +"tour_section_9_bottom_text_3" = "Խմբերը իրական կազմակերպույուններ են, որոնց մասնակիցները ցանկանում են մնալ կապի հետ իրենց լսարանի հետ"; + + +"tour_section_10_title_1" = "Վա՛յ"; +"tour_section_10_text_1" = "Այս բաժնում էլ լավ կլիներ սարքել ծանոթություն, սակայն այն դեռ սարքվում է։ Եկե՛ք սիրուն ձևերով շրջանցեք այն և առաջ գնանք..."; + + +"tour_section_11_title_1" = "Տեսքեր"; +"tour_section_11_text_1" = "Գրանցվելուց հետո Ձեր էջում կիրառվում է սովորական տեսքը"; +"tour_section_11_text_2" = "Որոշ նորեկները կարող է չսիրեն լռելյայն տեսքը, քանի որ այն անգամ հնության զգացում է տալիս"; +"tour_section_11_text_3" = "<b>Բայց հլը հո՛պ.</b> Դուք կարող եք անգամ ստեղծել Ձեր տեսքը ՝ կարդալով <a href='https://docs.openvk.uk/'>դոկումենտացիան</a>, կամ էլ ընտրել եղածներից մեկը"; +"tour_section_11_bottom_text_1" = "Տեսքերի ցանկը հասանելի է "Իմ Կարգավորումներ" –ի "Ինտերֆեյս" բաժնում;"; +"tour_section_11_wordart" = "<img src='https://openvk.uk/assets/packages/static/openvk/img/tour/wordart_en.png' width='65%'>"; + +"tour_section_12_title_1" = "Պրոֆիլ և խմբի ետնապատկերներ"; +"tour_section_12_text_1" = "Դուք կարող եք երկու ետնապատկեր տեղադրել"; +"tour_section_12_text_2" = "Նրանք կցուցադրվեն Ձեր էջի ծայրամասերում"; +"tour_section_12_text_3" = "<b>Ակնարկ.</b> նախքան ետնապատկեր տեղադրելը, փորձե՛ք էքսպերիմենտներ անել իր հետ. փոխել գույնը, հայելու էֆֆեկտ դնել կամ մի բան անել"; +"tour_section_12_title_2" = "Ավատարներ"; +"tour_section_12_text_2_1" = "Դուք կարող եք դնել տարբեր կարգավորումներ ավատարը տեսնելու համար. սովորական, շրջանաձև կամ քառակուսի (1:1)"; +"tour_section_12_text_2_2" = "Այս կարգավորումները տեսանելի են միայն Ձեզ"; +"tour_section_12_title_3" = "Ձախ մենյույի փոփոխումը"; +"tour_section_12_text_3_1" = "Եթե պետք է, կարող եք թաքցնել էջում որոշակի բաժինները"; +"tour_section_12_text_3_2" = "<b>Հիշե՛ք.</b> Հիմնական բաժինները (Իմ Էջը, Իմ Ընկերները, Իմ Պատասխանները, Իմ Կարգավորումները) չի լինի թաքցնել"; +"tour_section_12_title_4" = "Գրառումների դիտարկում"; +"tour_section_12_text_4_1" = "Եթե հոգնել եք պատի հին դիզայնից որը վաղեմի հայտնի ՎԿոնտակտե–ին էր հարիր, ապա միշտ էլ կարող եք փոխել այն սարքելով միկրոբլոգ"; +"tour_section_12_text_4_2" = "Գրառումների տեսքը կարող է փոփոխվել երկու տարբերակի միջև ՝ ցանկացած ժամանակ"; +"tour_section_12_text_4_3" = "<b>Հաշվի առեք</b>, որ եթե հին դիզայնն եք ընտրել, վերջին մեկնաբանությունները չեն ցուցադրվի"; +"tour_section_12_bottom_text_1" = "Ետնապատկերի կարգավորման էջ"; +"tour_section_12_bottom_text_2" = "Ետնապատկերներով էջերի օրինակներ"; +"tour_section_12_bottom_text_3" = "Այս հարմարանքով կարող եք ավելի շատ անհատականացնել Ձեր էջը"; +"tour_section_12_bottom_text_4" = "Հին տեսք"; +"tour_section_12_bottom_text_5" = "Միկրոբլոգ"; + + +"tour_section_13_title_1" = "Պրոմոկոդեր"; +"tour_section_13_text_1" = "OpenVK–ն ունի պրոմոկոդերի համակարգ, որը ավելացնում է որոշակի արժույթ (գնահատման տոկոսներ, ձայներ և այլն)"; +"tour_section_13_text_2" = "Որոշակի կուպոններ ստեղծվում են տոների ժամանակ։ Դրանք հայթհայթելու համար հետևե՛ք <a href='https://t.me/openvk'>OpenVK–ի Telegram–յան ալիքին</a>"; +"tour_section_13_text_3" = "Պրոմոկոդն ակտիվացնելուց հետո սահմանված արժույթը անմիջապես կփոխանցվի Ձեզ"; +"tour_section_13_text_4" = "<b>Հիշե՛ք.</b> Բոլոր պրոմոկոդերի ակտիվացման ժամկետը խիստ սահմանափակ է"; +"tour_section_13_bottom_text_1" = "Պրոմոկոդերն ունեն 24 տառ ու թիվ"; +"tour_section_13_bottom_text_2" = "Հաջողված ակտիվացիա (խոսքի ՝ մրցանակաբաշխում ենք 100 ձայնով)"; +"tour_section_13_bottom_text_3" = "<b>Ուշադի՛ր.</b> Պրոմոկոդի ակտիվացումից հետո կրկին չեք կարող այն օգտագործել"; + +"tour_section_14_title_1" = "Հեռախոսի տարբերակ"; +"tour_section_14_text_1" = "Այս պահին կայքի հեռախոսի վերսիան դեռևս չկա, սակայն գոյություն ունի հավելվածը Android-ի համար"; +"tour_section_14_text_2" = "OpenVK Legacy–ն OpenVK-ի ռետրո հավելվածն է, որը իմիտացնում է ՎԿոնտակտե–ի 2013թ. դիզայնը"; +"tour_section_14_text_3" = "Մինիմալ սպասարկվող տարբերակը Android 2.1 Eclair–ն է, որը անգամ վաղ 2010–ականների սարքերի վրա է աշխատում"; + +"tour_section_14_title_2" = "Որտեղի՞ց ես կարող եմ ներբեռնել այն"; +"tour_section_14_text_2_1" = "Ռելիզային տարբերակները տեղադրվում են F-Droid–ի ռեպոզիտորիայում"; +"tour_section_14_text_2_2" = "Եթե Դուք բետա թեստավորող եք, հավելվածի նոր տարբերակները հրապարակվում են առանձին թարմացումների ալիքում"; +"tour_section_14_text_2_3" = "<b>Հաշվի՛ առեք.</b> Հավելվածը կարող է ունենալ բագեր և խնդիրներ, որոնց մասին խնդրվում է հայտնել <a href='/app'>հավելվածի պաշտոնական խմբում</a>"; + +"tour_section_14_bottom_text_1" = "Էկրանի նկարներ"; +"tour_section_14_bottom_text_2" = "Սա ավարտում է կայքի ճամփորդությունը։ Եթե ցանկանում եք փորձարկել հեռախոսի հավելվածը, ստեղծել Ձեր խումբը, հրավիրել ընկերներին կամ նորերին գտնել, կամ էլ ուղղակի հավես ժամանակ անցկացնել, Դուք կարող եք անել դա հենց հիմա փոքրիկ <a href='/reg'>գրանցում անելով</a>"; +"tour_section_14_bottom_text_3" = "Սա ավարտում է կայքի ճամփորդությունը"; + +/* Search */ + +"s_people" = "Մարդիկ"; +"s_groups" = "Ակումբներ"; +"s_events" = "Իրադարձություններ"; +"s_apps" = "Հավելվածներ"; +"s_questions" = "Հարցեր"; +"s_notes" = "Նշումներ"; +"s_themes" = "Տեսքեր"; +"s_posts" = "Գրառումներ"; +"s_comments" = "Մեկնաբանություններ"; +"s_videos" = "Վիդեոներ"; +"s_audios" = "Երաժշտություն"; +"s_by_people" = "մարդկանց համար"; +"s_by_groups" = "խմբերի համար"; +"s_by_posts" = "գրառումների համար"; +"s_by_comments" = "մեկնաբանությունների համար"; +"s_by_videos" = "վիդեոների համար"; +"s_by_apps" = "հավելվածների համար"; +"s_by_audios" = "երգերի համար"; + +"s_order_by" = "Դասավորել ըստ..."; + +"s_order_by_id" = "Ըստ ID-ի"; +"s_order_by_name" = "Ըստ անվան"; +"s_order_by_random" = "Ըստ պատահականության"; +"s_order_by_rating" = "Ըստ վարկանիշի"; +"s_order_invert" = "Շրջել"; + +"s_by_date" = "Ըստ ամսաթվի"; +"s_registered_before" = "Գրանցված մինչև"; +"s_registered_after" = "Գրանցված հետո"; +"s_date_before" = "Առաջ"; +"s_date_after" = "Հետո"; + +"s_main" = "Հիմնական"; + +"s_now_on_site" = "հիմա կայքում"; +"s_with_photo" = "նկարով"; +"s_only_in_names" = "միայն անուններում"; + +"s_any" = "ցանկացած"; +"reset" = "Վերականգնել"; + +"closed_group_post" = "Սա մասնավոր խմբի գրառում է"; +"deleted_target_comment" = "Այս մեկնաբանությունը ջնջված գրառման տակ է եղել"; + +"no_results" = "Արդյունք չկա"; + +/* Mobile */ +"mobile_friends" = "Ընկերներ"; +"mobile_photos" = "Նկարներ"; +"mobile_videos" = "Վիդեոներ"; +"mobile_messages" = "Նամակներ"; +"mobile_notes" = "Գրառումներ"; +"mobile_groups" = "Խմբեր"; +"mobile_search" = "Որոնում"; +"mobile_settings" = "Կարգավորումներ"; +"mobile_desktop_version" = "Համակարգչի տարբերակ"; +"mobile_log_out" = "Դուրս գալ"; +"mobile_menu" = "Մենյու"; +"mobile_like" = "Հավանել"; +"mobile_user_info_hide" = "Թաքցնել"; +"mobile_user_info_show_details" = "Ցույց տալ մանրամասն"; From 4c0deec5afc35ab5ed36ee9ee2b79142e89124db Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Mon, 21 Aug 2023 12:45:27 +0300 Subject: [PATCH 07/26] r (#971) --- Web/Presenters/templates/components/video.xml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Web/Presenters/templates/components/video.xml b/Web/Presenters/templates/components/video.xml index 33961c4f..f568e942 100644 --- a/Web/Presenters/templates/components/video.xml +++ b/Web/Presenters/templates/components/video.xml @@ -5,8 +5,10 @@ <td valign="top"> <div class="video-preview"> <a href="/video{$video->getPrettyId()}"> - <img src="{$video->getThumbnailURL()}" - style="max-width: 170px; max-height: 127px; margin: auto;" > + <div class="video-preview"> + <img src="{$video->getThumbnailURL()}" + style="max-width: 170px; max-height: 127px; margin: auto;" > + </div> </a> </div> </td> @@ -33,5 +35,5 @@ </td> </tr> </tbody> - </table +</table> {/block} From 0d66c8e9d677ea29bc46ed3422885725c82a053f Mon Sep 17 00:00:00 2001 From: n1rwana <aydashkin@vk.com> Date: Mon, 21 Aug 2023 12:47:25 +0300 Subject: [PATCH 08/26] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=BE=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B1=D0=BB=D0=BE=D0=BA=D0=B0?= =?UTF-8?q?=20=D1=81=20=D0=B8=D0=BD=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B5=D0=B9=20=D0=BE=20=D0=B4=D0=B5=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=B8=D0=B2=D0=B0=D1=86=D0=B8=D0=B8=20(#966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Web/Presenters/templates/@layout.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml index f20169d6..3d724cf5 100644 --- a/Web/Presenters/templates/@layout.xml +++ b/Web/Presenters/templates/@layout.xml @@ -301,7 +301,7 @@ </div> </div> {ifset $thisUser} - {if !$thisUser->isBanned()} + {if !$thisUser->isBanned() && !$thisUser->isDeleted()} </div> {/if} {/ifset} From e433e46b36251359c9543267521609ce45030c31 Mon Sep 17 00:00:00 2001 From: n1rwana <aydashkin@vk.com> Date: Mon, 21 Aug 2023 12:47:51 +0300 Subject: [PATCH 09/26] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D1=80=D0=B5=D0=B4=D0=B8=D1=80=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20=D1=83=D0=B4=D0=B0?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B0=D0=B2=D0=B0=D1=82=D0=B0?= =?UTF-8?q?=D1=80=D0=BA=D0=B8=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B2=D0=B0=20(#967)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Web/Presenters/PhotosPresenter.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Web/Presenters/PhotosPresenter.php b/Web/Presenters/PhotosPresenter.php index 345b2c60..9d64ba20 100644 --- a/Web/Presenters/PhotosPresenter.php +++ b/Web/Presenters/PhotosPresenter.php @@ -1,6 +1,6 @@ <?php declare(strict_types=1); namespace openvk\Web\Presenters; -use openvk\Web\Models\Entities\{Club, Photo, Album}; +use openvk\Web\Models\Entities\{Club, Photo, Album, User}; use openvk\Web\Models\Repositories\{Photos, Albums, Users, Clubs}; use Nette\InvalidStateException as ISE; @@ -292,11 +292,13 @@ final class PhotosPresenter extends OpenVKPresenter if(!$photo) $this->notFound(); if(is_null($this->user) || $this->user->id != $ownerId) $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); - + + $redirect = $photo->getAlbum()->getOwner() instanceof User ? "/id0" : "/club" . $ownerId; + $photo->isolate(); $photo->delete(); $this->flash("succ", "Фотография удалена", "Эта фотография была успешно удалена."); - $this->redirect("/id0"); + $this->redirect($redirect); } } From 245f8690c681f6327b137ce4423d043ebe8cbe29 Mon Sep 17 00:00:00 2001 From: n1rwana <aydashkin@vk.com> Date: Sat, 26 Aug 2023 13:14:25 +0300 Subject: [PATCH 10/26] =?UTF-8?q?[noSpam]=20=D0=9F=D0=B0=D1=80=D0=B0=D0=BC?= =?UTF-8?q?=D0=B5=D1=82=D1=80=D1=8B=20=D0=B1=D0=BB=D0=BE=D0=BA=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=BA=D0=B8=20(#960)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Параметры блокировки * Защита от SQLi и доработка поиска * Фикс блокировки, если дата разблокировки не указана --- Web/Presenters/AdminPresenter.php | 2 +- Web/Presenters/NoSpamPresenter.php | 299 +++++++++++----------- Web/Presenters/templates/NoSpam/Index.xml | 39 ++- 3 files changed, 191 insertions(+), 149 deletions(-) diff --git a/Web/Presenters/AdminPresenter.php b/Web/Presenters/AdminPresenter.php index f5c40bcc..37fb50e8 100644 --- a/Web/Presenters/AdminPresenter.php +++ b/Web/Presenters/AdminPresenter.php @@ -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/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/templates/NoSpam/Index.xml b/Web/Presenters/templates/NoSpam/Index.xml index 746f86d5..89305c5b 100644 --- a/Web/Presenters/templates/NoSpam/Index.xml +++ b/Web/Presenters/templates/NoSpam/Index.xml @@ -106,13 +106,31 @@ <span class="nobold">Параметры блокировки:</span> </td> <td> - <select name="ban_type" id="noSpam-ban-type"> + <select name="ban_type" id="noSpam-ban-type" style="width: 140px;"> <option value="1">Только откат</option> <option value="2">Только блокировка</option> <option value="3">Откат и блокировка</option> </select> </td> </tr> + <tr class="banSettings" style="width: 129px; border-top: 1px solid #ECECEC; display: none;"> + <td> + <span class="nobold">Причина:</span> + </td> + <td> + <input type="text" name="ban-reason" id="ban-reason" style="width: 140px;" /> + </td> + </tr> + <tr class="banSettings" style="width: 129px; border-top: 1px solid #ECECEC; display: none;"> + <td> + <span class="nobold">До:</span> + </td> + <td> + <input type="datetime-local" name="unban-time" id="unban-time" style="width: 140px;" /> + <br /> + <input type="checkbox" name="is_forever" id="is-forever" /> навсегда + </td> + </tr> </tbody> </table> <div style="border-top: 1px solid #ECECEC; margin: 8px 0;"/> @@ -158,7 +176,6 @@ $("#noSpam-results-loader").show(); $("#noSpam-loader").show(); - let models = []; $(".model").each(function (i) { let name = $(this).val(); @@ -178,6 +195,10 @@ let ts = $("#ts").val() ? Math.floor(new Date($("#ts").val()).getTime() / 1000) : null; let te = $("#te").val() ? Math.floor(new Date($("#te").val()).getTime() / 1000) : null; let user = $("#user").val(); + let ban_reason = $("#ban-reason").val(); + let unban_time = $("#unban-time").val() ? Math.floor(new Date($("#unban-time").val()).getTime() / 1000) : null; + let is_forever = $("#is-forever").prop('checked'); + console.log(ban_reason, unban_time, is_forever); await $.ajax({ type: "POST", @@ -193,6 +214,9 @@ ts: ts, te: te, user: user, + ban_reason: ban_reason, + unban_time: unban_time, + is_forever: is_forever, hash: {=$csrfToken} }, success: (response) => { @@ -277,6 +301,17 @@ selectChange(e.target.value); }) + $("#noSpam-ban-type").change(async (e) => { + if (e.target.value > 1) { + $(".banSettings").show(); + } else { + $("#ban-reason").val(null); + $("#unban-time").val(null); + $("#is-forever").prop('checked', false); + $(".banSettings").hide(); + } + }); + $("#add-model").on("click", () => { console.log($(".model").length); $("#models-list").append(` From 14d5caaf9f5e0a4517bda5ba71165890b087c8a3 Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Thu, 14 Sep 2023 20:36:29 +0300 Subject: [PATCH 11/26] Photos: AJAX support (#980) * aj * Drag'n'drop * add good view --- Web/Presenters/PhotosPresenter.php | 94 ++++++--- Web/Presenters/templates/Photos/EditAlbum.xml | 9 + .../templates/Photos/UploadPhoto.xml | 77 +++++--- Web/static/css/main.css | 98 +++++++++- Web/static/js/al_photos.js | 179 ++++++++++++++++++ locales/en.strings | 21 ++ locales/ru.strings | 21 ++ 7 files changed, 442 insertions(+), 57 deletions(-) create mode 100644 Web/static/js/al_photos.js diff --git a/Web/Presenters/PhotosPresenter.php b/Web/Presenters/PhotosPresenter.php index 9d64ba20..11026d00 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, ]; @@ -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 @@ -221,39 +221,74 @@ final class PhotosPresenter extends OpenVKPresenter function renderUploadPhoto(): void { $this->assertUserLoggedIn(); - $this->willExecuteWriteAction(); + $this->willExecuteWriteAction(true); if(is_null($this->queryParam("album"))) - $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в <b>DELETED</b>."); + $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в DELETED.", 500, true); [$owner, $id] = explode("_", $this->queryParam("album")); $album = $this->albums->get((int) $id); if(!$album) - $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в <b>DELETED</b>."); + $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в DELETED.", 500, true); if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity)) - $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); + $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса.", 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", "Неизвестная ошибка", "Не удалось сохранить фотографию в <b>$name</b>."); - } - - $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", "Нету фотографии", "Выберите файл.", 500, true); + + $photos = []; + 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; } @@ -285,7 +320,7 @@ 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); @@ -298,6 +333,9 @@ final class PhotosPresenter extends OpenVKPresenter $photo->isolate(); $photo->delete(); + if($_SERVER["REQUEST_METHOD"] === "POST") + $this->returnJson(["success" => true]); + $this->flash("succ", "Фотография удалена", "Эта фотография была успешно удалена."); $this->redirect($redirect); } diff --git a/Web/Presenters/templates/Photos/EditAlbum.xml b/Web/Presenters/templates/Photos/EditAlbum.xml index 271d1034..a10b5c7f 100644 --- a/Web/Presenters/templates/Photos/EditAlbum.xml +++ b/Web/Presenters/templates/Photos/EditAlbum.xml @@ -14,6 +14,15 @@ {/block} {block content} + <div class="tabs"> + <div id="activetabs" class="tab"> + <a id="act_tab_a" href="/album{$album->getPrettyId()}/edit">{_edit_album}</a> + </div> + <div class="tab"> + <a href="/photos/upload?album={$album->getPrettyId()}">{_add_photos}</a> + </div> + </div> + <form method="post" enctype="multipart/form-data"> <table cellspacing="6"> <tbody> diff --git a/Web/Presenters/templates/Photos/UploadPhoto.xml b/Web/Presenters/templates/Photos/UploadPhoto.xml index 9876e5b9..6ea987cb 100644 --- a/Web/Presenters/templates/Photos/UploadPhoto.xml +++ b/Web/Presenters/templates/Photos/UploadPhoto.xml @@ -12,32 +12,53 @@ {/block} {block content} - <form action="/photos/upload?album={$album->getPrettyId()}" method="post" enctype="multipart/form-data"> - <table cellspacing="6"> - <tbody> - <tr> - <td width="120" valign="top"><span class="nobold">{_description}:</span></td> - <td><textarea style="margin: 0px; height: 50px; width: 159px; resize: none;" name="desc"></textarea></td> - </tr> - <tr> - <td width="120" valign="top"><span class="nobold">{_photo}:</span></td> - <td> - <label class="button" style="">{_browse} - <input type="file" id="blob" name="blob" style="display: none;" onchange="filename.innerHTML=blob.files[0].name" /> - </label> - <div id="filename" style="margin-top: 10px;"></div> - </td> - </tr> - <tr> - <td width="120" valign="top"></td> - <td> - <input type="hidden" name="hash" value="{$csrfToken}" /> - <input type="submit" class="button" name="submit" value="Загрузить" /> - </td> - </tr> - </tbody> - </table> - - <input n:ifset="$_GET['album']" type="hidden" name="album" value="{$_GET['album']}" /> - </form> + <div class="tabs"> + <div class="tab"> + <a href="/album{$album->getPrettyId()}/edit">{_edit_album}</a> + </div> + <div id="activetabs" class="tab"> + <a id="act_tab_a" href="#">{_add_photos}</a> + </div> + </div> + + <input type="file" accept=".jpg,.png,.gif" name="files[]" multiple class="button" id="uploadButton" style="display:none"> + + <div class="container_gray" style="height: 344px;"> + <div class="insertThere"></div> + <div class="whiteBox" style="display: block;"> + <div class="boxContent"> + <h4>{_uploading_photos_from_computer}</h4> + + <div class="limits" style="margin-top:17px"> + <b style="color:#45688E">{_admin_limits}</b> + <ul class="blueList" style="margin-left: -25px;margin-top: 1px;"> + <li>{_supported_formats}</li> + <li>{_max_load_photos}</li> + </ul> + + <div style="text-align: center;padding-top: 4px;" class="insertAgain"> + <input type="button" class="button" id="fakeButton" onclick="uploadButton.click()" value="{_upload_picts}"> + </div> + + <div class="tipping" style="margin-top: 19px;"> + <span style="line-height: 15px"><b>{_tip}</b>: {_tip_ctrl}</span> + </div> + </div> + </div> + </div> + + <div class="insertPhotos" id="photos" style="margin-top: 9px;padding-bottom: 12px;"></div> + + <input type="button" class="button" style="display:none;margin-left: auto;margin-right: auto;" id="endUploading" value="{_end_uploading}"> + </div> + + <input n:ifset="$_GET['album']" type="hidden" id="album" value="{$_GET['album']}" /> + + <script> + uploadButton.value = '' + </script> +{/block} + +{block bodyScripts} + {script "js/al_photos.js"} {/block} diff --git a/Web/static/css/main.css b/Web/static/css/main.css index 55484f13..f05d0a2b 100644 --- a/Web/static/css/main.css +++ b/Web/static/css/main.css @@ -2700,4 +2700,100 @@ body.article .floating_sidebar, body.article .page_content { position: absolute; right: 22px; font-size: 12px; -} \ No newline at end of file +} + +.uploadedImage img { + max-height: 76px; + object-fit: cover; +} + +.lagged { + filter: opacity(0.5); + cursor: progress; + user-select: 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/js/al_photos.js b/Web/static/js/al_photos.js new file mode 100644 index 00000000..59965c09 --- /dev/null +++ b/Web/static/js/al_photos.js @@ -0,0 +1,179 @@ +$(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; + } + + 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", `<img id="loader" src="/assets/packages/static/openvk/img/loading_mini.gif">`) + } + + 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", ` + <div id="photo" class="insertedPhoto" data-id="${photo.id}"> + <div class="uploadedImageDescription" style="float: left;"> + <span style="color: #464646;position: absolute;">${tr("description")}:</span> + <textarea style="margin-left: 62px; resize: none;" maxlength="255"></textarea> + </div> + <div class="uploadedImage"> + <a href="${photo.link}" target="_blank"><img width="125" src="${photo.url}"></a> + <a class="profile_link" style="width: 125px;" id="deletePhoto" data-id="${photo.vid}" data-owner="${photo.owner}">${tr("delete")}</a> + <!--<div class="smallFrame" style="margin-top: 6px;"> + <div class="smallBtn">${tr("album_poster")}</div> + </div>--> + </div> + </div> + `) + } + + 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; +}) + +$(document).on("dragover", (e) => { + e.preventDefault() + document.querySelector("#fakeButton").classList.add("dragged") + document.querySelector("#fakeButton").value = tr("drag_files_here") +}) + +$(document).on("dragleave", (e) => { + e.preventDefault() + document.querySelector("#fakeButton").classList.remove("dragged") + document.querySelector("#fakeButton").value = tr("upload_picts") +}) + +$("#fakeButton").on("drop", (e) => { + e.originalEvent.dataTransfer.dropEffect = 'move'; + e.preventDefault() + + $(document).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") +}) diff --git a/locales/en.strings b/locales/en.strings index 487d2156..780da8b3 100644 --- a/locales/en.strings +++ b/locales/en.strings @@ -381,6 +381,25 @@ "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"; + /* Notes */ "notes" = "Notes"; @@ -1066,6 +1085,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"; @@ -1117,6 +1137,7 @@ "media_file_corrupted_or_too_large" = "The media content file is corrupted or too large."; "post_is_empty_or_too_big" = "The post is empty or too big."; "post_is_too_big" = "The post is too big."; +"description_too_long" = "Description is too long."; /* Admin actions */ diff --git a/locales/ru.strings b/locales/ru.strings index dc101e2b..2364175a 100644 --- a/locales/ru.strings +++ b/locales/ru.strings @@ -364,6 +364,25 @@ "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" = "Обложка альбома"; + /* Notes */ "notes" = "Заметки"; @@ -982,6 +1001,7 @@ "error_repost_fail" = "Не удалось поделиться записью"; "error_data_too_big" = "Аттрибут '$1' не может быть длиннее $2 $3"; "forbidden" = "Ошибка доступа"; +"unknown_error" = "Неизвестная ошибка"; "forbidden_comment" = "Настройки приватности этого пользователя не разрешают вам смотреть на его страницу."; "changes_saved" = "Изменения сохранены"; "changes_saved_comment" = "Новые данные появятся на вашей странице"; @@ -1017,6 +1037,7 @@ "media_file_corrupted_or_too_large" = "Файл медиаконтента повреждён или слишком велик."; "post_is_empty_or_too_big" = "Пост пустой или слишком большой."; "post_is_too_big" = "Пост слишком большой."; +"description_too_long" = "Описание слишком длинное."; /* Admin actions */ From 97a176c261e0813abe9a3942ec71294f2d187eb6 Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Thu, 14 Sep 2023 20:54:22 +0300 Subject: [PATCH 12/26] =?UTF-8?q?=D0=A0=D0=B5=D0=B4=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D0=B2=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=BA=D1=80=D1=83=D1=87=D0=B5=20(#979)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add editing posts * Add checkboxes * Add ctrl+enter + fix empty posts * Fix funny bug --- Web/Models/Entities/Comment.php | 8 ++ Web/Models/Entities/Post.php | 11 ++ Web/Presenters/WallPresenter.php | 61 +++++++++- Web/Presenters/templates/Wall/Post.xml | 8 ++ .../templates/components/comment.xml | 12 +- .../components/post/microblogpost.xml | 23 ++-- .../templates/components/post/oldpost.xml | 23 +++- Web/routes.yml | 2 + Web/static/css/main.css | 15 +++ Web/static/css/microblog.css | 14 +++ Web/static/img/edit.png | Bin 0 -> 571 bytes Web/static/js/al_wall.js | 109 +++++++++++++++++- locales/en.strings | 3 + locales/ru.strings | 2 + 14 files changed, 270 insertions(+), 21 deletions(-) create mode 100644 Web/static/img/edit.png diff --git a/Web/Models/Entities/Comment.php b/Web/Models/Entities/Comment.php index d813d6be..d64a2763 100644 --- a/Web/Models/Entities/Comment.php +++ b/Web/Models/Entities/Comment.php @@ -90,4 +90,12 @@ class Comment extends Post { return "/wall" . $this->getTarget()->getPrettyId() . "#_comment" . $this->getId(); } + + function canBeEditedBy(?User $user = NULL): bool + { + if(!$user) + return false; + + return $user->getId() == $this->getOwner(false)->getId(); + } } diff --git a/Web/Models/Entities/Post.php b/Web/Models/Entities/Post.php index 42941901..2d323a8e 100644 --- a/Web/Models/Entities/Post.php +++ b/Web/Models/Entities/Post.php @@ -245,6 +245,17 @@ class Post extends Postable $this->unwire(); $this->save(); } + + function canBeEditedBy(?User $user = NULL): bool + { + if(!$user) + return false; + + if($this->isDeactivationMessage() || $this->isUpdateAvatarMessage()) + return false; + + return $user->getId() == $this->getOwner(false)->getId(); + } use Traits\TRichText; } diff --git a/Web/Presenters/WallPresenter.php b/Web/Presenters/WallPresenter.php index 3e115ec7..09392bc3 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, Comments}; use Chandler\Database\DatabaseConnection; use Nette\InvalidStateException as ISE; use Bhaktaraz\RSSGenerator\Item; @@ -498,4 +498,63 @@ 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", + "author" => [ + "name" => $post->getOwner()->getCanonicalName(), + "avatar" => $post->getOwner()->getAvatarUrl() + ]]); + } } diff --git a/Web/Presenters/templates/Wall/Post.xml b/Web/Presenters/templates/Wall/Post.xml index 575c7bba..8ac11bb7 100644 --- a/Web/Presenters/templates/Wall/Post.xml +++ b/Web/Presenters/templates/Wall/Post.xml @@ -34,6 +34,14 @@ {/if} <a n:if="$canDelete ?? false" class="profile_link" style="display:block;width:96%;" href="/wall{$post->getPrettyId()}/delete">{_delete}</a> + <a + n:if="$thisUser->getChandlerUser()->can('access')->model('admin')->whichBelongsTo(NULL) AND $post->getEditTime()" + style="display:block;width:96%;" + class="profile_link" + href="/admin/logs?type=1&obj_type=Post&obj_id={$post->getId()}" + > + {_changes_history} + </a> <a n:if="$canReport ?? false" class="profile_link" style="display:block;width:96%;" href="javascript:reportPost()">{_report}</a> </div> <script n:if="$canReport ?? false"> diff --git a/Web/Presenters/templates/components/comment.xml b/Web/Presenters/templates/components/comment.xml index 714893d1..fb8fb244 100644 --- a/Web/Presenters/templates/components/comment.xml +++ b/Web/Presenters/templates/components/comment.xml @@ -20,7 +20,7 @@ </div> <div class="post-content" id="{$comment->getId()}"> <div class="text" id="text{$comment->getId()}"> - {$comment->getText()|noescape} + <span class="really_text">{$comment->getText()|noescape}</span> <div n:ifcontent class="attachments_b"> <div class="attachment" n:foreach="$comment->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}"> @@ -29,17 +29,17 @@ </div> </div> <div n:if="isset($thisUser) &&! ($compact ?? false)" class="post-menu"> - <a - href="{=$linkWithPost && get_class($comment->getTarget()) == 'openvk\Web\Models\Entities\Post' ? '/wall' . $comment->getTarget()->getPrettyId() : ''}#_comment{$comment->getId()}" - class="date" - > - {$comment->getPublicationTime()} + <a href="#_comment{$comment->getId()}" class="date">{$comment->getPublicationTime()} + <span n:if="$comment->getEditTime()" class="edited editedMark">({_edited_short})</span> </a> {if !$timeOnly} | {if $comment->canBeDeletedBy($thisUser)} <a href="/comment{$comment->getId()}/delete">{_delete}</a> | {/if} + {if $comment->canBeEditedBy($thisUser)} + <a id="editPost" data-id="{$comment->getId()}">{_edit}</a> | + {/if} <a class="comment-reply">{_reply}</a> {if $thisUser->getId() != $comment->getOwner()->getId()} {var $canReport = true} diff --git a/Web/Presenters/templates/components/post/microblogpost.xml b/Web/Presenters/templates/components/post/microblogpost.xml index 98a41d72..c8cd2a12 100644 --- a/Web/Presenters/templates/components/post/microblogpost.xml +++ b/Web/Presenters/templates/components/post/microblogpost.xml @@ -18,13 +18,13 @@ <tr> <td width="54" valign="top"> <a href="{$author->getURL()}"> - <img src="{$author->getAvatarURL('miniscule')}" width="{if $compact}25{else}50{/if}" {if $compact}class="cCompactAvatars"{/if} /> + <img src="{$author->getAvatarURL('miniscule')}" width="{if $compact}25{else}50{/if}" class="post-avatar {if $compact}cCompactAvatars{/if}" /> <span n:if="!$post->isPostedOnBehalfOfGroup() && !$compact && $author->isOnline()" class="post-online">{_online}</span> </a> </td> <td width="100%" valign="top"> <div class="post-author"> - <a href="{$author->getURL()}"><b>{$author->getCanonicalName()}</b></a> + <a href="{$author->getURL()}"><b class="post-author-name">{$author->getCanonicalName()}</b></a> <img n:if="$author->isVerified()" class="name-checkmark" src="/assets/packages/static/openvk/img/checkmark.png"> {$post->isDeactivationMessage() ? ($author->isFemale() ? tr($deac . "_f") : tr($deac . "_m"))} {$post->isUpdateAvatarMessage() && !$post->isPostedOnBehalfOfGroup() ? ($author->isFemale() ? tr("upd_f") : tr("upd_m"))} @@ -62,11 +62,18 @@ <a class="pin" href="/wall{$post->getPrettyId()}/pin?act=pin&hash={rawurlencode($csrfToken)}"></a> {/if} {/if} + + {if $post->canBeEditedBy($thisUser) && !($forceNoEditLink ?? false) && $compact == false} + <a class="edit" id="editPost" + data-id="{$post->getId()}" + data-nsfw="{(int)$post->isExplicit()}" + {if $post->getTargetWall() < 0 && $post->getWallOwner()->canBeModifiedBy($thisUser)}data-fromgroup="{(int)$post->isPostedOnBehalfOfGroup()}"{/if}></a> + {/if} </div> <div class="post-content" id="{$post->getPrettyId()}"> - <div class="text" id="text{$post->getPrettyId()}"> - {$post->getText()|noescape} - + <div class="text"> + <span class="really_text">{$post->getText()|noescape}</span> + <div n:ifcontent class="attachments_b"> <div class="attachment" n:foreach="$post->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}"> {include "../attachment.xml", attachment => $attachment} @@ -88,13 +95,15 @@ </div> </div> <div class="post-menu" n:if="$compact == false"> - <a href="/wall{$post->getPrettyId()}" class="date">{$post->getPublicationTime()}</a> + <a href="/wall{$post->getPrettyId()}" class="date">{$post->getPublicationTime()} + <span n:if="$post->getEditTime()" class="edited editedMark">({_edited_short})</span> + </a> <a n:if="!empty($platform)" class="client_app" data-app-tag="{$platform}" data-app-name="{$platformDetails['name']}" data-app-url="{$platformDetails['url']}" data-app-img="{$platformDetails['img']}"> <img src="/assets/packages/static/openvk/img/app_icons_mini/{$post->getPlatform(this)}.svg"> </a> {if isset($thisUser)} - + <a n:if="!($forceNoCommentsLink ?? false) && $commentsCount == 0" href="javascript:expand_comment_textarea({$commentTextAreaId})">{_comment}</a> <div class="like_wrap"> diff --git a/Web/Presenters/templates/components/post/oldpost.xml b/Web/Presenters/templates/components/post/oldpost.xml index c893e289..ad7896f2 100644 --- a/Web/Presenters/templates/components/post/oldpost.xml +++ b/Web/Presenters/templates/components/post/oldpost.xml @@ -7,18 +7,20 @@ {var $deac = "post_deact_silent"} {/if} + + <table border="0" style="font-size: 11px;" n:class="post, $post->isExplicit() ? post-nsfw"> <tbody> <tr> <td width="54" valign="top"> <a href="{$author->getURL()}"> - <img src="{$author->getAvatarURL('miniscule')}" width="50" /> + <img src="{$author->getAvatarURL('miniscule')}" class="post-avatar" width="50" /> <span n:if="!$post->isPostedOnBehalfOfGroup() && !($compact ?? false) && $author->isOnline()" class="post-online">{_online}</span> </a> </td> <td width="100%" valign="top"> <div class="post-author"> - <a href="{$author->getURL()}"><b>{$author->getCanonicalName()}</b></a> + <a href="{$author->getURL()}"><b class="post-author-name">{$author->getCanonicalName()}</b></a> <img n:if="$author->isVerified()" class="name-checkmark" src="/assets/packages/static/openvk/img/checkmark.png"> {if $post->isDeactivationMessage()} {$author->isFemale() ? tr($deac . "_f") : tr($deac . "_m")} @@ -51,16 +53,18 @@ {/if} <br/> <a href="/wall{$post->getPrettyId()}" class="date"> - {$post->getPublicationTime()}{if $post->isPinned()}, {_pinned}{/if} + {$post->getPublicationTime()} <span n:if="$post->getEditTime()" class="editedMark">({_edited_short})</span>{if $post->isPinned()}, {_pinned}{/if} <a n:if="!empty($platform)" class="client_app" data-app-tag="{$platform}" data-app-name="{$platformDetails['name']}" data-app-url="{$platformDetails['url']}" data-app-img="{$platformDetails['img']}"> <img src="/assets/packages/static/openvk/img/app_icons_mini/{$post->getPlatform(this)}.svg"> </a> </a> </div> <div class="post-content" id="{$post->getPrettyId()}"> - <div class="text" id="text{$post->getPrettyId()}"> - {$post->getText()|noescape} - + <div class="text"> + {var $owner = $author->getId()} + + <span class="really_text">{$post->getText()|noescape}</span> + <div n:ifcontent class="attachments_b"> <div class="attachment" n:foreach="$post->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}"> {include "../attachment.xml", attachment => $attachment} @@ -87,6 +91,13 @@ {var $forceNoPinLink = true} {/if} + {if !($forceNoEditLink ?? false) && $post->canBeEditedBy($thisUser)} + <a id="editPost" + data-id="{$post->getId()}" + data-nsfw="{(int)$post->isExplicit()}" + {if $post->getTargetWall() < 0 && $post->getWallOwner()->canBeModifiedBy($thisUser)}data-fromgroup="{(int)$post->isPostedOnBehalfOfGroup()}"{/if}>{_edit}</a> | + {/if} + {if !($forceNoDeleteLink ?? false) && $post->canBeDeletedBy($thisUser)} <a href="/wall{$post->getPrettyId()}/delete">{_delete}</a> | {/if} diff --git a/Web/routes.yml b/Web/routes.yml index effa68eb..bc95f44a 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" diff --git a/Web/static/css/main.css b/Web/static/css/main.css index f05d0a2b..46ad74b0 100644 --- a/Web/static/css/main.css +++ b/Web/static/css/main.css @@ -2702,6 +2702,10 @@ body.article .floating_sidebar, body.article .page_content { font-size: 12px; } +.edited { + color: #9b9b9b; +} + .uploadedImage img { max-height: 76px; object-fit: cover; @@ -2713,6 +2717,16 @@ body.article .floating_sidebar, body.article .page_content { user-select: none; } +.editMenu.loading { + filter: opacity(0.5); + cursor: progress; + user-select: none; +} + +.editMenu.loading * { + pointer-events: none; +} + .lagged * { pointer-events: none; } @@ -2797,3 +2811,4 @@ body.article .floating_sidebar, body.article .page_content { .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 0000000000000000000000000000000000000000..b3474d0f7a5dbe1221e549119fbfaf687899c81c GIT binary patch literal 571 zcmeAS@N?(olHy`uVBq!ia0vp^+#t-s3?%0jwTl2LmUKs7M+SzC^Add=zXSOS$sR$z z3=Hi83=BO&3=DsP>_$5VhRD|pj6(bj3^vag7$nc_I;mvDz`&@O>FgZf>Flf!P?VpR znUl)Epm9DqA;GAiq_8MeC?Vm*S#3?OqVk}Qj-H?d`|}5m9XP|l)_L+MYab({okCe$ zU7Qv(+mEu|MnRYOYpgSmI!Hb6($mmlyj0(CLqm0|#mvUWiwq~u>FH}SbQzyx+~hiG z)4?~q3T1&F6O1R+vYdG4-M(Rc!wpY?Z^8n*IbLuo{O0zkwVHAE2%{PMp=pd8y@RwJ zz8P6O7Y`9jU6wvYLn10FO3Px|6ZNH9Ea76)mpl{Gu6U;UTwL^txazbe%NVXY-C<{I z5@=l@aG4=%%Y~E$Mhr)pTfSNf$n5J$N*74tm=F^c!j@`eBq{t%`OFOEg9je$pVHX) zl;7#hlIe|!jeN|^|3g}4%u-}-Zm?rMyw8&1z%l>7`IDZq00UMfz$e5tyLaP(^Y<RV z{7^n^dv4#R?q!EQeEwcFW9R*6?`}SNy>ib*pu!G5kvt&9QxfDC{2u`rgzld^2NdTl z@Q5r1MxrnXGcwGYBLNh&@pN$vkqCD^<tWymz;on6g6o8x|7(x>)uw$FcNF!V-xzl9 z&fhP|%evS1TkKu5E?ecON{RZ>ro66*%ai2pUuW(YIr-xq&>RL&S3j3^P6<r_()Qi* literal 0 HcmV?d00001 diff --git a/Web/static/js/al_wall.js b/Web/static/js/al_wall.js index bb349c14..9b54adce 100644 --- a/Web/static/js/al_wall.js +++ b/Web/static/js/al_wall.js @@ -262,4 +262,111 @@ async function showArticle(note_id) { u("#articleText").html(`<h1 class="articleView_nameHeading">${note.title}</h1>` + note.html); u("body").removeClass("dimmed"); u("body").addClass("article"); -} \ No newline at end of file +} + +$(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", ` + <div class="editMenu"> + <div id="wall-post-input999"> + <textarea id="new_content">${text.innerHTML.replace(/(<([^>]+)>)/gi, '')}</textarea> + <input type="button" class="button" value="${tr("save")}" id="endEditing"> + <input type="button" class="button" value="${tr("cancel")}" id="cancelEditing"> + </div> + ${e.currentTarget.dataset.nsfw != null ? ` + <div class="postOptions"> + <label><input type="checkbox" id="nswfw" ${e.currentTarget.dataset.nsfw == 1 ? `checked` : ``}>${tr("contains_nsfw")}</label> + </div> + ` : ``} + ${e.currentTarget.dataset.fromgroup != null ? ` + <div class="postOptions"> + <label><input type="checkbox" id="fromgroup" ${e.currentTarget.dataset.fromgroup == 1 ? `checked` : ``}>${tr("post_as_group")}</label> + </div> + ` : ``} + </div> + `) + + 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", ` + <span class="edited editedMark">(${tr("edited_short")})</span> + `) + } + + 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 + } 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" + } +}) diff --git a/locales/en.strings b/locales/en.strings index 780da8b3..f6e325d5 100644 --- a/locales/en.strings +++ b/locales/en.strings @@ -214,6 +214,8 @@ "reply" = "Reply"; +"edited_short" = "edited"; + /* Friends */ "friends" = "Friends"; @@ -1149,6 +1151,7 @@ "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 */ diff --git a/locales/ru.strings b/locales/ru.strings index 2364175a..b66a5404 100644 --- a/locales/ru.strings +++ b/locales/ru.strings @@ -191,6 +191,7 @@ "version_incompatibility" = "Не удалось отобразить это вложение. Возможно, база данных несовместима с текущей версией OpenVK."; "graffiti" = "Граффити"; "reply" = "Ответить"; +"edited_short" = "ред."; /* Friends */ @@ -1049,6 +1050,7 @@ "warn_user_action" = "Предупредить пользователя"; "ban_in_support_user_action" = "Заблокировать в поддержке"; "unban_in_support_user_action" = "Разблокировать в поддержке"; +"changes_history" = "История редактирования"; /* Admin panel */ From 06f324f98cf985631e3aeb833eb9087e399fbf28 Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Sat, 16 Sep 2023 19:14:23 +0300 Subject: [PATCH 13/26] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81=D1=8B=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20#980=20=D0=B8=20#979=20(#982)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Что я должен здесь сказать? * playerock * Copypaste --- Web/Models/Entities/Post.php | 3 +++ Web/Models/Entities/Postable.php | 4 +-- Web/Presenters/WallPresenter.php | 1 + .../templates/Photos/UploadPhoto.xml | 10 ++++--- .../templates/components/comment.xml | 6 ++--- .../components/post/microblogpost.xml | 2 +- .../templates/components/post/oldpost.xml | 2 +- Web/static/js/al_photos.js | 27 ++++++++++++++++--- Web/static/js/al_wall.js | 3 ++- 9 files changed, 43 insertions(+), 15 deletions(-) diff --git a/Web/Models/Entities/Post.php b/Web/Models/Entities/Post.php index 2d323a8e..8c38c567 100644 --- a/Web/Models/Entities/Post.php +++ b/Web/Models/Entities/Post.php @@ -254,6 +254,9 @@ class Post extends Postable if($this->isDeactivationMessage() || $this->isUpdateAvatarMessage()) return false; + if($this->getTargetWall() > 0) + return $this->getPublicationTime()->timestamp() + WEEK > time(); + return $user->getId() == $this->getOwner(false)->getId(); } diff --git a/Web/Models/Entities/Postable.php b/Web/Models/Entities/Postable.php index 23981cc1..ffbf480c 100644 --- a/Web/Models/Entities/Postable.php +++ b/Web/Models/Entities/Postable.php @@ -167,9 +167,9 @@ abstract class Postable extends Attachable $this->stateChanges("created", time()); $this->stateChanges("virtual_id", $pCount + 1); - } else { + } /*else { $this->stateChanges("edited", time()); - } + }*/ parent::save(); } diff --git a/Web/Presenters/WallPresenter.php b/Web/Presenters/WallPresenter.php index 09392bc3..96b61b48 100644 --- a/Web/Presenters/WallPresenter.php +++ b/Web/Presenters/WallPresenter.php @@ -552,6 +552,7 @@ final class WallPresenter extends OpenVKPresenter "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/Photos/UploadPhoto.xml b/Web/Presenters/templates/Photos/UploadPhoto.xml index 6ea987cb..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} - <a href="{$thisUser->getURL()}">{$thisUser->getCanonicalName()}</a> + <a href="{$album->getOwner()->getURL()}">{$album->getOwner()->getCanonicalName()}</a> » - <a href="/albums{$thisUser->getId()}">{_albums}</a> + {if $album->getOwner() instanceof openvk\Web\Models\Entities\Club} + <a href="/albums{$album->getOwner()->getId() * -1}">{_albums}</a> + {else} + <a href="/albums{$album->getOwner()->getId()}">{_albums}</a> + {/if} » <a href="/album{$album->getPrettyId()}">{$album->getName()}</a> » @@ -23,7 +27,7 @@ <input type="file" accept=".jpg,.png,.gif" name="files[]" multiple class="button" id="uploadButton" style="display:none"> - <div class="container_gray" style="height: 344px;"> + <div class="container_gray" style="min-height: 344px;"> <div class="insertThere"></div> <div class="whiteBox" style="display: block;"> <div class="boxContent"> diff --git a/Web/Presenters/templates/components/comment.xml b/Web/Presenters/templates/components/comment.xml index fb8fb244..69238417 100644 --- a/Web/Presenters/templates/components/comment.xml +++ b/Web/Presenters/templates/components/comment.xml @@ -8,19 +8,19 @@ <tr> <td width="30" valign="top"> <a href="{$author->getURL()}"> - <img src="{$author->getAvatarURL('miniscule')}" width="30" class="cCompactAvatars" /> + <img src="{$author->getAvatarURL('miniscule')}" width="30" class="cCompactAvatars post-avatar" /> </a> </td> <td width="100%" valign="top"> <div class="post-author"> - <a href="{$author->getURL()}"><b> + <a href="{$author->getURL()}"><b class="post-author-name"> {$author->getCanonicalName()} </b></a> <img n:if="$author->isVerified()" class="name-checkmark" src="/assets/packages/static/openvk/img/checkmark.png"><br/> </div> <div class="post-content" id="{$comment->getId()}"> <div class="text" id="text{$comment->getId()}"> - <span 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"> <div class="attachment" n:foreach="$comment->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}"> diff --git a/Web/Presenters/templates/components/post/microblogpost.xml b/Web/Presenters/templates/components/post/microblogpost.xml index c8cd2a12..6b0444c7 100644 --- a/Web/Presenters/templates/components/post/microblogpost.xml +++ b/Web/Presenters/templates/components/post/microblogpost.xml @@ -72,7 +72,7 @@ </div> <div class="post-content" id="{$post->getPrettyId()}"> <div class="text"> - <span 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"> <div class="attachment" n:foreach="$post->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}"> diff --git a/Web/Presenters/templates/components/post/oldpost.xml b/Web/Presenters/templates/components/post/oldpost.xml index ad7896f2..6e0b4c53 100644 --- a/Web/Presenters/templates/components/post/oldpost.xml +++ b/Web/Presenters/templates/components/post/oldpost.xml @@ -63,7 +63,7 @@ <div class="text"> {var $owner = $author->getId()} - <span 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"> <div class="attachment" n:foreach="$post->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}"> diff --git a/Web/static/js/al_photos.js b/Web/static/js/al_photos.js index 59965c09..b4632d60 100644 --- a/Web/static/js/al_photos.js +++ b/Web/static/js/al_photos.js @@ -6,6 +6,18 @@ $(document).on("change", "#uploadButton", (e) => { 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")); @@ -142,23 +154,23 @@ $(document).on("dragover drop", (e) => { return false; }) -$(document).on("dragover", (e) => { +$(".container_gray").on("dragover", (e) => { e.preventDefault() document.querySelector("#fakeButton").classList.add("dragged") document.querySelector("#fakeButton").value = tr("drag_files_here") }) -$(document).on("dragleave", (e) => { +$(".container_gray").on("dragleave", (e) => { e.preventDefault() document.querySelector("#fakeButton").classList.remove("dragged") document.querySelector("#fakeButton").value = tr("upload_picts") }) -$("#fakeButton").on("drop", (e) => { +$(".container_gray").on("drop", (e) => { e.originalEvent.dataTransfer.dropEffect = 'move'; e.preventDefault() - $(document).trigger("dragleave") + $(".container_gray").trigger("dragleave") let files = e.originalEvent.dataTransfer.files @@ -177,3 +189,10 @@ $("#fakeButton").on("drop", (e) => { 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 9b54adce..4c8bb933 100644 --- a/Web/static/js/al_wall.js +++ b/Web/static/js/al_wall.js @@ -273,7 +273,7 @@ $(document).on("click", "#editPost", (e) => { content.insertAdjacentHTML("afterbegin", ` <div class="editMenu"> <div id="wall-post-input999"> - <textarea id="new_content">${text.innerHTML.replace(/(<([^>]+)>)/gi, '')}</textarea> + <textarea id="new_content">${text.dataset.text}</textarea> <input type="button" class="button" value="${tr("save")}" id="endEditing"> <input type="button" class="button" value="${tr("cancel")}" id="cancelEditing"> </div> @@ -343,6 +343,7 @@ $(document).on("click", "#editPost", (e) => { 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() From 293993653441ef367880a590035a02df9a84854c Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Sat, 16 Sep 2023 19:51:36 +0300 Subject: [PATCH 14/26] Update Post.php (#983) --- Web/Models/Entities/Post.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Web/Models/Entities/Post.php b/Web/Models/Entities/Post.php index 8c38c567..6d0fe8cf 100644 --- a/Web/Models/Entities/Post.php +++ b/Web/Models/Entities/Post.php @@ -255,7 +255,7 @@ class Post extends Postable return false; if($this->getTargetWall() > 0) - return $this->getPublicationTime()->timestamp() + WEEK > time(); + return $this->getPublicationTime()->timestamp() + WEEK > time() && $user->getId() == $this->getOwner(false)->getId(); return $user->getId() == $this->getOwner(false)->getId(); } From 0ef413a5b9554b0742bb16cf8ece92dbfb39f511 Mon Sep 17 00:00:00 2001 From: n1rwana <aydashkin@vk.com> Date: Sun, 17 Sep 2023 00:56:36 +0300 Subject: [PATCH 15/26] Ability to hide "My applications" from the menu (#937) --- Web/Models/Entities/User.php | 2 ++ Web/Presenters/UserPresenter.php | 1 + Web/Presenters/templates/@layout.xml | 2 +- Web/Presenters/templates/User/Settings.xml | 10 ++++++++++ 4 files changed, 14 insertions(+), 1 deletion(-) 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/Presenters/UserPresenter.php b/Web/Presenters/UserPresenter.php index 9cfa3654..46aa7e92 100644 --- a/Web/Presenters/UserPresenter.php +++ b/Web/Presenters/UserPresenter.php @@ -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/templates/@layout.xml b/Web/Presenters/templates/@layout.xml index 3d724cf5..f37532a7 100644 --- a/Web/Presenters/templates/@layout.xml +++ b/Web/Presenters/templates/@layout.xml @@ -196,7 +196,7 @@ (<b>{$thisUser->getNotificationsCount()}</b>) {/if} </a> - <a href="/apps?act=installed" class="link">{_my_apps}</a> + <a n:if="$thisUser->getLeftMenuItemStatus('apps')" href="/apps?act=installed" class="link">{_my_apps}</a> <a href="/settings" class="link">{_my_settings}</a> {var $canAccessAdminPanel = $thisUser->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL)} diff --git a/Web/Presenters/templates/User/Settings.xml b/Web/Presenters/templates/User/Settings.xml index e61f900d..9d9a2b25 100644 --- a/Web/Presenters/templates/User/Settings.xml +++ b/Web/Presenters/templates/User/Settings.xml @@ -650,6 +650,16 @@ <td> <span class="nobold">{_my_feed}</span> </td> + </tr><tr> + <td width="120" valign="top" align="right" align="right"> + <input + n:attr="checked => $user->getLeftMenuItemStatus('apps')" + type="checkbox" + name="menu_aplikoj" /> + </td> + <td> + <span class="nobold">{_my_apps}</span> + </td> </tr><tr n:if="sizeof(OPENVK_ROOT_CONF['openvk']['preferences']['menu']['links']) > 0"> <td width="120" valign="top" align="right" align="right"> <input From 468eba80bdd4d8931280a229f7db3e4a162c058a Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Sun, 17 Sep 2023 16:22:59 +0300 Subject: [PATCH 16/26] Locales: Make more strings translatable (#961) --- Web/Models/Entities/Club.php | 6 +- Web/Presenters/AdminPresenter.php | 8 +- Web/Presenters/CommentPresenter.php | 18 +- Web/Presenters/GiftsPresenter.php | 10 +- Web/Presenters/GroupPresenter.php | 30 +-- Web/Presenters/NotesPresenter.php | 6 +- Web/Presenters/PhotosPresenter.php | 28 +-- Web/Presenters/ReportPresenter.php | 12 +- Web/Presenters/TopicsPresenter.php | 4 +- Web/Presenters/UserPresenter.php | 10 +- Web/Presenters/VideosPresenter.php | 16 +- Web/Presenters/WallPresenter.php | 2 +- Web/Presenters/templates/@layout.xml | 5 +- Web/Presenters/templates/About/BB.xml | 8 +- Web/Presenters/templates/About/Help.xml | 2 +- Web/Presenters/templates/About/Sandbox.xml | 2 +- .../templates/Admin/BansHistory.xml | 18 +- Web/Presenters/templates/Admin/Logs.xml | 36 +-- Web/Presenters/templates/Apps/Play.xml | 14 +- Web/Presenters/templates/Away/View.xml | 4 +- Web/Presenters/templates/Group/Followers.xml | 4 +- Web/Presenters/templates/Group/Statistics.xml | 8 +- Web/Presenters/templates/Group/View.xml | 16 +- Web/Presenters/templates/NoSpam/Index.xml | 50 ++-- Web/Presenters/templates/NoSpam/Tabs.xml | 6 +- Web/Presenters/templates/NoSpam/Templates.xml | 20 +- Web/Presenters/templates/Notes/Create.xml | 2 +- Web/Presenters/templates/Notes/Edit.xml | 2 +- Web/Presenters/templates/Photos/Album.xml | 5 +- Web/Presenters/templates/Photos/AlbumList.xml | 2 +- Web/Presenters/templates/Photos/EditAlbum.xml | 2 +- Web/Presenters/templates/Photos/EditPhoto.xml | 2 +- Web/Presenters/templates/Photos/Photo.xml | 13 +- .../templates/Photos/UnlinkPhoto.xml | 10 +- Web/Presenters/templates/User/Edit.xml | 4 +- Web/Presenters/templates/User/Settings.xml | 8 +- Web/Presenters/templates/User/VerifyPhone.xml | 6 +- Web/Presenters/templates/User/View.xml | 16 +- Web/Presenters/templates/User/banned.xml | 6 +- Web/Presenters/templates/Videos/Edit.xml | 6 +- Web/Presenters/templates/Videos/View.xml | 14 +- Web/Presenters/templates/Wall/Post.xml | 12 +- .../templates/components/comment.xml | 14 +- .../components/notifications/9601/_20_18_.xml | 2 +- .../components/post/microblogpost.xml | 2 +- .../templates/components/post/oldpost.xml | 2 +- locales/en.strings | 228 ++++++++++++++++++ locales/ru.strings | 226 +++++++++++++++++ 48 files changed, 688 insertions(+), 239 deletions(-) diff --git a/Web/Models/Entities/Club.php b/Web/Models/Entities/Club.php index 31485129..fbdc503b 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/Presenters/AdminPresenter.php b/Web/Presenters/AdminPresenter.php index 37fb50e8..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 <b>Recycle Bin</b>."); + $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 <b>$name</b>."); + $this->flash("succ", tr("admin_gift_moved_successfully"), "This gift will now be in <b>$name</b>."); $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(); diff --git a/Web/Presenters/CommentPresenter.php b/Web/Presenters/CommentPresenter.php index cb0efd0d..29c54c78 100644 --- a/Web/Presenters/CommentPresenter.php +++ b/Web/Presenters/CommentPresenter.php @@ -55,7 +55,7 @@ final class CommentPresenter extends OpenVKPresenter $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."); + $this->flashFail("err", tr("error"), tr("video_uploads_disabled")); $flags = 0; if($this->postParam("as_group") === "on" && !is_null($club) && $club->canBeModifiedBy($this->user->identity)) @@ -66,7 +66,7 @@ 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")); } } @@ -86,11 +86,11 @@ final class CommentPresenter extends OpenVKPresenter $video = Video::fastMake($this->user->id, $_FILES["_vid_attachment"]["name"], $this->postParam("text"), $_FILES["_vid_attachment"]); } } catch(ISE $ex) { - $this->flashFail("err", "Не удалось опубликовать комментарий", "Файл медиаконтента повреждён или слишком велик."); + $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_file_too_big")); } if(empty($this->postParam("text")) && !$photo && !$video) - $this->flashFail("err", "Не удалось опубликовать комментарий", "Комментарий пустой или слишком большой."); + $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_empty")); try { $comment = new Comment; @@ -102,7 +102,7 @@ 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)) @@ -124,7 +124,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 +135,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", - "Успешно", - "Этот комментарий больше не будет показыватся.<br/><a href='/al_comments/spam?$id'>Отметить как спам</a>?" + tr("success"), + tr("comment_will_not_appear") ); } } diff --git a/Web/Presenters/GiftsPresenter.php b/Web/Presenters/GiftsPresenter.php index 8f59bdcb..71480540 100644 --- a/Web/Presenters/GiftsPresenter.php +++ b/Web/Presenters/GiftsPresenter.php @@ -49,7 +49,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 +66,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 +91,7 @@ final class GiftsPresenter extends OpenVKPresenter $user->gift($this->user->identity, $gift, $comment, !is_null($this->postParam("anonymous"))); $gift->used(); - $this->flash("succ", "Подарок отправлен", "Вы отправили подарок <b>" . $user->getFirstName() . "</b> за " . $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/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 11026d00..bef984b7 100644 --- a/Web/Presenters/PhotosPresenter.php +++ b/Web/Presenters/PhotosPresenter.php @@ -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()); } @@ -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()); } @@ -224,14 +224,14 @@ final class PhotosPresenter extends OpenVKPresenter $this->willExecuteWriteAction(true); if(is_null($this->queryParam("album"))) - $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в DELETED.", 500, true); + $this->flashFail("err", tr("error"), tr("error_adding_to_deleted"), 500, true); [$owner, $id] = explode("_", $this->queryParam("album")); $album = $this->albums->get((int) $id); if(!$album) - $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в 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)) - $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса.", 500, true); + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"), 500, true); if($_SERVER["REQUEST_METHOD"] === "POST") { if($this->queryParam("act") == "finish") { @@ -258,7 +258,7 @@ final class PhotosPresenter extends OpenVKPresenter } if(!isset($_FILES)) - $this->flashFail("err", "Нету фотографии", "Выберите файл.", 500, true); + $this->flashFail("err", tr("no_photo"), tr("select_file"), 500, true); $photos = []; for($i = 0; $i < $this->postParam("count"); $i++) { @@ -304,7 +304,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(); @@ -312,7 +312,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()); } } @@ -326,7 +326,7 @@ 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")); $redirect = $photo->getAlbum()->getOwner() instanceof User ? "/id0" : "/club" . $ownerId; @@ -336,7 +336,7 @@ final class PhotosPresenter extends OpenVKPresenter if($_SERVER["REQUEST_METHOD"] === "POST") $this->returnJson(["success" => true]); - $this->flash("succ", "Фотография удалена", "Эта фотография была успешно удалена."); + $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 46aa7e92..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")); } 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 96b61b48..32ac421e 100644 --- a/Web/Presenters/WallPresenter.php +++ b/Web/Presenters/WallPresenter.php @@ -233,7 +233,7 @@ final class WallPresenter extends OpenVKPresenter $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."); + $this->flashFail("err", tr("error"), tr("video_uploads_disabled")); $anon = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"]; if($wallOwner instanceof Club && $this->postParam("as_group") === "on" && $this->postParam("force_sign") !== "on" && $anon) { diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml index f37532a7..f8a975e0 100644 --- a/Web/Presenters/templates/@layout.xml +++ b/Web/Presenters/templates/@layout.xml @@ -38,9 +38,8 @@ <body> <div id="sudo-banner" n:if="isset($thisUser) && $userTainted"> <p> - Вы вошли как <b>{$thisUser->getCanonicalName()}</b>. Пожалуйста, уважайте - право на тайну переписки других людей и не злоупотребляйте подменой пользователя. - Нажмите <a href="/setSID/unset?hash={rawurlencode($csrfToken)}">здесь</a>, чтобы выйти. + {_you_entered_as} <b>{$thisUser->getCanonicalName()}</b>. {_please_rights} + {_click_on} <a href="/setSID/unset?hash={rawurlencode($csrfToken)}">{_there}</a>, {_to_leave}. </p> </div> 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+ или - эквивалентный по функционалу навигатор по всемирной сети интернет.<br/> - Сожалеем об этом. + {_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 @@ <div id="faqhead">Для кого этот сайт?</div> <div id="faqcontent">Сайт предназначен для поиска друзей и знакомых, а также просмотр данных пользователя. Это как справочник города, с помощью которого люди могут быстро найти актуальную информацию о человеке. Также этот сайт подойдёт для ностальгираторов и тех, кто решил слезть с трубы "ВКонтакте", которого клон и является.<br></div> Я попозже допишу ок ~~ 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 @@ <thead> <tr> <th>ID</th> - <th>Забаненный</th> - <th>Инициатор</th> - <th>Начало</th> - <th>Конец</th> - <th>Время</th> - <th>Причина</th> - <th>Снята</th> + <th>{_bans_history_blocked}</th> + <th>{_bans_history_initiator}</th> + <th>{_bans_history_start}</th> + <th>{_bans_history_end}</th> + <th>{_bans_history_time}</th> + <th>{_bans_history_reason}</th> + <th>{_bans_history_removed}</th> </tr> </thead> <tbody> @@ -77,7 +77,7 @@ {_admin_banned} </span> {else} - <b style="color: red;">Активная блокировка</b> + <b style="color: red;">{_bans_history_active}</b> {/if} </td> </tr> 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 @@ </style> <form class="aui"> <div> - <select class="select medium-field" type="number" id="type" name="type" placeholder="Тип изменения"> - <option value="any" n:attr="selected => !$type">Любое</option> - <option value="0" n:attr="selected => $type === 0">Создание</option> - <option value="1" n:attr="selected => $type === 1">Редактирование</option> - <option value="2" n:attr="selected => $type === 2">Удаление</option> - <option value="3" n:attr="selected => $type === 3">Восстановление</option> + <select class="select medium-field" type="number" id="type" name="type" placeholder="{_logs_change_type}"> + <option value="any" n:attr="selected => !$type">{_logs_anything}</option> + <option value="0" n:attr="selected => $type === 0">{_logs_adding}</option> + <option value="1" n:attr="selected => $type === 1">{_logs_editing}</option> + <option value="2" n:attr="selected => $type === 2">{_logs_removing}</option> + <option value="3" n:attr="selected => $type === 3">{_logs_restoring}</option> </select> - <input class="text medium-field" type="number" id="id" name="id" placeholder="ID записи" n:attr="value => $id"/> - <input class="text medium-field" type="text" id="uid" name="uid" placeholder="UUID пользователя" n:attr="value => $user"/> + <input class="text medium-field" type="number" id="id" name="id" placeholder="{_logs_id_post}" n:attr="value => $id"/> + <input class="text medium-field" type="text" id="uid" name="uid" placeholder="{_logs_uuid_user}" n:attr="value => $user"/> </div> <div style="margin: 8px 0;" /> <div> - <select class="select medium-field" id="obj_type" name="obj_type" placeholder="Тип объекта"> - <option value="any" n:attr="selected => !$obj_type">Любой</option> + <select class="select medium-field" id="obj_type" name="obj_type" placeholder="{_logs_change_object}"> + <option value="any" n:attr="selected => !$obj_type">{_logs_anything}</option> <option n:foreach="$object_types as $type" n:attr="selected => $obj_type === $type">{$type}</option> </select> - <input class="text medium-field" type="number" id="obj_id" name="obj_id" placeholder="ID объекта" n:attr="value => $obj_id"/> + <input class="text medium-field" type="number" id="obj_id" name="obj_id" placeholder="{_logs_id_object}" n:attr="value => $obj_id"/> <input type="submit" class="aui-button aui-button-primary medium-field" value="Поиск" style="width: 165px;"/> </div> </form> @@ -42,11 +42,11 @@ <thead> <tr> <th>ID</th> - <th>Пользователь</th> - <th>Объект</th> - <th>Тип</th> - <th>Изменения</th> - <th>Время</th> + <th>{_logs_user}</th> + <th>{_logs_object}</th> + <th>{_logs_type}</th> + <th>{_logs_changes}</th> + <th>{_logs_time}</th> </tr> </thead> <tbody> 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} - <a style="float: right;" onClick="reportApp()" n:if="$canReport ?? false">Пожаловаться</a> + <a style="float: right;" onClick="reportApp()" n:if="$canReport ?? false">{_report}</a> {/block} {block content} @@ -37,20 +37,20 @@ <script n:if="$canReport ?? false"> function reportApp() { - uReportMsgTxt = "Вы собираетесь пожаловаться на данное приложение."; - uReportMsgTxt += "<br/>Что именно вам кажется недопустимым в этом материале?"; - uReportMsgTxt += "<br/><br/><b>Причина жалобы</b>: <input type='text' id='uReportMsgInput' placeholder='Причина' />" + uReportMsgTxt = {_going_to_report_app}; + uReportMsgTxt += "<br/>"+tr("report_question_text"); + uReportMsgTxt += "<br/><br/><b>"+tr("report_reason")+"</b>: <input type='text' id='uReportMsgInput' placeholder='" + tr("reason") + "' />" - MessageBox("Пожаловаться?", uReportMsgTxt, ["Подтвердить", "Отмена"], [ + MessageBox(tr("report_question"), uReportMsgTxt, [tr("confirm_m"), tr("cancel")], [ (function() { res = document.querySelector("#uReportMsgInput").value; xhr = new XMLHttpRequest(); xhr.open("GET", "/report/" + {$id} + "?reason=" + res + "&type=app", true); xhr.onload = (function() { if(xhr.responseText.indexOf("reason") === -1) - MessageBox("Ошибка", "Не удалось подать жалобу...", ["OK"], [Function.noop]); + MessageBox(tr("error"), tr("error_sending_report"), ["OK"], [Function.noop]); else - MessageBox("Операция успешна", "Скоро её рассмотрят модераторы", ["OK"], [Function.noop]); + MessageBox(tr("action_successfully"), tr("will_be_watched"), ["OK"], [Function.noop]); }); xhr.send(null); }), diff --git a/Web/Presenters/templates/Away/View.xml b/Web/Presenters/templates/Away/View.xml index f6d05a2c..870f541f 100644 --- a/Web/Presenters/templates/Away/View.xml +++ b/Web/Presenters/templates/Away/View.xml @@ -1,9 +1,9 @@ {extends "../@layout.xml"} -{block title}Переход по ссылке заблокирован{/block} +{block title}{_transition_is_blocked}{/block} {block header} -Предупреждение +{_caution} {/block} {block content} diff --git a/Web/Presenters/templates/Group/Followers.xml b/Web/Presenters/templates/Group/Followers.xml index 45e40bbb..482d156a 100644 --- a/Web/Presenters/templates/Group/Followers.xml +++ b/Web/Presenters/templates/Group/Followers.xml @@ -55,7 +55,7 @@ <tbody> <tr> <td width="120" valign="top"><span class="nobold">{_gender}: </span></td> - <td>{$user->isFemale() ? "женский" : "мужской"}</td> + <td>{$user->isFemale() ? tr("female"): tr("male")}</td> </tr> <tr> <td width="120" valign="top"><span class="nobold">{_registration_date}: </span></td> @@ -82,8 +82,6 @@ </table> <script n:if="$club->getOwner()->getId() != $user->getId() && $manager && $thisUser->getId() == $club->getOwner()->getId()"> - console.log("gayshit"); - console.log("сам такой"); function changeOwner(club, newOwner) { const action = "/groups/" + club + "/setNewOwner/" + newOwner; diff --git a/Web/Presenters/templates/Group/Statistics.xml b/Web/Presenters/templates/Group/Statistics.xml index f1b53cc1..4654557a 100644 --- a/Web/Presenters/templates/Group/Statistics.xml +++ b/Web/Presenters/templates/Group/Statistics.xml @@ -7,12 +7,12 @@ {block content} <div> - <h4>Охват</h4> - <p>Этот график отображает охват за последние 7 дней.</p> + <h4>{_coverage}</h4> + <p>{_coverage_this_week}</p> <div id="reachChart" style="width: 100%; height: 280px;"></div> - <h4>Просмотры</h4> - <p>Этот график отображает просмотры постов сообщества за последние 7 дней.</p> + <h4>{_views}</h4> + <p>{_views_this_week}</p> <div id="viewsChart" style="width: 100%; height: 280px;"></div> <style> diff --git a/Web/Presenters/templates/Group/View.xml b/Web/Presenters/templates/Group/View.xml index 42cdbde3..0242bbb8 100644 --- a/Web/Presenters/templates/Group/View.xml +++ b/Web/Presenters/templates/Group/View.xml @@ -9,7 +9,7 @@ <img n:if="$club->isVerified()" class="name-checkmark" src="/assets/packages/static/openvk/img/checkmark.png" - alt="Подтверждённая страница" + alt="{_verified_page}" /> {/block} @@ -143,24 +143,24 @@ {/if} {var $canReport = $thisUser->getId() != $club->getOwner()->getId()} {if $canReport} - <a class="profile_link" style="display:block;width:96%;" href="javascript:reportVideo()">{_report}</a> + <a class="profile_link" style="display:block;" href="javascript:reportVideo()">{_report}</a> <script> function reportVideo() { - uReportMsgTxt = "Вы собираетесь пожаловаться на данное сообщество."; - uReportMsgTxt += "<br/>Что именно вам кажется недопустимым в этом материале?"; - uReportMsgTxt += "<br/><br/><b>Причина жалобы</b>: <input type='text' id='uReportMsgInput' placeholder='Причина' />" + uReportMsgTxt = tr("going_to_report_club"); + uReportMsgTxt += "<br/>"+tr("report_question_text"); + uReportMsgTxt += "<br/><br/><b>"+tr("report_reason")+"</b>: <input type='text' id='uReportMsgInput' placeholder='" + tr("reason") + "' />" - MessageBox("Пожаловаться?", uReportMsgTxt, ["Подтвердить", "Отмена"], [ + MessageBox(tr("report_question"), uReportMsgTxt, [tr("confirm_m"), tr("cancel")], [ (function() { res = document.querySelector("#uReportMsgInput").value; xhr = new XMLHttpRequest(); xhr.open("GET", "/report/" + {$club->getId()} + "?reason=" + res + "&type=group", true); xhr.onload = (function() { if(xhr.responseText.indexOf("reason") === -1) - MessageBox("Ошибка", "Не удалось подать жалобу...", ["OK"], [Function.noop]); + MessageBox(tr("error"), tr("error_sending_report"), ["OK"], [Function.noop]); else - MessageBox("Операция успешна", "Скоро её рассмотрят модераторы", ["OK"], [Function.noop]); + MessageBox(tr("action_successfully"), tr("will_be_watched"), ["OK"], [Function.noop]); }); xhr.send(null); }), diff --git a/Web/Presenters/templates/NoSpam/Index.xml b/Web/Presenters/templates/NoSpam/Index.xml index 89305c5b..2bf9d1df 100644 --- a/Web/Presenters/templates/NoSpam/Index.xml +++ b/Web/Presenters/templates/NoSpam/Index.xml @@ -27,13 +27,13 @@ <tbody id="models-list"> <tr id="0-model"> <td width="83px"> - <span class="nobold">Раздел:</span> + <span class="nobold">{_section}:</span> </td> <td> <div style="display: flex; gap: 8px; justify-content: space-between;"> <div id="add-model" class="noSpamIcon noSpamIcon-Add" style="display: none;" /> <select name="model" id="model" class="model initialModel" style="margin-left: -2px;"> - <option selected value="none">Не выбрано</option> + <option selected value="none">{_relationship_0}</option> <option n:foreach="$models as $model" value="{$model}">{$model}</option> </select> </div> @@ -47,7 +47,7 @@ <tbody> <tr style="width: 129px; border-top: 1px solid #ECECEC;"> <td> - <span class="nobold">Подстрока:</span> + <span class="nobold">{_substring}:</span> </td> <td> <input type="text" name="regex" placeholder="Regex" id="regex"> @@ -55,10 +55,10 @@ </tr> <tr style="width: 129px; border-top: 1px solid #ECECEC;"> <td> - <span class="nobold">Пользователь:</span> + <span class="nobold">{_n_user}:</span> </td> <td> - <input type="text" name="user" placeholder="Ссылка на страницу" id="user"> + <input type="text" name="user" placeholder="{_link_to_page}" id="user"> </td> </tr> <tr style="width: 129px"> @@ -66,12 +66,12 @@ <span class="nobold">IP:</span> </td> <td> - <input type="text" name="ip" id="ip" placeholder="или подсеть"> + <input type="text" name="ip" id="ip" placeholder="{_or_subnet}"> </td> </tr> <tr style="width: 129px"> <td> - <span class="nobold">Юзер-агент:</span> + <span class="nobold">User-Agent:</span> </td> <td> <input type="text" name="useragent" id="useragent" placeholder="Mozila 1.0 Blablabla/test"> @@ -79,7 +79,7 @@ </tr> <tr style="width: 129px"> <td> - <span class="nobold">Время раньше, чем:</span> + <span class="nobold">{_time_before}:</span> </td> <td> <input type="datetime-local" name="ts" id="ts"> @@ -87,7 +87,7 @@ </tr> <tr style="width: 129px"> <td> - <span class="nobold">Время позже, чем:</span> + <span class="nobold">{_time_after}:</span> </td> <td> <input type="datetime-local" name="te" id="te"> @@ -97,19 +97,19 @@ </table> <textarea style="resize: vertical; width: calc(100% - 6px)" placeholder='city = "Воскресенск" && id = 1' name="where" id="where"/> - <span style="color: grey; font-size: 8px;">WHERE для поиска по разделу</span> + <span style="color: grey; font-size: 8px;">{_where_for_search}</span> <div style="border-top: 1px solid #ECECEC; margin: 8px 0;"/> <table cellspacing="7" cellpadding="0" width="100%" border="0"> <tbody> <tr style="width: 129px; border-top: 1px solid #ECECEC;"> <td> - <span class="nobold">Параметры блокировки:</span> + <span class="nobold">{_block_params}:</span> </td> <td> - <select name="ban_type" id="noSpam-ban-type" style="width: 140px;"> - <option value="1">Только откат</option> - <option value="2">Только блокировка</option> - <option value="3">Откат и блокировка</option> + <select name="ban_type" id="noSpam-ban-type" style="width: 140px;" + <option value="1">{_only_rollback}</option> + <option value="2">{_only_block}</option> + <option value="3">{_rollback_and_block}</option> </select> </td> </tr> @@ -136,8 +136,8 @@ <div style="border-top: 1px solid #ECECEC; margin: 8px 0;"/> <center> <div id="noSpam-buttons"> - <input id="search" type="submit" value="Поиск" class="button"/> - <input id="apply" type="submit" value="Применить" class="button" style="display: none;"/> + <input id="search" type="submit" value="{_header_search}" class="button"/> + <input id="apply" type="submit" value="{_subm}" class="button" style="display: none;"/> </div> <div id="noSpam-loader" style="display: none;"> <img src="/assets/packages/static/openvk/img/loading_mini.gif" style="width: 40px;"> @@ -145,7 +145,7 @@ </center> </div> <div id="noSpam-model-not-selected"> - <center id="noSpam-model-not-selected-text" style="padding: 71px 25px;">Выберите раздел для начала работы</center> + <center id="noSpam-model-not-selected-text" style="padding: 71px 25px;">{_select_section_for_start}</center> <center id="noSpam-model-not-selected-loader" style="display: none;"> <img src="/assets/packages/static/openvk/img/loading_mini.gif" style="width: 40px; margin: 125px 0;"> </center> @@ -155,11 +155,11 @@ <center id="noSpam-results-loader" style="display: none;"> <img src="/assets/packages/static/openvk/img/loading_mini.gif" style="width: 40px; margin: 125px 0;"> </center> - <center id="noSpam-results-text" style="margin: 125px 25px;">Здесь будут отображаться результаты поиска</center> + <center id="noSpam-results-text" style="margin: 125px 25px;">{_results_will_be_there}</center> <div id="noSpam-results-block" style="display: none;"> - <h4 style="padding: 8px;">Результаты поиска + <h4 style="padding: 8px;">{_search_results} <span style="color: #a2a2a2; font-weight: inherit"> - (<span id="noSpam-results-count" style="color: #a2a2a2; font-weight: inherit;"></span> шт.) + (<span id="noSpam-results-count" style="color: #a2a2a2; font-weight: inherit;"></span> {_cnt}.) </span> </h4> <ul style="padding-inline-start:18px;" id="noSpam-results-list"></ul> @@ -242,17 +242,17 @@ $("#noSpam-results-block").show(); $("#apply").show(); } else { - $("#noSpam-results-text").text(ban ? "Операция завершена успешно" : "Ничего не найдено :("); + $("#noSpam-results-text").text(ban ? tr("operation_successfully") : tr("no_found")); $("#noSpam-results-text").show(); } } else { - $("#noSpam-results-text").text(response?.error ?? "Неизвестная ошибка"); + $("#noSpam-results-text").text(response?.error ?? tr("unknown_error")); $("#noSpam-results-text").show(); } }, error: (error) => { console.error("Error while searching noSpam:", error); - $("#noSpam-results-text").text("Ошибка при выполнении запроса"); + $("#noSpam-results-text").text(tr("error_when_searching")); $("#noSpam-results-text").show(); } }); @@ -323,7 +323,7 @@ <div style="display: flex; gap: 8px; justify-content: space-between;"> <div class="noSpamIcon noSpamIcon-Delete" onClick="deleteModelSelect(${ $('.model').length});"></div> <select name="model" class="model" style="margin-left: -2px;" onChange="selectChange($(this).val())"> - <option selected value="none">Не выбрано</option> + <option selected value="none">{_relationship_0}</option> {foreach $models as $model} <option value={$model}>{$model|noescape}</option> {/foreach} diff --git a/Web/Presenters/templates/NoSpam/Tabs.xml b/Web/Presenters/templates/NoSpam/Tabs.xml index e80db91e..69943531 100644 --- a/Web/Presenters/templates/NoSpam/Tabs.xml +++ b/Web/Presenters/templates/NoSpam/Tabs.xml @@ -1,9 +1,9 @@ <div n:attr="id => ($mode === 'form' ? 'activetabs' : 'ki')" class="tab"> - <a n:attr="id => ($mode === 'form' ? 'act_tab_a' : 'ki')" href="/noSpam">Бан по шаблону</a> + <a n:attr="id => ($mode === 'form' ? 'act_tab_a' : 'ki')" href="/noSpam">{_template_ban}</a> </div> <div n:attr="id => ($mode === 'templates' ? 'activetabs' : 'ki')" class="tab"> - <a n:attr="id => ($mode === 'templates' ? 'act_tab_a' : 'ki')" href="/noSpam?act=templates">Действующие шаблоны</a> + <a n:attr="id => ($mode === 'templates' ? 'act_tab_a' : 'ki')" href="/noSpam?act=templates">{_active_templates}</a> </div> <div n:attr="id => ($mode === 'reports' ? 'activetabs' : 'ki')" class="tab"> - <a n:attr="id => ($mode === 'reports' ? 'act_tab_a' : 'ki')" href="/scumfeed">Жалобы пользователей</a> + <a n:attr="id => ($mode === 'reports' ? 'act_tab_a' : 'ki')" href="/scumfeed">{_users_reports}</a> </div> diff --git a/Web/Presenters/templates/NoSpam/Templates.xml b/Web/Presenters/templates/NoSpam/Templates.xml index 83fd77c7..a86df534 100644 --- a/Web/Presenters/templates/NoSpam/Templates.xml +++ b/Web/Presenters/templates/NoSpam/Templates.xml @@ -1,6 +1,6 @@ {extends "../@layout.xml"} -{block title}Шаблоны{/block} +{block title}{_templates}{/block} {block header}{include title}{/block} {block content} @@ -44,14 +44,14 @@ <table n:if="count($templates) > 0" cellspacing="0" cellpadding="7" width="100%"> <tr> <th style="text-align: center;">ID</th> - <th>Пользователь</th> - <th style="text-align: center;">Раздел</th> - <th>Подстрока</th> + <th>{_n_user}</th> + <th style="text-align: center;">{_section}</th> + <th>{_substring}</th> <th>Where</th> - <th style="text-align: center;">Тип</th> - <th style="text-align: center;">Количество</th> - <th>Время</th> - <th style="text-align: center;">Действия</th> + <th style="text-align: center;">{_type}</th> + <th style="text-align: center;">{_count}</th> + <th>{_time}</th> + <th style="text-align: center;">{_actions}</th> </tr> <tr n:foreach="$templates as $template"> <td id="id-{$template->getId()}" onClick="openTableField('id', {$template->getId()})" style="text-align: center;"><b>{$template->getId()}</b></td> @@ -75,8 +75,8 @@ <div id="noSpam-rollback-loader-{$template->getId()}" style="display: none;"> <img src="/assets/packages/static/openvk/img/loading_mini.gif" style="width: 40px;"> </div> - <a n:if="!$template->isRollbacked()" id="noSpam-rollback-template-link-{$template->getId()}" onClick="rollbackTemplate({$template->getId()})">откатить</a> - <span n:attr="style => $template->isRollbacked() ? '' : 'display: none;'" id="noSpam-rollback-template-rollbacked-{$template->getId()}">откачен</span> + <a n:if="!$template->isRollbacked()" id="noSpam-rollback-template-link-{$template->getId()}" onClick="rollbackTemplate({$template->getId()})">{_roll_back}</a> + <span n:attr="style => $template->isRollbacked() ? '' : 'display: none;'" id="noSpam-rollback-template-rollbacked-{$template->getId()}">{roll_backed}</span> </div> </td> </tr> diff --git a/Web/Presenters/templates/Notes/Create.xml b/Web/Presenters/templates/Notes/Create.xml index cd653924..e8e2d20e 100644 --- a/Web/Presenters/templates/Notes/Create.xml +++ b/Web/Presenters/templates/Notes/Create.xml @@ -13,7 +13,7 @@ <textarea name="html" style="display:none;"></textarea> <div id="editor" style="width:600px;height:300px;border:1px solid grey"></div> - <p><i><a href="/kb/notes">Кое-что</a> из (X)HTML поддерживается.</i></p> + <p><i><a href="/kb/notes">{_something}</a> {_supports_xhtml}</i></p> <input type="hidden" name="hash" value="{$csrfToken}" /> <button class="button">{_save}</button> diff --git a/Web/Presenters/templates/Notes/Edit.xml b/Web/Presenters/templates/Notes/Edit.xml index b010c6bc..b904c72e 100644 --- a/Web/Presenters/templates/Notes/Edit.xml +++ b/Web/Presenters/templates/Notes/Edit.xml @@ -18,7 +18,7 @@ <textarea name="html" style="display:none;"></textarea> <div id="editor" style="width:600px;height:300px;border:1px solid grey"></div> - <p><i><a href="/kb/notes">Кое-что</a> из (X)HTML поддерживается.</i></p> + <p><i><a href="/kb/notes">{_something}</a> {_supports_xhtml}</i></p> <input type="hidden" name="hash" value="{$csrfToken}" /> <button class="button">{_save}</button> diff --git a/Web/Presenters/templates/Photos/Album.xml b/Web/Presenters/templates/Photos/Album.xml index 6f22c490..1bd49a6f 100644 --- a/Web/Presenters/templates/Photos/Album.xml +++ b/Web/Presenters/templates/Photos/Album.xml @@ -1,6 +1,6 @@ {extends "../@layout.xml"} -{block title}Альбом {$album->getName()}{/block} +{block title}{_album} {$album->getName()}{/block} {block header} {var $isClub = ($album->getOwner() instanceof openvk\Web\Models\Entities\Club)} @@ -18,7 +18,8 @@ {block content} <a href="/album{$album->getPrettyId()}"> - <b>{$album->getPhotosCount()} фотографий</b> + {* TODO: Добавить склонения *} + <b>{$album->getPhotosCount()} {_photos}</b> </a> {if !is_null($thisUser) && $album->canBeModifiedBy($thisUser) && !$album->isCreatedBySystem()} diff --git a/Web/Presenters/templates/Photos/AlbumList.xml b/Web/Presenters/templates/Photos/AlbumList.xml index f6742463..e59b6a58 100644 --- a/Web/Presenters/templates/Photos/AlbumList.xml +++ b/Web/Presenters/templates/Photos/AlbumList.xml @@ -58,7 +58,7 @@ {block description} <span>{$x->getDescription() ?? $x->getName()}</span><br /> - <span style="color: grey;">{$x->getPhotosCount()} фотографий</span><br /> + <span style="color: grey;">{$x->getPhotosCount()} {_photos}</span><br /> <span style="color: grey;">{tr("updated_at", $x->getEditTime() ?? $x->getCreationTime())}</span><br /> <span style="color: grey;">{_created} {$x->getCreationTime()}</span><br /> {/block} diff --git a/Web/Presenters/templates/Photos/EditAlbum.xml b/Web/Presenters/templates/Photos/EditAlbum.xml index a10b5c7f..5a247e27 100644 --- a/Web/Presenters/templates/Photos/EditAlbum.xml +++ b/Web/Presenters/templates/Photos/EditAlbum.xml @@ -1,5 +1,5 @@ {extends "../@layout.xml"} -{block title}Изменить альбом{/block} +{block title}{_edit_album}{/block} {block header} <a href="{$album->getOwner()->getURL()}">{$album->getOwner()->getCanonicalName()}</a> diff --git a/Web/Presenters/templates/Photos/EditPhoto.xml b/Web/Presenters/templates/Photos/EditPhoto.xml index 9c269491..159916ea 100644 --- a/Web/Presenters/templates/Photos/EditPhoto.xml +++ b/Web/Presenters/templates/Photos/EditPhoto.xml @@ -1,5 +1,5 @@ {extends "../@layout.xml"} -{block title}Изменить фотографию{/block} +{block title}{_edit_photo}{/block} {block header} <a href="{$thisUser->getURL()}">{$thisUser->getCanonicalName()}</a> diff --git a/Web/Presenters/templates/Photos/Photo.xml b/Web/Presenters/templates/Photos/Photo.xml index 047f382a..bb6f171f 100644 --- a/Web/Presenters/templates/Photos/Photo.xml +++ b/Web/Presenters/templates/Photos/Photo.xml @@ -53,20 +53,20 @@ <a n:if="$canReport ?? false" class="profile_link" style="display:block;width:96%;" href="javascript:reportPhoto()">{_report}</a> <script n:if="$canReport ?? false"> function reportPhoto() { - uReportMsgTxt = "Вы собираетесь пожаловаться на данную фотографию."; - uReportMsgTxt += "<br/>Что именно вам кажется недопустимым в этом материале?"; - uReportMsgTxt += "<br/><br/><b>Причина жалобы</b>: <input type='text' id='uReportMsgInput' placeholder='Причина' />" + uReportMsgTxt = tr("going_to_report_photo"); + uReportMsgTxt += "<br/>"+tr("report_question_text"); + uReportMsgTxt += "<br/><br/><b>"+tr("report_reason")+"</b>: <input type='text' id='uReportMsgInput' placeholder='" + tr("reason") + "' />" - MessageBox("Пожаловаться?", uReportMsgTxt, ["Подтвердить", "Отмена"], [ + MessageBox(tr("report_question"), uReportMsgTxt, [tr("confirm_m"), tr("cancel")], [ (function() { res = document.querySelector("#uReportMsgInput").value; xhr = new XMLHttpRequest(); xhr.open("GET", "/report/" + {$photo->getId()} + "?reason=" + res + "&type=photo", true); xhr.onload = (function() { if(xhr.responseText.indexOf("reason") === -1) - MessageBox("Ошибка", "Не удалось подать жалобу...", ["OK"], [Function.noop]); + MessageBox(tr("error"), tr("error_sending_report"), ["OK"], [Function.noop]); else - MessageBox("Операция успешна", "Скоро её рассмотрят модераторы", ["OK"], [Function.noop]); + MessageBox(tr("action_successfully"), tr("will_be_watched"), ["OK"], [Function.noop]); }); xhr.send(null); }), @@ -74,7 +74,6 @@ ]); } </script> - <a href="{$photo->getURL()}" class="profile_link" target="_blank" style="display:block;width:96%;">{_open_original}</a> </div> </div> {/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} <br/> <br/> <form method="POST"> <input type="hidden" value="{$csrfToken}" name="hash" /> - <a href="{$_SERVER['HTTP_REFERER']}" class="button">Нет</a> + <a href="{$_SERVER['HTTP_REFERER']}" class="button">{_no}</a> - <button class="button">Да</button> + <button class="button">{_yes}</button> </form> {/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 @@ <form method="POST" enctype="multipart/form-data"> <div id="backdropEditor"> <div id="backdropFilePicker"> - <label class="button" style="">Обзор<input type="file" accept="image/*" name="backdrop1" style="display: none;"></label> + <label class="button" style="">{_browse}<input type="file" accept="image/*" name="backdrop1" style="display: none;"></label> <div id="spacer" style="width: 366px;"></div> - <label class="button" style="">Обзор<input type="file" accept="image/*" name="backdrop2" style="display: none;"></label> + <label class="button" style="">{_browse}<input type="file" accept="image/*" name="backdrop2" style="display: none;"></label> <div id="spacer" style="width: 366px;"></div> </div> </div> diff --git a/Web/Presenters/templates/User/Settings.xml b/Web/Presenters/templates/User/Settings.xml index 9d9a2b25..c8f0f61b 100644 --- a/Web/Presenters/templates/User/Settings.xml +++ b/Web/Presenters/templates/User/Settings.xml @@ -190,13 +190,13 @@ <script> function viewBackupCodes() { - MessageBox("Просмотр резервных кодов", ` + MessageBox(tr("viewing_backup_codes"), ` <form id="back-codes-view-form" method="post" action="/settings/2fa"> <label for="password">Пароль</label> <input type="password" id="password" name="password" required /> <input type="hidden" name="hash" value={$csrfToken} /> </form> - `, ["Просмотреть", "Отменить"], [ + `, [tr("viewing"), tr("cancel")], [ () => { document.querySelector("#back-codes-view-form").submit(); }, Function.noop @@ -204,13 +204,13 @@ } function disableTwoFactorAuth() { - MessageBox("Отключить 2FA", ` + MessageBox(tr("disable_2fa"), ` <form id="two-factor-auth-disable-form" method="post" action="/settings/2fa/disable"> <label for="password">Пароль</label> <input type="password" id="password" name="password" required /> <input type="hidden" name="hash" value={$csrfToken} /> </form> - `, ["Отключить", "Отменить"], [ + `, [tr("disable"), tr("cancel")], [ () => { document.querySelector("#two-factor-auth-disable-form").submit(); }, Function.noop diff --git a/Web/Presenters/templates/User/VerifyPhone.xml b/Web/Presenters/templates/User/VerifyPhone.xml index d65b6bb4..a16101cb 100644 --- a/Web/Presenters/templates/User/VerifyPhone.xml +++ b/Web/Presenters/templates/User/VerifyPhone.xml @@ -1,13 +1,13 @@ {extends "../@layout.xml"} -{block title}Подтвердить номер телефона{/block} +{block title}{_verify_phone_number}{/block} {block header} - Подтвердить номер телефона + {_verify_phone_number} {/block} {block content} <center> - <p>Мы отправили SMS с кодом на номер <b>{substr_replace($change->number, "*****", 5, 5)}</b>, введите его сюда:</p> + <p>{_we_sended_first} <b>{substr_replace($change->number, "*****", 5, 5)}</b>, {_we_sended_end}:</p> <form method="POST"> <input type="text" name="code" placeholder="34156, например" required /> diff --git a/Web/Presenters/templates/User/View.xml b/Web/Presenters/templates/User/View.xml index b3cd3257..9014750d 100644 --- a/Web/Presenters/templates/User/View.xml +++ b/Web/Presenters/templates/User/View.xml @@ -119,10 +119,10 @@ {_warn_user_action} </a> <a href="/admin/user{$user->getId()}/bans" class="profile_link"> - Блокировки + {_blocks} </a> <a href="/admin/logs?uid={$user->getId()}" class="profile_link" style="width: 194px;"> - Последние действия + {_last_actions} </a> {/if} @@ -173,20 +173,20 @@ <a class="profile_link" style="display:block;width:96%;" href="javascript:reportUser()">{_report}</a> <script> function reportUser() { - uReportMsgTxt = "Вы собираетесь пожаловаться на данного пользователя."; - uReportMsgTxt += "<br/>Что именно вам кажется недопустимым в этом материале?"; - uReportMsgTxt += "<br/><br/><b>Причина жалобы</b>: <input type='text' id='uReportMsgInput' placeholder='Причина' />" + uReportMsgTxt = tr("going_to_report_user"); + uReportMsgTxt += "<br/>"+tr("report_question_text"); + uReportMsgTxt += "<br/><br/><b>"+tr("report_reason")+"</b>: <input type='text' id='uReportMsgInput' placeholder='" + tr("reason") + "' />" - MessageBox("Пожаловаться?", uReportMsgTxt, ["Подтвердить", "Отмена"], [ + MessageBox(tr("report_question"), uReportMsgTxt, [tr("confirm_m"), tr("cancel")], [ (function() { res = document.querySelector("#uReportMsgInput").value; xhr = new XMLHttpRequest(); xhr.open("GET", "/report/" + {$user->getId()} + "?reason=" + res + "&type=user", true); xhr.onload = (function() { if(xhr.responseText.indexOf("reason") === -1) - MessageBox("Ошибка", "Не удалось подать жалобу...", ["OK"], [Function.noop]); + MessageBox(tr("error"), tr("error_sending_report"), ["OK"], [Function.noop]); else - MessageBox("Операция успешна", "Скоро её рассмотрят модераторы", ["OK"], [Function.noop]); + MessageBox(tr("action_successfully"), tr("will_be_watched"), ["OK"], [Function.noop]); }); xhr.send(null); }), diff --git a/Web/Presenters/templates/User/banned.xml b/Web/Presenters/templates/User/banned.xml index 8abe4d89..8afcf642 100644 --- a/Web/Presenters/templates/User/banned.xml +++ b/Web/Presenters/templates/User/banned.xml @@ -3,9 +3,9 @@ <p> {tr("user_banned", htmlentities($user->getFirstName()))|noescape}<br/> {_user_banned_comment} <b>{$user->getBanReason()}</b>.<br/> - Пользователь заблокирован - <span n:if="$user->getUnbanTime() !== NULL">до: <b>{$user->getUnbanTime()}</b></span> - <span n:if="$user->getUnbanTime() === NULL"><b>навсегда</b></span> + {_user_is_blocked} + <span n:if="$user->getUnbanTime() !== NULL">{_before}: <b>{$user->getUnbanTime()}</b></span> + <span n:if="$user->getUnbanTime() === NULL"><b>{_forever}</b></span> </p> {if isset($thisUser)} <p n:if="$thisUser->getChandlerUser()->can('access')->model('admin')->whichBelongsTo(NULL) || $thisUser->getChandlerUser()->can('write')->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0)"> diff --git a/Web/Presenters/templates/Videos/Edit.xml b/Web/Presenters/templates/Videos/Edit.xml index 4a97b319..c9ca9a9d 100644 --- a/Web/Presenters/templates/Videos/Edit.xml +++ b/Web/Presenters/templates/Videos/Edit.xml @@ -1,5 +1,5 @@ {extends "../@layout.xml"} -{block title}Изменить видеозапись{/block} +{block title}{_change_video}{/block} {block header} <a href="{$thisUser->getURL()}">{$thisUser->getCanonicalName()}</a> @@ -8,12 +8,12 @@ » <a href="/video{$video->getPrettyId()}">{_video}</a> » - Изменить видеозапись + {_change_video} {/block} {block content} <div class="container_gray"> - <h4>Изменить видеозапись</h4> + <h4>{_change_video}</h4> <form method="post" enctype="multipart/form-data"> <table cellspacing="7" cellpadding="0" width="60%" border="0" align="center"> <tbody> diff --git a/Web/Presenters/templates/Videos/View.xml b/Web/Presenters/templates/Videos/View.xml index 7cc41fe4..38967b5e 100644 --- a/Web/Presenters/templates/Videos/View.xml +++ b/Web/Presenters/templates/Videos/View.xml @@ -19,7 +19,7 @@ {else} {var $driver = $video->getVideoDriver()} {if !$driver} - Эта видеозапись не поддерживается в вашей версии OpenVK. + {_unknown_video} {else} {$driver->getEmbed()|noescape} {/if} @@ -70,20 +70,20 @@ <script n:if="$canReport ?? false"> function reportVideo() { - uReportMsgTxt = "Вы собираетесь пожаловаться на данную видеозапись."; - uReportMsgTxt += "<br/>Что именно вам кажется недопустимым в этом материале?"; - uReportMsgTxt += "<br/><br/><b>Причина жалобы</b>: <input type='text' id='uReportMsgInput' placeholder='Причина' />" + uReportMsgTxt = tr("going_to_report_video"); + uReportMsgTxt += "<br/>"+tr("report_question_text"); + uReportMsgTxt += "<br/><br/><b>"+tr("report_reason")+"</b>: <input type='text' id='uReportMsgInput' placeholder='" + tr("reason") + "' />" - MessageBox("Пожаловаться?", uReportMsgTxt, ["Подтвердить", "Отмена"], [ + MessageBox(tr("report_question"), uReportMsgTxt, [tr("confirm_m"), tr("cancel")], [ (function() { res = document.querySelector("#uReportMsgInput").value; xhr = new XMLHttpRequest(); xhr.open("GET", "/report/" + {$video->getId()} + "?reason=" + res + "&type=video", true); xhr.onload = (function() { if(xhr.responseText.indexOf("reason") === -1) - MessageBox("Ошибка", "Не удалось подать жалобу...", ["OK"], [Function.noop]); + MessageBox(tr("error"), tr("error_sending_report"), ["OK"], [Function.noop]); else - MessageBox("Операция успешна", "Скоро её рассмотрят модераторы", ["OK"], [Function.noop]); + MessageBox(tr("action_successfully"), tr("will_be_watched"), ["OK"], [Function.noop]); }); xhr.send(null); }), diff --git a/Web/Presenters/templates/Wall/Post.xml b/Web/Presenters/templates/Wall/Post.xml index 8ac11bb7..bd40b6c5 100644 --- a/Web/Presenters/templates/Wall/Post.xml +++ b/Web/Presenters/templates/Wall/Post.xml @@ -46,20 +46,20 @@ </div> <script n:if="$canReport ?? false"> function reportPost() { - uReportMsgTxt = "Вы собираетесь пожаловаться на данную запись."; - uReportMsgTxt += "<br/>Что именно вам кажется недопустимым в этом материале?"; - uReportMsgTxt += "<br/><br/><b>Причина жалобы</b>: <input type='text' id='uReportMsgInput' placeholder='Причина' />" + uReportMsgTxt = tr("going_to_report_post"); + uReportMsgTxt += "<br/>"+tr("report_question_text"); + uReportMsgTxt += "<br/><br/><b>"+tr("report_reason")+"</b>: <input type='text' id='uReportMsgInput' placeholder='" + tr("reason") + "' />" - MessageBox("Пожаловаться?", uReportMsgTxt, ["Подтвердить", "Отмена"], [ + MessageBox(tr("report_question"), uReportMsgTxt, [tr("confirm_m"), tr("cancel")], [ (function() { res = document.querySelector("#uReportMsgInput").value; xhr = new XMLHttpRequest(); xhr.open("GET", "/report/" + {$post->getId()} + "?reason=" + res + "&type=post", true); xhr.onload = (function() { if(xhr.responseText.indexOf("reason") === -1) - MessageBox("Ошибка", "Не удалось подать жалобу...", ["OK"], [Function.noop]); + MessageBox(tr("error"), tr("error_sending_report"), ["OK"], [Function.noop]); else - MessageBox("Операция успешна", "Скоро её рассмотрят модераторы", ["OK"], [Function.noop]); + MessageBox(tr("action_successfully"), tr("will_be_watched"), ["OK"], [Function.noop]); }); xhr.send(null); }), diff --git a/Web/Presenters/templates/components/comment.xml b/Web/Presenters/templates/components/comment.xml index 69238417..9be7d838 100644 --- a/Web/Presenters/templates/components/comment.xml +++ b/Web/Presenters/templates/components/comment.xml @@ -43,7 +43,7 @@ <a class="comment-reply">{_reply}</a> {if $thisUser->getId() != $comment->getOwner()->getId()} {var $canReport = true} - | <a href="javascript:reportComment()">Пожаловаться</a> + | <a href="javascript:reportComment()">{_report}</a> {/if} <div style="float: right; font-size: .7rem;"> <a class="post-like-button" href="/comment{$comment->getId()}/like?hash={rawurlencode($csrfToken)}"> @@ -87,20 +87,20 @@ </table> <script n:if="$canReport ?? false"> function reportComment() { - uReportMsgTxt = "Вы собираетесь пожаловаться на данный комментарий."; - uReportMsgTxt += "<br/>Что именно вам кажется недопустимым в этом материале?"; - uReportMsgTxt += "<br/><br/><b>Причина жалобы</b>: <input type='text' id='uReportMsgInput' placeholder='Причина' />" + uReportMsgTxt = tr("going_to_report_comment"); + uReportMsgTxt += "<br/>"+tr("report_question_text"); + uReportMsgTxt += "<br/><br/><b>"+tr("report_reason")+"</b>: <input type='text' id='uReportMsgInput' placeholder='" + tr("reason") + "' />" - MessageBox("Пожаловаться?", uReportMsgTxt, ["Подтвердить", "Отмена"], [ + MessageBox(tr("report_question"), uReportMsgTxt, [tr("confirm_m"), tr("cancel")], [ (function() { res = document.querySelector("#uReportMsgInput").value; xhr = new XMLHttpRequest(); xhr.open("GET", "/report/" + {$comment->getId()} + "?reason=" + res + "&type=comment", true); xhr.onload = (function() { if(xhr.responseText.indexOf("reason") === -1) - MessageBox("Ошибка", "Не удалось подать жалобу...", ["OK"], [Function.noop]); + MessageBox(tr("error"), tr("error_sending_report"), ["OK"], [Function.noop]); else - MessageBox("Операция успешна", "Скоро её рассмотрят модераторы", ["OK"], [Function.noop]); + MessageBox(tr("action_successfully"), tr("will_be_watched"), ["OK"], [Function.noop]); }); xhr.send(null); }), diff --git a/Web/Presenters/templates/components/notifications/9601/_20_18_.xml b/Web/Presenters/templates/components/notifications/9601/_20_18_.xml index 08a80a54..87eff8a3 100644 --- a/Web/Presenters/templates/components/notifications/9601/_20_18_.xml +++ b/Web/Presenters/templates/components/notifications/9601/_20_18_.xml @@ -1,7 +1,7 @@ {var $gift = $notification->getModel(0)} {var $sender = $notification->getModel(1)} -<a href="{$sender->getURL()}"><b>{$sender->getCanonicalName()}</b></a> отправил вам подарок. +<a href="{$sender->getURL()}"><b>{$sender->getCanonicalName()}</b></a> {_nt_sent_gift}. <div class="nobold"> {$notification->getDateTime()} </div> \ No newline at end of file diff --git a/Web/Presenters/templates/components/post/microblogpost.xml b/Web/Presenters/templates/components/post/microblogpost.xml index 6b0444c7..bc499818 100644 --- a/Web/Presenters/templates/components/post/microblogpost.xml +++ b/Web/Presenters/templates/components/post/microblogpost.xml @@ -82,7 +82,7 @@ </div> <div n:if="$post->isAd()" style="color:grey;"> <br/> - ! Этот пост был размещён за взятку. + ! {_post_is_ad} </div> <div n:if="$post->isSigned()" class="post-signature"> {var $actualAuthor = $post->getOwner(false)} diff --git a/Web/Presenters/templates/components/post/oldpost.xml b/Web/Presenters/templates/components/post/oldpost.xml index 6e0b4c53..9dadeaab 100644 --- a/Web/Presenters/templates/components/post/oldpost.xml +++ b/Web/Presenters/templates/components/post/oldpost.xml @@ -73,7 +73,7 @@ </div> <div n:if="$post->isAd()" style="color:grey;"> <br/> - ! Этот пост был размещён за взятку. + ! {_post_is_ad} </div> <div n:if="$post->isSigned()" class="post-signature"> {var $actualAuthor = $post->getOwner(false)} diff --git a/locales/en.strings b/locales/en.strings index f6e325d5..f09ee65e 100644 --- a/locales/en.strings +++ b/locales/en.strings @@ -149,6 +149,10 @@ "user_banned" = "Unfortunately, we had to block the <b>$1</b> user page."; "user_banned_comment" = "Moderator's comment:"; +"verified_page" = "Verified page"; +"user_is_blocked" = "User is blocked"; +"before" = "before"; +"forever" = "forever"; /* Wall */ @@ -213,6 +217,7 @@ "graffiti" = "Graffiti"; "reply" = "Reply"; +"post_is_ad" = "This post is sponsored."; "edited_short" = "edited"; @@ -339,9 +344,14 @@ "create" = "Create"; "albums" = "Albums"; +"album" = "Album"; +"photos" = "photos"; "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"; @@ -436,6 +446,8 @@ "notes_closed" = "You can't attach note to post, because only you can see them.<br> You can change it in <a href=\"/settings?act=privacy\">settings</a>."; "do_not_attach_note" = "Do not attach note"; +"something" = "Something"; +"supports_xhtml" = "from (X)HTML supported."; /* Notes: Article Viewer */ "aw_legacy_ui" = "Legacy interface"; @@ -651,6 +663,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 <b>10 more codes</b>, 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 */ @@ -677,6 +692,8 @@ "videos_other" = "$1 videos"; "view_video" = "View"; +"change_video" = "Change video"; +"unknown_video" = "This video is not supported in your version of OpenVK."; /* Notifications */ @@ -716,6 +733,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 */ @@ -930,6 +948,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"; @@ -1139,6 +1171,96 @@ "media_file_corrupted_or_too_large" = "The media content file is corrupted or too large."; "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 <b>DELETED</b>."; +"error_adding_to_x" = "Failed to save photo to <b>$1</b>."; +"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 */ @@ -1147,6 +1269,8 @@ "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"; @@ -1157,6 +1281,7 @@ "admin" = "Admin panel"; +"sandbox_for_developers" = "Sandbox for developers"; "admin_ownerid" = "Owner ID"; "admin_author" = "Author"; "admin_name" = "Name"; @@ -1242,6 +1367,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 <b>Recycle Bin</b>."; + +"logs" = "Logs"; +"logs_anything" = "Anything"; "logs_adding" = "Creation"; "logs_editing" = "Editing"; "logs_removing" = "Deletion"; @@ -1250,6 +1380,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) */ @@ -1297,6 +1449,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 */ @@ -1310,6 +1464,8 @@ /* Away */ +"transition_is_blocked" = "Transition is blocked"; +"caution" = "Caution"; "url_is_banned" = "Link is not allowed"; "url_is_banned_comment" = "The <b>$1</b> administration recommends not to follow this link."; "url_is_banned_comment_r" = "The <b>$1</b> administration recommends not to follow this link.<br><br>The reason is: <b>$2</b>"; @@ -1574,6 +1730,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"; @@ -1589,3 +1780,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 b66a5404..787f1648 100644 --- a/locales/ru.strings +++ b/locales/ru.strings @@ -132,6 +132,10 @@ "updated_at" = "Обновлено $1"; "user_banned" = "К сожалению, нам пришлось заблокировать страницу пользователя <b>$1</b>."; "user_banned_comment" = "Комментарий модератора:"; +"verified_page" = "Подтверждённая страница"; +"user_is_blocked" = "Пользователь заблокирован"; +"before" = "до"; +"forever" = "навсегда"; /* Wall */ @@ -191,6 +195,7 @@ "version_incompatibility" = "Не удалось отобразить это вложение. Возможно, база данных несовместима с текущей версией OpenVK."; "graffiti" = "Граффити"; "reply" = "Ответить"; +"post_is_ad" = "Этот пост был размещён за взятку."; "edited_short" = "ред."; /* Friends */ @@ -320,10 +325,15 @@ /* Albums */ "create" = "Создать"; +"album" = "Альбом"; "albums" = "Альбомы"; +"photos" = "фотографий"; "create_album" = "Создать альбом"; "edit_album" = "Редактировать альбом"; +"edit_photo" = "Изменить фотографию"; "creating_album" = "Создание альбома"; +"delete_photo" = "Удалить фотографию"; +"sure_deleting_photo" = "Вы уверены, что хотите удалить эту фотографию?"; "upload_photo" = "Загрузить фотографию"; "photo" = "Фотография"; "upload_button" = "Загрузить"; @@ -419,6 +429,8 @@ "notes_closed" = "Вы не можете прикрепить заметку к записи, так как ваши заметки видны только вам.<br><br> Вы можете поменять это в <a href=\"/settings?act=privacy\">настройках</a>."; "do_not_attach_note" = "Не прикреплять заметку"; +"something" = "Кое-что"; +"supports_xhtml" = "из (X)HTML поддерживается."; /* Notes: Article Viewer */ "aw_legacy_ui" = "Старый интерфейс"; @@ -609,6 +621,9 @@ "two_factor_authentication_backup_codes_1" = "Резервные коды позволяют подтверждать вход, когда у вас нет доступа к телефону, например, в путешествии."; "two_factor_authentication_backup_codes_2" = "У вас есть ещё <b>10 кодов</b>, каждым кодом можно воспользоваться только один раз. Распечатайте их, уберите в надежное место и используйте, когда потребуются коды для подтверждения входа."; "two_factor_authentication_backup_codes_3" = "Вы можете получить новые коды, если они заканчиваются. Действительны только последние созданные резервные коды."; +"viewing_backup_codes" = "Просмотр резервных кодов"; +"disable_2fa" = "Отключить 2FA"; +"viewing" = "Просмотреть"; /* Sorting */ @@ -634,6 +649,8 @@ "videos_many" = "$1 видеозаписей"; "videos_other" = "$1 видеозаписей"; "view_video" = "Просмотр"; +"change_video" = "Изменить видеозапись"; +"unknown_video" = "Эта видеозапись не поддерживается в вашей версии OpenVK."; /* Notifications */ @@ -670,6 +687,7 @@ "nt_mention_in_video" = "в обсуждении видеозаписи"; "nt_mention_in_note" = "в обсуждении заметки"; "nt_mention_in_topic" = "в обсуждении"; +"nt_sent_gift" = "отправил вам подарок"; /* Time */ @@ -863,6 +881,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" = "Автор"; @@ -1038,6 +1070,93 @@ "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" = "Вы отправили подарок <b>$1</b> за $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" = "Не удалось сохранить фотографию в <b>DELETED</b>."; +"error_adding_to_x" = "Не удалось сохранить фотографию в <b>$1</b>."; +"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 */ @@ -1046,6 +1165,8 @@ "manage_user_action" = "Управление пользователем"; "manage_group_action" = "Управление группой"; "ban_user_action" = "Заблокировать пользователя"; +"blocks" = "Блокировки"; +"last_actions" = "Последние действия"; "unban_user_action" = "Разблокировать пользователя"; "warn_user_action" = "Предупредить пользователя"; "ban_in_support_user_action" = "Заблокировать в поддержке"; @@ -1055,6 +1176,7 @@ /* Admin panel */ "admin" = "Админ-панель"; +"sandbox_for_developers" = "Sandbox для разработчиков"; "admin_ownerid" = "ID владельца"; "admin_author" = "Автор"; "admin_name" = "Имя"; @@ -1129,6 +1251,12 @@ "admin_banned_link_initiator" = "Инициатор"; "admin_banned_link_not_specified" = "Ссылка не указана"; "admin_banned_link_not_found" = "Ссылка не найдена"; + +"admin_gift_moved_successfully" = "Подарок успешно перемещён"; +"admin_gift_moved_to_recycle" = "Теперь подарок находится в <b>корзине</b>."; + +"logs" = "Логи"; +"logs_anything" = "Любое"; "logs_adding" = "Создание"; "logs_editing" = "Редактирование"; "logs_removing" = "Удаление"; @@ -1137,6 +1265,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) */ @@ -1186,6 +1336,8 @@ "close" = "Закрыть"; "warning" = "Внимание"; "question_confirm" = "Это действие нельзя отменить. Вы действительно уверены в том что хотите сделать?"; +"confirm_m" = "Подтвердить"; +"action_successfully" = "Операция успешна"; /* User alerts */ @@ -1199,6 +1351,8 @@ /* Away */ +"transition_is_blocked" = "Переход по ссылке заблокирован"; +"caution" = "Предупреждение"; "url_is_banned" = "Переход невозможен"; "url_is_banned_comment" = "Администрация <b>$1</b> не рекомендует переходить по этой ссылке."; "url_is_banned_comment_r" = "Администрация <b>$1</b> не рекомендует переходить по этой ссылке.<br><br>Причина: <b>$2</b>"; @@ -1465,6 +1619,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" = "Фотографии"; @@ -1480,3 +1669,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" = "откачено"; From 43de40a0dc62c4cc29cdaad6192dac31ffa8f2e9 Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Sun, 17 Sep 2023 19:19:25 +0300 Subject: [PATCH 17/26] Add Video Picker (#981) --- ServiceAPI/Wall.php | 44 ++++- VKAPI/Handlers/Wall.php | 27 ++- Web/Presenters/CommentPresenter.php | 31 +++- Web/Presenters/WallPresenter.php | 37 +++-- .../templates/components/textArea.xml | 8 +- Web/static/css/main.css | 34 ++++ Web/static/img/video.png | Bin 0 -> 510 bytes Web/static/js/al_wall.js | 154 ++++++++++++++++++ Web/static/js/messagebox.js | 2 + locales/en.strings | 8 + locales/ru.strings | 7 + 11 files changed, 316 insertions(+), 36 deletions(-) create mode 100644 Web/static/img/video.png 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 d52dfce1..6b78a0b0 100644 --- a/VKAPI/Handlers/Wall.php +++ b/VKAPI/Handlers/Wall.php @@ -463,28 +463,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); } @@ -678,7 +675,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(); @@ -736,16 +733,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/Presenters/CommentPresenter.php b/Web/Presenters/CommentPresenter.php index 29c54c78..b68c7d11 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}; final class CommentPresenter extends OpenVKPresenter { @@ -73,7 +73,6 @@ final class CommentPresenter extends OpenVKPresenter # 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) @@ -81,13 +80,28 @@ final class CommentPresenter extends OpenVKPresenter $photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"], $album); } - - if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK) { - $video = Video::fastMake($this->user->id, $_FILES["_vid_attachment"]["name"], $this->postParam("text"), $_FILES["_vid_attachment"]); - } } catch(ISE $ex) { $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_file_too_big")); } + + $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) $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_empty")); @@ -108,8 +122,9 @@ final class CommentPresenter extends OpenVKPresenter if(!is_null($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) diff --git a/Web/Presenters/WallPresenter.php b/Web/Presenters/WallPresenter.php index 32ac421e..ef9e4689 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, Comments}; +use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums, Notes, Videos, Comments}; 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"), tr("video_uploads_disabled")); - + $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); @@ -263,8 +260,8 @@ final class WallPresenter extends OpenVKPresenter $photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"], $album, $anon); } - 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); + /*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) { @@ -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")) && !$photo && sizeof($videos) < 1 && !$poll && !$note) $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big")); try { @@ -316,8 +332,9 @@ final class WallPresenter extends OpenVKPresenter if(!is_null($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); diff --git a/Web/Presenters/templates/components/textArea.xml b/Web/Presenters/templates/components/textArea.xml index f76649d6..939e5ad5 100644 --- a/Web/Presenters/templates/components/textArea.xml +++ b/Web/Presenters/templates/components/textArea.xml @@ -17,6 +17,8 @@ <div class="post-has-note"> </div> + <div class="post-has-videos"></div> + <div n:if="$postOpts ?? true" class="post-opts"> {var $anonEnabled = OPENVK_ROOT_CONF['openvk']['preferences']['wall']['anonymousPosting']['enable']} {if !is_null($thisUser) && !is_null($club ?? NULL) && $owner < 0} @@ -55,7 +57,7 @@ </label> </div> <input type="file" class="postFileSel" id="postFilePic" name="_pic_attachment" accept="image/*" style="display:none;" /> - <input n:if="!OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading']" type="file" class="postFileSel" id="postFileVid" name="_vid_attachment" accept="video/*" style="display:none;" /> + <input type="hidden" name="videos" value="" /> <input type="hidden" name="poll" value="none" /> <input type="hidden" id="note" name="note" value="none" /> <input type="hidden" name="type" value="1" /> @@ -75,7 +77,7 @@ <img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/mimetypes/application-x-egon.png" /> {_photo} </a> - <a n:if="!OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading']" href="javascript:void(document.querySelector('#post-buttons{$textAreaId} input[name=_vid_attachment]').click());"> + <a id="videoAttachment"> <img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/mimetypes/application-vnd.rn-realmedia.png" /> {_video} </a> @@ -105,6 +107,8 @@ setupWallPostInputHandlers({$textAreaId}); }); + + u("#post-buttons{$textAreaId} input[name='videos']")["nodes"].at(0).value = "" </script> {if $graffiti} diff --git a/Web/static/css/main.css b/Web/static/css/main.css index 46ad74b0..caa49283 100644 --- a/Web/static/css/main.css +++ b/Web/static/css/main.css @@ -1466,6 +1466,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 +1483,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; } @@ -2702,6 +2730,12 @@ body.article .floating_sidebar, body.article .page_content { font-size: 12px; } +.topGrayBlock { + background: #F0F0F0; + height: 37px; + border-bottom: 1px solid #C7C7C7; +} + .edited { color: #9b9b9b; } diff --git a/Web/static/img/video.png b/Web/static/img/video.png new file mode 100644 index 0000000000000000000000000000000000000000..5c115f1c231030eafacd5f18a415232bbd0a6d12 GIT binary patch literal 510 zcmV<a0RjGrP)<h;3K|Lk000e1NJLTq000gE000jN1^@s6)D-@700001b5ch_0Itp) z=>Px#1ZP1_K>z@;j|==^1poj532;bRa{vGf6951U69E94oEQKA0h38YK~y+Tjgw7J z!axv)-`PTo2n79q#OQ%&VxkZYiJ$nHATdGIC?tM`qX#|kmyF>cD72*o>e#70TJ^b1 zHt+7dGqbyNb9>K;=>#T|2{cUu!-KBHf;B!FL<Bt^2Q#DB{Xlkh2A<~uk5A7GpPPsK zApS<X-46CN8Vy$cejgXtS1fLS@$&MDX0yr3WD?4;Y%DJ>k!hNt9F6jm=ks||N<o1= z+&$d#lo1c*Xr#|ptHqs82XYwma|_h#y@hf#L)%0ym!oVp3pE;zaCv#onM@{>qlp_j z>h(Iit_!MEDqJd+s8T&)v3JK~q*5u^whfOzgOZpDu~-ZmDXbS(saC5oom5X*?n<Uw zEUZwN{70&1XY5)g61o8;nby~eeviFSj>fVqY;A0kL^T);P~JQEUyuF^D(~;HuIo^S zVW7ObO=G{ezv#bzpuDq7G8`qg;@ItWLpd6m#m3g=x8ra)3>?$xbSOt713W%FB598b z#Wj}NrDQ2reuX5lT_@Nq*CD?_q6UH@hT}N+0?diKEB)@BE&u=k07*qoM6N<$f@P%F A4FCWD literal 0 HcmV?d00001 diff --git a/Web/static/js/al_wall.js b/Web/static/js/al_wall.js index 4c8bb933..ef3d5dba 100644 --- a/Web/static/js/al_wall.js +++ b/Web/static/js/al_wall.js @@ -264,6 +264,160 @@ async function showArticle(note_id) { u("body").addClass("article"); } +$(document).on("click", "#videoAttachment", async (e) => { + e.preventDefault() + + let body = ` + <div class="topGrayBlock"> + <div style="padding-top: 11px;padding-left: 12px;"> + <a href="/videos/upload">${tr("upload_new_video")}</a> + <input type="text" id="vquery" maxlength="20" placeholder="${tr("header_search")}" style="float: right;width: 160px;margin-right: 17px;margin-top: -2px;"> + </div> + </div> + + <div class="videosInsert" style="padding: 5px;height: 287px;overflow-y: scroll;"></div> + ` + + 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", `<img id="loader" src="/assets/packages/static/openvk/img/loading_mini.gif">`) + + 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 = `<span>${noVideosText}</span>` + } + + 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", ` + <div class="content" style="padding: unset;"> + <table> + <tbody> + <tr> + <td valign="top"> + <a href="/video${vid.video.owner_id}_${vid.video.id}"> + <div class="video-preview" style="height: 75px;width: 133px;overflow: hidden;"> + <img src="${vid.video.image[0].url}" alt="${escapeHtml(vid.video.title)}" style="max-width: 133px; height: 75px; margin: auto;"> + </div> + </a> + </td> + <td valign="top" style="width: 100%"> + <a href="/video${vid.video.owner_id}_${vid.video.id}"> + <b> + ${ovk_proc_strtr(escapeHtml(vid.video.title), 30)} + </b> + </a> + <br> + <p> + <span>${ovk_proc_strtr(escapeHtml(vid.video.description ?? ""), 140)}</span> + </p> + <span><a href="/id${vid.video.owner_id}" target="_blank">${escapeHtml(vid.video.author_name ?? "")}</a></span> + </td> + <td valign="top" class="action_links" style="width: 150px;"> + <a class="profile_link" id="attachvid" data-name="${escapeHtml(vid.video.title)}" data-attachmentData="${vid.video.owner_id}_${vid.video.id}">${!isAttached ? tr("attach") : tr("detach")}</a> + </td> + </tr> + </tbody> + </table> + </div> + `) + } + + if(page < pagesCount) { + document.querySelector(".videosInsert").insertAdjacentHTML("beforeend", ` + <div id="showMoreVideos" data-pagesCount="${pagesCount}" data-page="${page + 1}" style="width: 100%;text-align: center;background: #d5d5d5;height: 22px;padding-top: 9px;cursor:pointer;"> + <span>more...</span> + </div>`) + } + } + + $(".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", ` + <div class="post-has-video" id="unattachVideo" data-id="${ev.currentTarget.dataset.attachmentdata}"> + <span>${tr("video")} <b>"${ovk_proc_strtr(escapeHtml(ev.currentTarget.dataset.name), 20)}"</b></span> + </div> + `) + + 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") diff --git a/Web/static/js/messagebox.js b/Web/static/js/messagebox.js index 45791fd3..368311dd 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( `<div class="ovk-diag-cont"> <div class="ovk-diag"> @@ -21,6 +22,7 @@ function MessageBox(title, body, buttons, callbacks) { let __closeDialog = () => { u("body").removeClass("dimmed"); u(".ovk-diag-cont").remove(); + document.querySelector("html").style.overflowY = "scroll" }; Reflect.apply(callbacks[callback], { diff --git a/locales/en.strings b/locales/en.strings index f09ee65e..22cee453 100644 --- a/locales/en.strings +++ b/locales/en.strings @@ -206,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"; @@ -692,6 +693,13 @@ "videos_other" = "$1 videos"; "view_video" = "View"; + +"selecting_video" = "Selecting videos"; +"upload_new_video" = "Upload new video"; +"max_attached_videos" = "Max is 10 videos"; +"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."; diff --git a/locales/ru.strings b/locales/ru.strings index 787f1648..2dfadb8f 100644 --- a/locales/ru.strings +++ b/locales/ru.strings @@ -186,6 +186,7 @@ "nsfw_warning" = "Данный пост может содержать 18+ контент"; "report" = "Пожаловаться"; "attach" = "Прикрепить"; +"detach" = "Открепить"; "attach_photo" = "Прикрепить фото"; "attach_video" = "Прикрепить видео"; "draw_graffiti" = "Нарисовать граффити"; @@ -652,6 +653,12 @@ "change_video" = "Изменить видеозапись"; "unknown_video" = "Эта видеозапись не поддерживается в вашей версии OpenVK."; +"selecting_video" = "Выбор видеозаписей"; +"upload_new_video" = "Загрузить новое видео"; +"max_attached_videos" = "Максимум 10 видеозаписей"; +"no_videos" = "У вас нет видео."; +"no_videos_results" = "Нет результатов."; + /* Notifications */ "feedback" = "Ответы"; From cc5a56917b57b6b92a54471ed7d47e452e042956 Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Mon, 18 Sep 2023 18:09:25 +0300 Subject: [PATCH 18/26] fix gifts pagination (#984) --- Web/Models/Repositories/Gifts.php | 6 ++++++ Web/Presenters/GiftsPresenter.php | 1 + 2 files changed, 7 insertions(+) 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/Presenters/GiftsPresenter.php b/Web/Presenters/GiftsPresenter.php index 71480540..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"; } From edf10c424856ae2651ccd4940a8379eb6bbf2726 Mon Sep 17 00:00:00 2001 From: Alexander Minkin <weryskok@gmail.com> Date: Fri, 22 Sep 2023 23:56:18 +0300 Subject: [PATCH 19/26] Docker: add imagick to dependencies I'm not sure if that's all we need to make it work, but I will solve this issue step by step --- install/automated/docker/base-php-apache.Dockerfile | 1 + 1 file changed, 1 insertion(+) 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 From 8483a2d343ad30dad61a908acd98a4605ee4d50a Mon Sep 17 00:00:00 2001 From: Alexander Minkin <weryskok@gmail.com> Date: Sat, 23 Sep 2023 01:10:03 +0300 Subject: [PATCH 20/26] SQL: fix all `support_names`-related migrations This solves some problems in Docker instance --- install/sqls/00002-support-aliases.sql | 8 +++++++- install/sqls/00032-agent-card.sql | 10 ++-------- install/sqls/00037-agent-card-profilefix.sql | 2 -- 3 files changed, 9 insertions(+), 11 deletions(-) delete mode 100644 install/sqls/00037-agent-card-profilefix.sql 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/00032-agent-card.sql b/install/sqls/00032-agent-card.sql index a8354c80..0f82460e 100644 --- a/install/sqls/00032-agent-card.sql +++ b/install/sqls/00032-agent-card.sql @@ -1,14 +1,8 @@ -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 `id` bigint(20) UNSIGNED NOT NULL FIRST; ALTER TABLE `support_names` - ADD PRIMARY KEY (`id`); + ADD UNIQUE KEY `id` (`id`); ALTER TABLE `support_names` MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT; 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; From 75ce995df5200dcda4c57813a709b1d4c4f63c3d Mon Sep 17 00:00:00 2001 From: Alexander Minkin <weryskok@gmail.com> Date: Sat, 23 Sep 2023 01:11:00 +0300 Subject: [PATCH 21/26] Docker: add KAFKA_CFG_NODE_ID to docker-compose --- install/automated/docker/docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/install/automated/docker/docker-compose.yml b/install/automated/docker/docker-compose.yml index cbd697a4..61ee3bac 100644 --- a/install/automated/docker/docker-compose.yml +++ b/install/automated/docker/docker-compose.yml @@ -76,6 +76,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 From 5710d131fd49b4b60dbfd72bb9af41d0b818d942 Mon Sep 17 00:00:00 2001 From: Alexander Minkin <weryskok@gmail.com> Date: Sat, 23 Sep 2023 01:18:33 +0300 Subject: [PATCH 22/26] SQL: Reorder migration files The issue was that numbers were duplicating, so I decided to fix them --- install/sqls/{00018-reports.sql => 00019-reports.sql} | 0 .../{00019-block-in-support.sql => 00020-block-in-support.sql} | 0 install/sqls/{00020-image-sizes.sql => 00021-image-sizes.sql} | 0 .../{00021-video-processing.sql => 00022-video-processing.sql} | 0 install/sqls/{00022-group-alerts.sql => 00023-group-alerts.sql} | 0 install/sqls/{00023-email-change.sql => 00024-email-change.sql} | 0 .../{00024-main-page-setting.sql => 00025-main-page-setting.sql} | 0 .../{00025-toncoin-fetching.sql => 00026-toncoin-fetching.sql} | 0 .../{00026-better-birthdays.sql => 00027-better-birthdays.sql} | 0 install/sqls/{00027-rating.sql => 00028-rating.sql} | 0 install/sqls/{00028-deactivation.sql => 00029-deactivation.sql} | 0 .../sqls/{00029-hashtag-search.sql => 00030-hashtag-search.sql} | 0 install/sqls/{00030-apps.sql => 00031-apps.sql} | 0 .../sqls/{00031-ban-page-until.sql => 00032-ban-page-until.sql} | 0 install/sqls/{00032-agent-card.sql => 00033-agent-card.sql} | 0 install/sqls/{00032-banned-urls.sql => 00034-banned-urls.sql} | 0 .../sqls/{00032-better-reports.sql => 00035-better-reports.sql} | 0 .../{00033-shortcode-aliases.sql => 00036-shortcode-aliases.sql} | 0 install/sqls/{00034-polls.sql => 00037-polls.sql} | 0 install/sqls/{00035-backdrops.sql => 00038-backdrops.sql} | 0 install/sqls/{00036-platforms.sql => 00039-platforms.sql} | 0 .../{00038-noSpam-templates.sql => 00040-noSpam-templates.sql} | 0 22 files changed, 0 insertions(+), 0 deletions(-) rename install/sqls/{00018-reports.sql => 00019-reports.sql} (100%) rename install/sqls/{00019-block-in-support.sql => 00020-block-in-support.sql} (100%) rename install/sqls/{00020-image-sizes.sql => 00021-image-sizes.sql} (100%) rename install/sqls/{00021-video-processing.sql => 00022-video-processing.sql} (100%) rename install/sqls/{00022-group-alerts.sql => 00023-group-alerts.sql} (100%) rename install/sqls/{00023-email-change.sql => 00024-email-change.sql} (100%) rename install/sqls/{00024-main-page-setting.sql => 00025-main-page-setting.sql} (100%) rename install/sqls/{00025-toncoin-fetching.sql => 00026-toncoin-fetching.sql} (100%) rename install/sqls/{00026-better-birthdays.sql => 00027-better-birthdays.sql} (100%) rename install/sqls/{00027-rating.sql => 00028-rating.sql} (100%) rename install/sqls/{00028-deactivation.sql => 00029-deactivation.sql} (100%) rename install/sqls/{00029-hashtag-search.sql => 00030-hashtag-search.sql} (100%) rename install/sqls/{00030-apps.sql => 00031-apps.sql} (100%) rename install/sqls/{00031-ban-page-until.sql => 00032-ban-page-until.sql} (100%) rename install/sqls/{00032-agent-card.sql => 00033-agent-card.sql} (100%) rename install/sqls/{00032-banned-urls.sql => 00034-banned-urls.sql} (100%) rename install/sqls/{00032-better-reports.sql => 00035-better-reports.sql} (100%) rename install/sqls/{00033-shortcode-aliases.sql => 00036-shortcode-aliases.sql} (100%) rename install/sqls/{00034-polls.sql => 00037-polls.sql} (100%) rename install/sqls/{00035-backdrops.sql => 00038-backdrops.sql} (100%) rename install/sqls/{00036-platforms.sql => 00039-platforms.sql} (100%) rename install/sqls/{00038-noSpam-templates.sql => 00040-noSpam-templates.sql} (100%) 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/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/00032-agent-card.sql b/install/sqls/00033-agent-card.sql similarity index 100% rename from install/sqls/00032-agent-card.sql rename to install/sqls/00033-agent-card.sql 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/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 From 569a8e8bee506d4e422fece04d7e1ff5d87d317c Mon Sep 17 00:00:00 2001 From: Vladimir Barinov <veselcraft@icloud.com> Date: Fri, 29 Sep 2023 18:47:53 +0300 Subject: [PATCH 23/26] repositories/logs does not exist --- Web/Presenters/AuthPresenter.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 @@ <?php declare(strict_types=1); namespace openvk\Web\Presenters; use openvk\Web\Models\Entities\{IP, User, PasswordReset, EmailVerification}; -use openvk\Web\Models\Repositories\{Bans, IPs, Users, Restores, Verifications, Logs}; +use openvk\Web\Models\Repositories\{Bans, IPs, Users, Restores, Verifications}; use openvk\Web\Models\Exceptions\InvalidUserNameException; use openvk\Web\Util\Validator; use Chandler\Session\Session; @@ -130,7 +130,6 @@ final class AuthPresenter extends OpenVKPresenter } $this->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(); } From db8e9d183f46d9a4fc0b37d37a60e90e89365169 Mon Sep 17 00:00:00 2001 From: lalka2016 <99399973+lalka2016@users.noreply.github.com> Date: Mon, 2 Oct 2023 17:24:01 +0300 Subject: [PATCH 24/26] Fix typos in NoSpam --- Web/Presenters/templates/NoSpam/Index.xml | 2 +- Web/Presenters/templates/NoSpam/Templates.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Web/Presenters/templates/NoSpam/Index.xml b/Web/Presenters/templates/NoSpam/Index.xml index 2bf9d1df..0c465af8 100644 --- a/Web/Presenters/templates/NoSpam/Index.xml +++ b/Web/Presenters/templates/NoSpam/Index.xml @@ -106,7 +106,7 @@ <span class="nobold">{_block_params}:</span> </td> <td> - <select name="ban_type" id="noSpam-ban-type" style="width: 140px;" + <select name="ban_type" id="noSpam-ban-type" style="width: 140px;"> <option value="1">{_only_rollback}</option> <option value="2">{_only_block}</option> <option value="3">{_rollback_and_block}</option> diff --git a/Web/Presenters/templates/NoSpam/Templates.xml b/Web/Presenters/templates/NoSpam/Templates.xml index a86df534..e2936b15 100644 --- a/Web/Presenters/templates/NoSpam/Templates.xml +++ b/Web/Presenters/templates/NoSpam/Templates.xml @@ -76,7 +76,7 @@ <img src="/assets/packages/static/openvk/img/loading_mini.gif" style="width: 40px;"> </div> <a n:if="!$template->isRollbacked()" id="noSpam-rollback-template-link-{$template->getId()}" onClick="rollbackTemplate({$template->getId()})">{_roll_back}</a> - <span n:attr="style => $template->isRollbacked() ? '' : 'display: none;'" id="noSpam-rollback-template-rollbacked-{$template->getId()}">{roll_backed}</span> + <span n:attr="style => $template->isRollbacked() ? '' : 'display: none;'" id="noSpam-rollback-template-rollbacked-{$template->getId()}">{_roll_backed}</span> </div> </td> </tr> From 6632d070f5a255a2fc9633b4e917401d1b12bd5f Mon Sep 17 00:00:00 2001 From: Alexander Minkin <weryskok@gmail.com> Date: Tue, 3 Oct 2023 01:49:39 +0300 Subject: [PATCH 25/26] fix(containers): :loud_sound: Set log output to stdout Let the docker handle logs. https://12factor.net/logs motivated me to do this --- install/automated/common/10-openvk.conf | 3 +-- install/automated/docker/acl_handler.sh | 1 - install/automated/docker/docker-compose.yml | 2 -- install/automated/docker/openvk.Dockerfile | 1 - install/automated/kubernetes/manifests/003-deployment.yaml | 5 ----- 5 files changed, 1 insertion(+), 11 deletions(-) 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 </Directory> - ErrorLog /var/log/openvk/error.log - CustomLog /var/log/openvk/access.log combinedio + LogFormat combinedio </VirtualHost> 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/docker-compose.yml b/install/automated/docker/docker-compose.yml index 61ee3bac..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: 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 From a859fa13a59d542b40b83bdd8f502c1590ed364a Mon Sep 17 00:00:00 2001 From: Vladimir Barinov <veselcraft@icloud.com> Date: Tue, 3 Oct 2023 19:40:13 +0300 Subject: [PATCH 26/26] [WIP] Textarea: Upload multiple pictures (#800) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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> --- ServiceAPI/Photos.php | 92 ++++ Web/Models/Entities/Comment.php | 2 +- Web/Models/Entities/IP.php | 2 +- Web/Models/Entities/Media.php | 4 +- Web/Models/Entities/Postable.php | 4 +- .../Entities/Traits/TAttachmentHost.php | 43 +- Web/Models/Repositories/Photos.php | 20 +- Web/Presenters/CommentPresenter.php | 37 +- Web/Presenters/InternalAPIPresenter.php | 38 ++ Web/Presenters/PhotosPresenter.php | 22 +- Web/Presenters/WallPresenter.php | 40 +- Web/Presenters/templates/Photos/Album.xml | 2 +- Web/Presenters/templates/Photos/Photo.xml | 10 +- Web/Presenters/templates/Wall/Feed.xml | 2 + Web/Presenters/templates/Wall/Post.xml | 2 +- .../templates/components/attachment.xml | 5 +- .../templates/components/comment.xml | 13 +- .../templates/components/comments.xml | 2 +- .../components/post/microblogpost.xml | 17 +- .../templates/components/post/oldpost.xml | 15 +- .../templates/components/textArea.xml | 15 +- Web/Util/Makima/Makima.php | 305 +++++++++++ Web/Util/Makima/MasonryLayout.php | 10 + Web/Util/Makima/ThumbTile.php | 14 + Web/routes.yml | 2 + Web/static/css/main.css | 130 ++++- Web/static/js/al_wall.js | 492 +++++++++++++++++- Web/static/js/messagebox.js | 7 +- Web/static/js/openvk.cls.js | 2 +- locales/en.strings | 16 + locales/ru.strings | 16 + 31 files changed, 1268 insertions(+), 113 deletions(-) create mode 100644 ServiceAPI/Photos.php create mode 100644 Web/Util/Makima/Makima.php create mode 100644 Web/Util/Makima/MasonryLayout.php create mode 100644 Web/Util/Makima/ThumbTile.php 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 @@ +<?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); + } +} diff --git a/Web/Models/Entities/Comment.php b/Web/Models/Entities/Comment.php index d64a2763..37b06dda 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 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/Postable.php b/Web/Models/Entities/Postable.php index ffbf480c..8f783238 100644 --- a/Web/Models/Entities/Postable.php +++ b/Web/Models/Entities/Postable.php @@ -152,7 +152,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; @@ -171,7 +171,7 @@ abstract class Postable extends Attachable $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 @@ <?php declare(strict_types=1); 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; trait TAttachmentHost @@ -29,6 +30,46 @@ trait TAttachmentHost yield $repo->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/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/CommentPresenter.php b/Web/Presenters/CommentPresenter.php index b68c7d11..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, Videos}; +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"), tr("video_uploads_disabled")); - $flags = 0; if($this->postParam("as_group") === "on" && !is_null($club) && $club->canBeModifiedBy($this->user->identity)) $flags |= 0b10000000; @@ -70,18 +67,22 @@ final class CommentPresenter extends OpenVKPresenter } } - # TODO move to trait - try { - $photo = 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; + } } - } catch(ISE $ex) { - $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_file_too_big")); } $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")); try { @@ -119,8 +120,8 @@ final class CommentPresenter extends OpenVKPresenter $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(sizeof($videos) > 0) foreach($videos as $vid) 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 @@ <?php declare(strict_types=1); namespace openvk\Web\Presenters; +use openvk\Web\Models\Repositories\{Posts, Comments}; use MessagePack\MessagePack; 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 + ]); + } + } } diff --git a/Web/Presenters/PhotosPresenter.php b/Web/Presenters/PhotosPresenter.php index bef984b7..0a8b87e4 100644 --- a/Web/Presenters/PhotosPresenter.php +++ b/Web/Presenters/PhotosPresenter.php @@ -222,15 +222,20 @@ final class PhotosPresenter extends OpenVKPresenter { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(true); - - if(is_null($this->queryParam("album"))) - $this->flashFail("err", tr("error"), tr("error_adding_to_deleted"), 500, true); - - [$owner, $id] = explode("_", $this->queryParam("album")); - $album = $this->albums->get((int) $id); + + 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", 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); 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); $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; diff --git a/Web/Presenters/WallPresenter.php b/Web/Presenters/WallPresenter.php index ef9e4689..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, Videos, Comments}; +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; @@ -249,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 { @@ -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")); try { @@ -329,8 +329,8 @@ 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(sizeof($videos) > 0) foreach($videos as $vid) diff --git a/Web/Presenters/templates/Photos/Album.xml b/Web/Presenters/templates/Photos/Album.xml index 1bd49a6f..0c919e90 100644 --- a/Web/Presenters/templates/Photos/Album.xml +++ b/Web/Presenters/templates/Photos/Album.xml @@ -41,7 +41,7 @@ </a> <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> </div> {/foreach} diff --git a/Web/Presenters/templates/Photos/Photo.xml b/Web/Presenters/templates/Photos/Photo.xml index bb6f171f..e3ecaf9c 100644 --- a/Web/Presenters/templates/Photos/Photo.xml +++ b/Web/Presenters/templates/Photos/Photo.xml @@ -26,11 +26,11 @@ <hr/> - <div style="width: 100%; min-height: 100px;"> - <div style="float: left; min-height: 100px; width: 70%;"> - {include "../components/comments.xml", comments => $comments, count => $cCount, page => $cPage, model => "photos", parent => $photo} + <div style="width: 100%; min-height: 100px;" class="ovk-photo-details"> + <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, custom_id => 999} </div> - <div style="float: left; min-height: 100px; width: 30%;"> + <div style="float:right;min-height: 100px;width: 30%;margin-left: 1px;"> <div> <h4>{_information}</h4> <span style="color: grey;">{_info_description}:</span> @@ -42,7 +42,7 @@ </div> <br/> <h4>{_actions}</h4> - {if $thisUser->getId() != $photo->getOwner()->getId()} + {if isset($thisUser) && $thisUser->getId() != $photo->getOwner()->getId()} {var canReport = true} {/if} <div n:if="isset($thisUser) && $thisUser->getId() === $photo->getOwner()->getId()"> diff --git a/Web/Presenters/templates/Wall/Feed.xml b/Web/Presenters/templates/Wall/Feed.xml index 5ed1e2fb..44c8cd5b 100644 --- a/Web/Presenters/templates/Wall/Feed.xml +++ b/Web/Presenters/templates/Wall/Feed.xml @@ -6,6 +6,8 @@ {/block} {block content} + {php $GLOBALS["_bigWall"] = 1} + <div class="tabs"> <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> diff --git a/Web/Presenters/templates/Wall/Post.xml b/Web/Presenters/templates/Wall/Post.xml index bd40b6c5..6ce9edd9 100644 --- a/Web/Presenters/templates/Wall/Post.xml +++ b/Web/Presenters/templates/Wall/Post.xml @@ -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="$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%;" class="profile_link" href="/admin/logs?type=1&obj_type=Post&obj_id={$post->getId()}" diff --git a/Web/Presenters/templates/components/attachment.xml b/Web/Presenters/templates/components/attachment.xml index c73e792d..a46be837 100644 --- a/Web/Presenters/templates/components/attachment.xml +++ b/Web/Presenters/templates/components/attachment.xml @@ -1,7 +1,8 @@ {if $attachment instanceof \openvk\Web\Models\Entities\Photo} {if !$attachment->isDeleted()} - <a href="{$attachment->getPageUrl()}"> - <img class="media" src="{$attachment->getURLBySizeId('normal')}" alt="{$attachment->getDescription()}" /> + {var $link = "/photo" . ($attachment->isAnonymous() ? ("s/" . base_convert((string) $attachment->getId(), 10, 32)) : $attachment->getPrettyId())} + <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> {else} <a href="javascript:alert('{_attach_no_longer_available}');"> diff --git a/Web/Presenters/templates/components/comment.xml b/Web/Presenters/templates/components/comment.xml index 9be7d838..461b8307 100644 --- a/Web/Presenters/templates/components/comment.xml +++ b/Web/Presenters/templates/components/comment.xml @@ -22,9 +22,16 @@ <div class="text" id="text{$comment->getId()}"> <span data-text="{$comment->getText(false)}" class="really_text">{$comment->getText()|noescape}</span> - <div n:ifcontent class="attachments_b"> - <div class="attachment" n:foreach="$comment->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}"> - {include "attachment.xml", attachment => $attachment} + {var $attachmentsLayout = $comment->getChildrenWithLayout(288)} + <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 => $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> diff --git a/Web/Presenters/templates/components/comments.xml b/Web/Presenters/templates/components/comments.xml index 3290e35c..0df0c91f 100644 --- a/Web/Presenters/templates/components/comments.xml +++ b/Web/Presenters/templates/components/comments.xml @@ -4,7 +4,7 @@ {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} {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} </div> diff --git a/Web/Presenters/templates/components/post/microblogpost.xml b/Web/Presenters/templates/components/post/microblogpost.xml index bc499818..8dba32f4 100644 --- a/Web/Presenters/templates/components/post/microblogpost.xml +++ b/Web/Presenters/templates/components/post/microblogpost.xml @@ -73,10 +73,21 @@ <div class="post-content" id="{$post->getPrettyId()}"> <div class="text"> <span data-text="{$post->getText(false)}" class="really_text">{$post->getText()|noescape}</span> + + {var $width = ($GLOBALS["_bigWall"] ?? false) ? 550 : 320} + {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_b"> - <div class="attachment" n:foreach="$post->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}"> - {include "../attachment.xml", attachment => $attachment} + <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> diff --git a/Web/Presenters/templates/components/post/oldpost.xml b/Web/Presenters/templates/components/post/oldpost.xml index 9dadeaab..3223e6b7 100644 --- a/Web/Presenters/templates/components/post/oldpost.xml +++ b/Web/Presenters/templates/components/post/oldpost.xml @@ -65,8 +65,19 @@ <span data-text="{$post->getText(false)}" class="really_text">{$post->getText()|noescape}</span> - <div n:ifcontent class="attachments_b"> - <div class="attachment" n:foreach="$post->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}"> + {var $width = ($GLOBALS["_bigWall"] ?? false) ? 550 : 320} + {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} </div> </div> diff --git a/Web/Presenters/templates/components/textArea.xml b/Web/Presenters/templates/components/textArea.xml index 939e5ad5..8727addf 100644 --- a/Web/Presenters/templates/components/textArea.xml +++ b/Web/Presenters/templates/components/textArea.xml @@ -1,5 +1,6 @@ {php if(!isset($GLOBALS["textAreaCtr"])) $GLOBALS["textAreaCtr"] = 10;} {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});"> <form action="{$route}" method="post" enctype="multipart/form-data" style="margin:0;"> @@ -8,8 +9,11 @@ <!-- padding to fix <br/> bug --> </div> <div id="post-buttons{$textAreaId}" style="display: none;"> + <div class="upload"> + + </div> <div class="post-upload"> - {_attachment}: <span>(unknown)</span> + <span style="color: inherit;"></span> </div> <div class="post-has-poll"> {_poll} @@ -56,7 +60,7 @@ <input type="checkbox" name="as_group" /> {_comment_as_group} </label> </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="poll" value="none" /> <input type="hidden" id="note" name="note" value="none" /> @@ -73,7 +77,7 @@ <a class="header" href="javascript:toggleMenu({$textAreaId});"> {_attach} </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" /> {_photo} </a> @@ -101,14 +105,11 @@ <script> $(document).ready(() => { - u("#post-buttons{$textAreaId} .postFileSel").on("change", function() { - handleUpload.bind(this, {$textAreaId})(); - }); - setupWallPostInputHandlers({$textAreaId}); }); u("#post-buttons{$textAreaId} input[name='videos']")["nodes"].at(0).value = "" + u("#post-buttons{$textAreaId} input[name='photos']")["nodes"].at(0).value = "" </script> {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 @@ +<?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; + } +} 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 @@ +<?php declare(strict_types=1); +namespace openvk\Web\Util\Makima; + +class MasonryLayout { + public $colSizes; + public $rowSizes; + public $tiles; + public $width; + public $height; +} diff --git a/Web/Util/Makima/ThumbTile.php b/Web/Util/Makima/ThumbTile.php new file mode 100644 index 00000000..360f280c --- /dev/null +++ b/Web/Util/Makima/ThumbTile.php @@ -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]; + } +} diff --git a/Web/routes.yml b/Web/routes.yml index bc95f44a..dea8ddd5 100644 --- a/Web/routes.yml +++ b/Web/routes.yml @@ -371,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 caa49283..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; @@ -2279,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; diff --git a/Web/static/js/al_wall.js b/Web/static/js/al_wall.js index ef3d5dba..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"), "<div id='ovkDraw'></div>", [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", `<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) { 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( + `<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) { @@ -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) @@ -525,3 +754,210 @@ $(document).on("click", "#editPost", (e) => { 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) + }) +}) diff --git a/Web/static/js/messagebox.js b/Web/static/js/messagebox.js index 368311dd..e56c720f 100644 --- a/Web/static/js/messagebox.js +++ b/Web/static/js/messagebox.js @@ -20,9 +20,12 @@ 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(); - document.querySelector("html").style.overflowY = "scroll" }; Reflect.apply(callbacks[callback], { 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 = "<form id='tmpPhDelF' action='" + u(this).attr("href") + "' >"; formHtml += "<input type='hidden' name='hash' value='" + u("meta[name=csrf]").attr("value") + "' />"; formHtml += "</form>"; diff --git a/locales/en.strings b/locales/en.strings index 22cee453..b1d211d1 100644 --- a/locales/en.strings +++ b/locales/en.strings @@ -347,6 +347,7 @@ "albums" = "Albums"; "album" = "Album"; "photos" = "photos"; +"photo" = "Photo"; "create_album" = "Create album"; "edit_album" = "Edit album"; "edit_photo" = "Edit photo"; @@ -412,6 +413,20 @@ "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 */ @@ -697,6 +712,7 @@ "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."; diff --git a/locales/ru.strings b/locales/ru.strings index 2dfadb8f..49871399 100644 --- a/locales/ru.strings +++ b/locales/ru.strings @@ -329,6 +329,7 @@ "album" = "Альбом"; "albums" = "Альбомы"; "photos" = "фотографий"; +"photo" = "Фотография"; "create_album" = "Создать альбом"; "edit_album" = "Редактировать альбом"; "edit_photo" = "Изменить фотографию"; @@ -394,6 +395,20 @@ "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 */ @@ -656,6 +671,7 @@ "selecting_video" = "Выбор видеозаписей"; "upload_new_video" = "Загрузить новое видео"; "max_attached_videos" = "Максимум 10 видеозаписей"; +"max_attached_photos" = "Максимум 10 фотографий"; "no_videos" = "У вас нет видео."; "no_videos_results" = "Нет результатов.";