From 5c76b56da4b1e207bdf65716f7133759d5407c85 Mon Sep 17 00:00:00 2001 From: n1rwana Date: Fri, 4 Aug 2023 15:10:23 +0300 Subject: [PATCH 001/231] =?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=B8=D0=B5=20=D0=B4=D1=83=D0=B1=D0=BB=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D0=BF=D0=B8=D1=81=D1=87=D0=B8=D0=BA=D0=BE=D0=B2/=D0=BB=D0=B0?= =?UTF-8?q?=D0=B9=D0=BA=D0=BE=D0=B2/=D0=B4=D1=80=D1=83=D0=B7=D0=B5=D0=B9?= =?UTF-8?q?=20(#941)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Web/Models/Entities/Club.php | 2 +- Web/Models/Entities/Postable.php | 2 +- Web/Models/Entities/User.php | 5 ++++- Web/Models/sql/get-followers.tsql | 2 +- Web/Models/sql/get-friends.tsql | 2 +- Web/Models/sql/get-online-friends.tsql | 2 +- Web/Models/sql/get-subscriptions-user.tsql | 2 +- 7 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Web/Models/Entities/Club.php b/Web/Models/Entities/Club.php index 15c458a9..06b2f49b 100644 --- a/Web/Models/Entities/Club.php +++ b/Web/Models/Entities/Club.php @@ -272,7 +272,7 @@ class Club extends RowModel return false; } - return $query; + return $query->group("follower"); } function getFollowersCount(): int diff --git a/Web/Models/Entities/Postable.php b/Web/Models/Entities/Postable.php index b4f8b4c6..b743793f 100644 --- a/Web/Models/Entities/Postable.php +++ b/Web/Models/Entities/Postable.php @@ -84,7 +84,7 @@ abstract class Postable extends Attachable return sizeof(DB::i()->getContext()->table("likes")->where([ "model" => static::class, "target" => $this->getRecord()->id, - ])); + ])->group("origin")); } # TODO add pagination diff --git a/Web/Models/Entities/User.php b/Web/Models/Entities/User.php index 348631e4..d132e015 100644 --- a/Web/Models/Entities/User.php +++ b/Web/Models/Entities/User.php @@ -39,11 +39,14 @@ class User extends RowModel $query = "SELECT id FROM\n" . file_get_contents(__DIR__ . "/../sql/$filename.tsql"); $query .= "\n LIMIT " . $limit . " OFFSET " . ( ($page - 1) * $limit ); + $ids = []; $rels = DatabaseConnection::i()->getConnection()->query($query, $id, $id); foreach($rels as $rel) { $rel = (new Users)->get($rel->id); if(!$rel) continue; - + if(in_array($rel->getId(), $ids)) continue; + $ids[] = $rel->getId(); + yield $rel; } } diff --git a/Web/Models/sql/get-followers.tsql b/Web/Models/sql/get-followers.tsql index ae23d63a..552aafb8 100644 --- a/Web/Models/sql/get-followers.tsql +++ b/Web/Models/sql/get-followers.tsql @@ -1,4 +1,4 @@ - (SELECT follower AS __id FROM + (SELECT DISTINCT(follower) AS __id FROM (SELECT follower FROM subscriptions WHERE target=? AND model="openvk\\Web\\Models\\Entities\\User") u0 LEFT JOIN (SELECT target FROM subscriptions WHERE follower=? AND model="openvk\\Web\\Models\\Entities\\User") u1 diff --git a/Web/Models/sql/get-friends.tsql b/Web/Models/sql/get-friends.tsql index dc7b0730..4831c968 100644 --- a/Web/Models/sql/get-friends.tsql +++ b/Web/Models/sql/get-friends.tsql @@ -1,4 +1,4 @@ - (SELECT follower AS __id FROM + (SELECT DISTINCT(follower) AS __id FROM (SELECT follower FROM subscriptions WHERE target=? AND model="openvk\\Web\\Models\\Entities\\User") u0 INNER JOIN (SELECT target FROM subscriptions WHERE follower=? AND model="openvk\\Web\\Models\\Entities\\User") u1 diff --git a/Web/Models/sql/get-online-friends.tsql b/Web/Models/sql/get-online-friends.tsql index ad646554..7e81c621 100755 --- a/Web/Models/sql/get-online-friends.tsql +++ b/Web/Models/sql/get-online-friends.tsql @@ -1,4 +1,4 @@ - (SELECT follower AS __id FROM + (SELECT DISTINCT(follower) AS __id FROM (SELECT follower FROM subscriptions WHERE target=? AND model="openvk\\Web\\Models\\Entities\\User") u0 INNER JOIN (SELECT target FROM subscriptions WHERE follower=? AND model="openvk\\Web\\Models\\Entities\\User") u1 diff --git a/Web/Models/sql/get-subscriptions-user.tsql b/Web/Models/sql/get-subscriptions-user.tsql index 722db194..ecf8ad79 100644 --- a/Web/Models/sql/get-subscriptions-user.tsql +++ b/Web/Models/sql/get-subscriptions-user.tsql @@ -1,4 +1,4 @@ - (SELECT target AS __id FROM + (SELECT DISTINCT(target) AS __id FROM (SELECT follower FROM subscriptions WHERE target=? AND model="openvk\\Web\\Models\\Entities\\User") u0 RIGHT JOIN (SELECT target FROM subscriptions WHERE follower=? AND model="openvk\\Web\\Models\\Entities\\User") u1 From 4a65096e6473a0bc42469e223f270f343c990e94 Mon Sep 17 00:00:00 2001 From: n1rwana Date: Wed, 9 Aug 2023 15:50:04 +0300 Subject: [PATCH 002/231] Clubs&Albums: Fix of empty names of albums and clubs (#936) --- Web/Presenters/GroupPresenter.php | 4 ++-- Web/Presenters/PhotosPresenter.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Web/Presenters/GroupPresenter.php b/Web/Presenters/GroupPresenter.php index 4f671df3..3e0d8c6e 100644 --- a/Web/Presenters/GroupPresenter.php +++ b/Web/Presenters/GroupPresenter.php @@ -39,7 +39,7 @@ final class GroupPresenter extends OpenVKPresenter $this->willExecuteWriteAction(); if($_SERVER["REQUEST_METHOD"] === "POST") { - if(!empty($this->postParam("name"))) + if(!empty($this->postParam("name")) && mb_strlen(trim($this->postParam("name"))) > 0) { $club = new Club; $club->setName($this->postParam("name")); @@ -201,7 +201,7 @@ final class GroupPresenter extends OpenVKPresenter if(!$club->setShortcode( empty($this->postParam("shortcode")) ? NULL : $this->postParam("shortcode") )) $this->flashFail("err", tr("error"), tr("error_shorturl_incorrect")); - $club->setName(empty($this->postParam("name")) ? $club->getName() : $this->postParam("name")); + $club->setName((empty($this->postParam("name")) || mb_strlen(trim($this->postParam("name"))) === 0) ? $club->getName() : $this->postParam("name")); $club->setAbout(empty($this->postParam("about")) ? NULL : $this->postParam("about")); $club->setWall(empty($this->postParam("wall")) ? 0 : 1); $club->setAdministrators_List_Display(empty($this->postParam("administrators_list_display")) ? 0 : $this->postParam("administrators_list_display")); diff --git a/Web/Presenters/PhotosPresenter.php b/Web/Presenters/PhotosPresenter.php index 02d6ae46..345b2c60 100644 --- a/Web/Presenters/PhotosPresenter.php +++ b/Web/Presenters/PhotosPresenter.php @@ -66,7 +66,7 @@ final class PhotosPresenter extends OpenVKPresenter } if($_SERVER["REQUEST_METHOD"] === "POST") { - if(empty($this->postParam("name"))) + if(empty($this->postParam("name")) || mb_strlen(trim($this->postParam("name"))) === 0) $this->flashFail("err", tr("error"), tr("error_segmentation")); else if(strlen($this->postParam("name")) > 36) $this->flashFail("err", tr("error"), tr("error_data_too_big", "name", 36, "bytes")); @@ -101,7 +101,7 @@ final class PhotosPresenter extends OpenVKPresenter if(strlen($this->postParam("name")) > 36) $this->flashFail("err", tr("error"), tr("error_data_too_big", "name", 36, "bytes")); - $album->setName(empty($this->postParam("name")) ? $album->getName() : $this->postParam("name")); + $album->setName((empty($this->postParam("name")) || mb_strlen(trim($this->postParam("name"))) === 0) ? $album->getName() : $this->postParam("name")); $album->setDescription(empty($this->postParam("desc")) ? NULL : $this->postParam("desc")); $album->setEdited(time()); $album->save(); From 8ca3de8afab964ab69b42eae7d0bf0b901a626f4 Mon Sep 17 00:00:00 2001 From: n1rwana Date: Wed, 9 Aug 2023 16:05:01 +0300 Subject: [PATCH 003/231] Clubs: Group bans fixed (#946) --- Web/Presenters/AdminPresenter.php | 3 +- Web/Presenters/CommentPresenter.php | 10 ++++++- Web/Presenters/GroupPresenter.php | 23 ++++++++++++--- Web/Presenters/WallPresenter.php | 36 +++++++++++++++++------ Web/Presenters/templates/Group/Banned.xml | 22 ++++++++++++++ locales/en.strings | 2 ++ locales/ru.strings | 1 + 7 files changed, 82 insertions(+), 15 deletions(-) create mode 100644 Web/Presenters/templates/Group/Banned.xml diff --git a/Web/Presenters/AdminPresenter.php b/Web/Presenters/AdminPresenter.php index ff21612b..ed1f55eb 100644 --- a/Web/Presenters/AdminPresenter.php +++ b/Web/Presenters/AdminPresenter.php @@ -128,7 +128,8 @@ final class AdminPresenter extends OpenVKPresenter $club->save(); break; case "ban": - $club->setBlock_reason($this->postParam("ban_reason")); + $reason = mb_strlen(trim($this->postParam("ban_reason"))) > 0 ? $this->postParam("ban_reason") : NULL; + $club->setBlock_reason($reason); $club->save(); break; } diff --git a/Web/Presenters/CommentPresenter.php b/Web/Presenters/CommentPresenter.php index dad79ac4..cb0efd0d 100644 --- a/Web/Presenters/CommentPresenter.php +++ b/Web/Presenters/CommentPresenter.php @@ -22,6 +22,9 @@ final class CommentPresenter extends OpenVKPresenter $comment = (new Comments)->get($id); if(!$comment || $comment->isDeleted()) $this->notFound(); + + if ($comment->getTarget() instanceof Post && $comment->getTarget()->getWallOwner()->isBanned()) + $this->flashFail("err", tr("error"), tr("forbidden")); if(!is_null($this->user)) $comment->toggleLike($this->user->identity); @@ -48,6 +51,9 @@ final class CommentPresenter extends OpenVKPresenter else if($entity instanceof Topic) $club = $entity->getClub(); + if ($entity instanceof Post && $entity->getWallOwner()->isBanned()) + $this->flashFail("err", tr("error"), tr("forbidden")); + if($_FILES["_vid_attachment"] && OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading']) $this->flashFail("err", tr("error"), "Video uploads are disabled by the system administrator."); @@ -130,7 +136,9 @@ final class CommentPresenter extends OpenVKPresenter if(!$comment) $this->notFound(); if(!$comment->canBeDeletedBy($this->user->identity)) $this->throwError(403, "Forbidden", "У вас недостаточно прав чтобы редактировать этот ресурс."); - + if ($comment->getTarget() instanceof Post && $comment->getTarget()->getWallOwner()->isBanned()) + $this->flashFail("err", tr("error"), tr("forbidden")); + $comment->delete(); $this->flashFail( "succ", diff --git a/Web/Presenters/GroupPresenter.php b/Web/Presenters/GroupPresenter.php index 3e0d8c6e..d8fbcb79 100644 --- a/Web/Presenters/GroupPresenter.php +++ b/Web/Presenters/GroupPresenter.php @@ -24,10 +24,14 @@ final class GroupPresenter extends OpenVKPresenter if(!$club) { $this->notFound(); } else { - $this->template->albums = (new Albums)->getClubAlbums($club, 1, 3); - $this->template->albumsCount = (new Albums)->getClubAlbumsCount($club); - $this->template->topics = (new Topics)->getLastTopics($club, 3); - $this->template->topicsCount = (new Topics)->getClubTopicsCount($club); + if ($club->isBanned()) { + $this->template->_template = "Group/Banned.xml"; + } else { + $this->template->albums = (new Albums)->getClubAlbums($club, 1, 3); + $this->template->albumsCount = (new Albums)->getClubAlbumsCount($club); + $this->template->topics = (new Topics)->getLastTopics($club, 3); + $this->template->topicsCount = (new Topics)->getClubTopicsCount($club); + } $this->template->club = $club; } @@ -72,6 +76,7 @@ final class GroupPresenter extends OpenVKPresenter $club = $this->clubs->get((int) $this->postParam("id")); if(!$club) exit("Invalid state"); + if ($club->isBanned()) $this->flashFail("err", tr("error"), tr("forbidden")); $club->toggleSubscription($this->user->identity); @@ -83,6 +88,8 @@ final class GroupPresenter extends OpenVKPresenter $this->assertUserLoggedIn(); $this->template->club = $this->clubs->get($id); + if ($this->template->club->isBanned()) $this->flashFail("err", tr("error"), tr("forbidden")); + $this->template->onlyShowManagers = $this->queryParam("onlyAdmins") == "1"; if($this->template->onlyShowManagers) { $this->template->followers = NULL; @@ -118,6 +125,8 @@ final class GroupPresenter extends OpenVKPresenter $this->badRequest(); $club = $this->clubs->get($id); + if ($club->isBanned()) $this->flashFail("err", tr("error"), tr("forbidden")); + $user = (new Users)->get((int) $user); if(!$user || !$club) $this->notFound(); @@ -194,6 +203,8 @@ final class GroupPresenter extends OpenVKPresenter $club = $this->clubs->get($id); if(!$club || !$club->canBeModifiedBy($this->user->identity)) $this->notFound(); + else if ($club->isBanned()) + $this->flashFail("err", tr("error"), tr("forbidden")); else $this->template->club = $club; @@ -255,6 +266,7 @@ final class GroupPresenter extends OpenVKPresenter { $photo = new Photo; $club = $this->clubs->get($id); + if ($club->isBanned()) $this->flashFail("err", tr("error"), tr("forbidden")); if($_SERVER["REQUEST_METHOD"] === "POST" && $_FILES["ava"]["error"] === UPLOAD_ERR_OK) { try { $anon = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"]; @@ -343,6 +355,8 @@ final class GroupPresenter extends OpenVKPresenter $club = $this->clubs->get($id); if(!$club->canBeModifiedBy($this->user->identity)) $this->notFound(); + else if ($club->isBanned()) + $this->flashFail("err", tr("error"), tr("forbidden")); else $this->template->club = $club; @@ -375,6 +389,7 @@ final class GroupPresenter extends OpenVKPresenter $this->flashFail("err", tr("error"), tr("incorrect_password")); $club = $this->clubs->get($id); + if ($club->isBanned()) $this->flashFail("err", tr("error"), tr("forbidden")); $newOwner = (new Users)->get($newOwnerId); if($this->user->id !== $club->getOwner()->getId()) $this->flashFail("err", tr("error"), tr("forbidden")); diff --git a/Web/Presenters/WallPresenter.php b/Web/Presenters/WallPresenter.php index 727101ff..3e115ec7 100644 --- a/Web/Presenters/WallPresenter.php +++ b/Web/Presenters/WallPresenter.php @@ -46,13 +46,13 @@ final class WallPresenter extends OpenVKPresenter function renderWall(int $user, bool $embedded = false): void { $owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user)); + if ($owner->isBanned()) + $this->flashFail("err", tr("error"), tr("forbidden")); + if(is_null($this->user)) { $canPost = false; } else if($user > 0) { - if(!$owner->isBanned()) - $canPost = $owner->getPrivacyPermission("wall.write", $this->user->identity); - else - $this->flashFail("err", tr("error"), tr("forbidden")); + $canPost = $owner->getPrivacyPermission("wall.write", $this->user->identity); } else if($user < 0) { if($owner->canBeModifiedBy($this->user->identity)) $canPost = true; @@ -100,6 +100,8 @@ final class WallPresenter extends OpenVKPresenter } else if($user < 0) { if($owner->canBeModifiedBy($this->user->identity)) $canPost = true; + else if ($owner->isBanned()) + $this->flashFail("err", tr("error"), tr("forbidden")); else $canPost = $owner->canPost(); } else { @@ -212,11 +214,12 @@ final class WallPresenter extends OpenVKPresenter $wallOwner = ($wall > 0 ? (new Users)->get($wall) : (new Clubs)->get($wall * -1)) ?? $this->flashFail("err", tr("failed_to_publish_post"), tr("error_4")); + + if ($wallOwner->isBanned()) + $this->flashFail("err", tr("error"), tr("forbidden")); + if($wall > 0) { - if(!$wallOwner->isBanned()) - $canPost = $wallOwner->getPrivacyPermission("wall.write", $this->user->identity); - else - $this->flashFail("err", tr("not_enough_permissions"), tr("not_enough_permissions_comment")); + $canPost = $wallOwner->getPrivacyPermission("wall.write", $this->user->identity); } else if($wall < 0) { if($wallOwner->canBeModifiedBy($this->user->identity)) $canPost = true; @@ -354,6 +357,9 @@ final class WallPresenter extends OpenVKPresenter } else { $this->template->wallOwner = (new Clubs)->get(abs($post->getTargetWall())); $this->template->isWallOfGroup = true; + + if ($this->template->wallOwner->isBanned()) + $this->flashFail("err", tr("error"), tr("forbidden")); } $this->template->cCount = $post->getCommentsCount(); $this->template->cPage = (int) ($_GET["p"] ?? 1); @@ -368,7 +374,10 @@ final class WallPresenter extends OpenVKPresenter $post = $this->posts->getPostById($wall, $post_id); if(!$post || $post->isDeleted()) $this->notFound(); - + + if ($post->getWallOwner()->isBanned()) + $this->flashFail("err", tr("error"), tr("forbidden")); + if(!is_null($this->user)) { $post->toggleLike($this->user->identity); } @@ -386,6 +395,9 @@ final class WallPresenter extends OpenVKPresenter if(!$post || $post->isDeleted()) $this->notFound(); + + if ($post->getWallOwner()->isBanned()) + $this->flashFail("err", tr("error"), tr("forbidden")); $where = $this->postParam("type") ?? "wall"; $groupId = NULL; @@ -444,6 +456,9 @@ final class WallPresenter extends OpenVKPresenter $wallOwner = ($wall > 0 ? (new Users)->get($wall) : (new Clubs)->get($wall * -1)) ?? $this->flashFail("err", tr("failed_to_delete_post"), tr("error_4")); + if ($wallOwner->isBanned()) + $this->flashFail("err", tr("error"), tr("forbidden")); + if($wall < 0) $canBeDeletedByOtherUser = $wallOwner->canBeModifiedBy($this->user->identity); else $canBeDeletedByOtherUser = false; @@ -467,6 +482,9 @@ final class WallPresenter extends OpenVKPresenter $post = $this->posts->getPostById($wall, $post_id); if(!$post) $this->notFound(); + + if ($post->getWallOwner()->isBanned()) + $this->flashFail("err", tr("error"), tr("forbidden")); if(!$post->canBePinnedBy($this->user->identity)) $this->flashFail("err", tr("not_enough_permissions"), tr("not_enough_permissions_comment")); diff --git a/Web/Presenters/templates/Group/Banned.xml b/Web/Presenters/templates/Group/Banned.xml new file mode 100644 index 00000000..152bf10f --- /dev/null +++ b/Web/Presenters/templates/Group/Banned.xml @@ -0,0 +1,22 @@ +{extends "../@layout.xml"} + +{block title}{$club->getCanonicalName()}{/block} + +{block header}{include title}{/block} + +{block content} +
+ Сообщество заблокировано. +

+ {tr("group_banned", htmlentities($club->getCanonicalName()))|noescape} +
+ {_user_banned_comment} {$club->getBanReason()}. +

+ {if isset($thisUser)} +

+
+ {_edit} +

+ {/if} +
+{/block} diff --git a/locales/en.strings b/locales/en.strings index 4aaa4483..1a7a8c72 100644 --- a/locales/en.strings +++ b/locales/en.strings @@ -331,6 +331,8 @@ "search_by_groups" = "Search by groups"; "search_group_desc" = "Here you can browse through the existing groups and choose a group to suit your needs..."; +"group_banned" = "Unfortunately, we had to block the $1 group."; + /* Albums */ "create" = "Create"; diff --git a/locales/ru.strings b/locales/ru.strings index 6faa5e2e..841e8776 100644 --- a/locales/ru.strings +++ b/locales/ru.strings @@ -314,6 +314,7 @@ "search_group" = "Поиск группы"; "search_by_groups" = "Поиск по группам"; "search_group_desc" = "Здесь Вы можете просмотреть существующие группы и выбрать группу себе по вкусу..."; +"group_banned" = "К сожалению, нам пришлось заблокировать сообщество $1."; /* Albums */ From 7f46d683c3d086e04ed97b5d9bf243a94ef27443 Mon Sep 17 00:00:00 2001 From: n1rwana Date: Fri, 11 Aug 2023 02:11:40 +0300 Subject: [PATCH 004/231] Support: Give users the ability to close tickets themselves (#925) * User can close ticket (#879) * Localization * Update SupportPresenter.php * Update View.xml --- Web/Presenters/SupportPresenter.php | 24 ++++++++++++++++++++-- Web/Presenters/templates/Support/Agent.xml | 3 +-- Web/Presenters/templates/Support/List.xml | 2 +- Web/Presenters/templates/Support/View.xml | 13 ++++++++++++ Web/routes.yml | 2 ++ locales/en.strings | 7 +++++++ locales/ru.strings | 6 ++++++ 7 files changed, 52 insertions(+), 5 deletions(-) diff --git a/Web/Presenters/SupportPresenter.php b/Web/Presenters/SupportPresenter.php index c4d729ea..8163dbf8 100644 --- a/Web/Presenters/SupportPresenter.php +++ b/Web/Presenters/SupportPresenter.php @@ -385,7 +385,7 @@ final class SupportPresenter extends OpenVKPresenter $agent->setNumerate((int) $this->postParam("number") ?? NULL); $agent->setIcon($this->postParam("avatar")); $agent->save(); - $this->flashFail("succ", "Успех", "Профиль отредактирован."); + $this->flashFail("succ", tr("agent_profile_edited")); } else { $agent = new SupportAgent; $agent->setAgent($this->user->identity->getId()); @@ -393,7 +393,27 @@ final class SupportPresenter extends OpenVKPresenter $agent->setNumerate((int) $this->postParam("number") ?? NULL); $agent->setIcon($this->postParam("avatar")); $agent->save(); - $this->flashFail("succ", "Успех", "Профиль создан. Теперь пользователи видят Ваши псевдоним и аватарку вместо стандартных аватарки и номера."); + $this->flashFail("succ", tr("agent_profile_created_1"), tr("agent_profile_created_2")); } } + + function renderCloseTicket(int $id): void + { + $this->assertUserLoggedIn(); + $this->assertNoCSRF(); + $this->willExecuteWriteAction(); + + $ticket = $this->tickets->get($id); + + if($ticket->isDeleted() === 1 || $ticket->getType() === 2 || $ticket->getUserId() !== $this->user->id) { + header("HTTP/1.1 403 Forbidden"); + header("Location: /support/view/" . $id); + exit; + } + + $ticket->setType(2); + $ticket->save(); + + $this->flashFail("succ", tr("ticket_changed"), tr("ticket_changed_comment")); + } } diff --git a/Web/Presenters/templates/Support/Agent.xml b/Web/Presenters/templates/Support/Agent.xml index 987fb081..a229d763 100644 --- a/Web/Presenters/templates/Support/Agent.xml +++ b/Web/Presenters/templates/Support/Agent.xml @@ -55,7 +55,6 @@

- {$agent->isShowNumber()} + + + + + + + + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + +
IDПользовательОбъектТипИзмененияВремя
{$log->getId()} + {$log->getUser()} + + + + {$log->getObjectName()} + + + {$log->getObjectName()} + {_$log->getTypeNom()} + {foreach $log->getChanges() as $change} +
+ {$change["field"]}: + {if array_key_exists('diff', $change)} + {$change["diff"]|noescape} + {else} + {$change["old_value"]} + {/if} +
+ {/foreach} +
+ {=new openvk\Web\Util\DateTime($change["ts"])} +
+
+
+ {var $isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + $amount) < $count} + + « + » +
+{/block} diff --git a/Web/Presenters/templates/Group/View.xml b/Web/Presenters/templates/Group/View.xml index bf392a9f..10d1f0dd 100644 --- a/Web/Presenters/templates/Group/View.xml +++ b/Web/Presenters/templates/Group/View.xml @@ -124,6 +124,7 @@ {/if} {if $thisUser->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL)} {_manage_group_action} + Последние действия {/if} {if $club->getSubscriptionStatus($thisUser) == false}
diff --git a/Web/Presenters/templates/User/View.xml b/Web/Presenters/templates/User/View.xml index 1fa71026..bc197310 100644 --- a/Web/Presenters/templates/User/View.xml +++ b/Web/Presenters/templates/User/View.xml @@ -118,6 +118,9 @@ {_warn_user_action} + + Последние действия + {/if} {if $thisUser->getChandlerUser()->can('write')->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0)} diff --git a/Web/routes.yml b/Web/routes.yml index 267c0608..0ff60bb6 100644 --- a/Web/routes.yml +++ b/Web/routes.yml @@ -341,6 +341,8 @@ routes: handler: "Admin->chandlerGroup" - url: "/admin/chandler/users/{slug}" handler: "Admin->chandlerUser" + - url: "/admin/logs" + handler: "Admin->logs" - url: "/internal/wall{num}" handler: "Wall->wallEmbedded" - url: "/robots.txt" diff --git a/locales/en.strings b/locales/en.strings index ce4ac0bf..2b71592a 100644 --- a/locales/en.strings +++ b/locales/en.strings @@ -1210,6 +1210,15 @@ "admin_banned_link_not_specified" = "The link is not specified"; "admin_banned_link_not_found" = "Link not found"; +"logs_adding" = "Creation"; +"logs_editing" = "Editing"; +"logs_removing" = "Deletion"; +"logs_restoring" = "Restoring"; +"logs_added" = "created"; +"logs_edited" = "edited"; +"logs_removed" = "removed"; +"logs_restored" = "restored"; + /* Paginator (deprecated) */ "paginator_back" = "Back"; diff --git a/locales/ru.strings b/locales/ru.strings index d4b85c79..e42c2fd0 100644 --- a/locales/ru.strings +++ b/locales/ru.strings @@ -1096,6 +1096,14 @@ "admin_banned_link_initiator" = "Инициатор"; "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) */ diff --git a/openvk-example.yml b/openvk-example.yml index e3fd1c3a..4b45e7ba 100644 --- a/openvk-example.yml +++ b/openvk-example.yml @@ -102,6 +102,7 @@ openvk: fartscroll: false testLabel: false defaultMobileTheme: "" + logs: true telemetry: plausible: From 6159262026f995b2cc6dbfabfa2094195b919d5f Mon Sep 17 00:00:00 2001 From: Jill Date: Fri, 11 Aug 2023 13:50:19 +0000 Subject: [PATCH 006/231] Add reports (#634) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Reports: [INDEV] Undone implementation of reports * Reports: Backend is done * Reports: Still makin it... * Reports: Added report window * Reports: Corrected the content type * Reports: Make it work * Reports: Minor fixes and localization * Reports: Ability to hide Share and Like buttons Also renamed the .sql file * Revent some changes from 8f8d7bb I will move them to the master branch * Reports: Only for those who can access Helpdesk * Reports: Modified the route * Reports: Change the routes * Reports: Show reports count * Report: Fix URL * Обновление репортов (#715) * Репорты живы * 2 * Better reports * Логи * Update DBEntity.updated.php * noSpam * Сбор IP и UserAgent + фикс логирования в IPs * Новые поля для поиска etc. * Fixes * Fixes and enhancements * Поиск по нескольким разделам * Reports enhancements * Совместимость с новыми логами * Совместимость с новыми логами * Update Logs.xml * Update Logs.xml * Logs i18n * Update Logs.xml * Update AdminPresenter.php --------- Co-authored-by: veselcraft Co-authored-by: Ilya Prokopenko <55238545+Xenforce@users.noreply.github.com> Co-authored-by: n1rwana --- Web/Models/Entities/Application.php | 5 +- Web/Models/Entities/Ban.php | 66 +++ Web/Models/Entities/Club.php | 18 +- Web/Models/Entities/NoSpamLog.php | 71 ++++ Web/Models/Entities/Postable.php | 3 +- Web/Models/Entities/Report.php | 142 +++++++ Web/Models/Entities/User.php | 126 +++++- Web/Models/Repositories/Bans.php | 33 ++ Web/Models/Repositories/ChandlerUsers.php | 3 +- Web/Models/Repositories/NoSpamLogs.php | 34 ++ Web/Models/Repositories/Reports.php | 67 ++++ Web/Models/Repositories/Users.php | 4 +- Web/Presenters/AdminPresenter.php | 30 +- Web/Presenters/AuthPresenter.php | 11 +- Web/Presenters/BlobPresenter.php | 2 + Web/Presenters/NoSpamPresenter.php | 377 ++++++++++++++++++ Web/Presenters/OpenVKPresenter.php | 6 +- Web/Presenters/ReportPresenter.php | 151 +++++++ Web/Presenters/templates/@banned.xml | 15 +- Web/Presenters/templates/@layout.xml | 26 +- Web/Presenters/templates/@listView.xml | 10 +- .../templates/Admin/BansHistory.xml | 86 ++++ Web/Presenters/templates/Apps/Play.xml | 26 ++ Web/Presenters/templates/Group/View.xml | 28 ++ Web/Presenters/templates/NoSpam/Index.xml | 315 +++++++++++++++ Web/Presenters/templates/NoSpam/Tabs.xml | 9 + Web/Presenters/templates/NoSpam/Templates.xml | 131 ++++++ Web/Presenters/templates/Photos/Photo.xml | 28 ++ Web/Presenters/templates/Report/List.xml | 60 +++ Web/Presenters/templates/Report/Tabs.xml | 145 +++++++ Web/Presenters/templates/Report/View.xml | 37 ++ .../templates/Report/ViewContent.xml | 30 ++ .../templates/Report/content/app.xml | 22 + .../templates/Report/content/note.xml | 18 + .../templates/Report/content/photo.xml | 26 ++ .../templates/Report/content/video.xml | 32 ++ .../templates/Support/AnswerTicket.xml | 37 +- Web/Presenters/templates/User/View.xml | 32 +- Web/Presenters/templates/User/banned.xml | 4 +- Web/Presenters/templates/Videos/View.xml | 32 ++ Web/Presenters/templates/Wall/Post.xml | 27 ++ .../templates/components/comment.xml | 41 ++ Web/Presenters/templates/components/group.xml | 43 ++ Web/Presenters/templates/components/photo.xml | 5 + Web/Presenters/templates/components/video.xml | 70 ++-- Web/di.yml | 5 +- Web/routes.yml | 18 + Web/static/css/main.css | 1 + Web/static/img/supp_icons.png | Bin 0 -> 1111 bytes install/sqls/00018-reports.sql | 11 + install/sqls/00032-better-reports.sql | 19 + install/sqls/00038-noSpam-templates.sql | 21 + locales/en.strings | 10 + locales/ru.strings | 12 + 54 files changed, 2514 insertions(+), 67 deletions(-) create mode 100644 Web/Models/Entities/Ban.php create mode 100644 Web/Models/Entities/NoSpamLog.php create mode 100644 Web/Models/Entities/Report.php create mode 100644 Web/Models/Repositories/Bans.php create mode 100644 Web/Models/Repositories/NoSpamLogs.php create mode 100644 Web/Models/Repositories/Reports.php create mode 100644 Web/Presenters/NoSpamPresenter.php mode change 100755 => 100644 Web/Presenters/OpenVKPresenter.php create mode 100644 Web/Presenters/ReportPresenter.php create mode 100644 Web/Presenters/templates/Admin/BansHistory.xml create mode 100644 Web/Presenters/templates/NoSpam/Index.xml create mode 100644 Web/Presenters/templates/NoSpam/Tabs.xml create mode 100644 Web/Presenters/templates/NoSpam/Templates.xml create mode 100644 Web/Presenters/templates/Report/List.xml create mode 100644 Web/Presenters/templates/Report/Tabs.xml create mode 100644 Web/Presenters/templates/Report/View.xml create mode 100644 Web/Presenters/templates/Report/ViewContent.xml create mode 100644 Web/Presenters/templates/Report/content/app.xml create mode 100644 Web/Presenters/templates/Report/content/note.xml create mode 100644 Web/Presenters/templates/Report/content/photo.xml create mode 100644 Web/Presenters/templates/Report/content/video.xml create mode 100644 Web/Presenters/templates/components/group.xml create mode 100644 Web/Presenters/templates/components/photo.xml create mode 100644 Web/static/img/supp_icons.png create mode 100644 install/sqls/00018-reports.sql create mode 100644 install/sqls/00032-better-reports.sql create mode 100644 install/sqls/00038-noSpam-templates.sql diff --git a/Web/Models/Entities/Application.php b/Web/Models/Entities/Application.php index 74569485..48975645 100644 --- a/Web/Models/Entities/Application.php +++ b/Web/Models/Entities/Application.php @@ -306,11 +306,14 @@ class Application extends RowModel function delete(bool $softly = true): void { if($softly) - throw new \UnexpectedValueException("Can't delete apps softly."); + throw new \UnexpectedValueException("Can't delete apps softly."); // why $cx = DatabaseConnection::i()->getContext(); $cx->table("app_users")->where("app", $this->getId())->delete(); parent::delete(false); } + + function getPublicationTime(): string + { return tr("recently"); } } \ No newline at end of file diff --git a/Web/Models/Entities/Ban.php b/Web/Models/Entities/Ban.php new file mode 100644 index 00000000..3962c6cb --- /dev/null +++ b/Web/Models/Entities/Ban.php @@ -0,0 +1,66 @@ +getRecord()->id; + } + + function getReason(): ?string + { + return $this->getRecord()->reason; + } + + function getUser(): ?User + { + return (new Users)->get($this->getRecord()->user); + } + + function getInitiator(): ?User + { + return (new Users)->get($this->getRecord()->initiator); + } + + function getStartTime(): int + { + return $this->getRecord()->iat; + } + + function getEndTime(): int + { + return $this->getRecord()->exp; + } + + function getTime(): int + { + return $this->getRecord()->time; + } + + function isPermanent(): bool + { + return $this->getEndTime() === 0; + } + + function isRemovedManually(): bool + { + return (bool) $this->getRecord()->removed_manually; + } + + function isOver(): bool + { + return $this->isRemovedManually(); + } + + function whoRemoved(): ?User + { + return (new Users)->get($this->getRecord()->removed_by); + } +} diff --git a/Web/Models/Entities/Club.php b/Web/Models/Entities/Club.php index 06b2f49b..31485129 100644 --- a/Web/Models/Entities/Club.php +++ b/Web/Models/Entities/Club.php @@ -351,9 +351,21 @@ class Club extends RowModel } function getWebsite(): ?string - { - return $this->getRecord()->website; - } + { + return $this->getRecord()->website; + } + + function ban(string $reason): void + { + $this->setBlock_Reason($reason); + $this->save(); + } + + function unban(): void + { + $this->setBlock_Reason(null); + $this->save(); + } function getAlert(): ?string { diff --git a/Web/Models/Entities/NoSpamLog.php b/Web/Models/Entities/NoSpamLog.php new file mode 100644 index 00000000..48d723c9 --- /dev/null +++ b/Web/Models/Entities/NoSpamLog.php @@ -0,0 +1,71 @@ +getRecord()->id; + } + + function getUser(): ?User + { + return (new Users)->get($this->getRecord()->user); + } + + function getModel(): string + { + return $this->getRecord()->model; + } + + function getRegex(): ?string + { + return $this->getRecord()->regex; + } + + function getRequest(): ?string + { + return $this->getRecord()->request; + } + + function getCount(): int + { + return $this->getRecord()->count; + } + + function getTime(): DateTime + { + return new DateTime($this->getRecord()->time); + } + + function getItems(): ?array + { + return explode(",", $this->getRecord()->items); + } + + function getTypeRaw(): int + { + return $this->getRecord()->ban_type; + } + + function getType(): string + { + switch ($this->getTypeRaw()) { + case 1: return "О"; + case 2: return "Б"; + case 3: return "ОБ"; + default: return (string) $this->getTypeRaw(); + } + } + + function isRollbacked(): bool + { + return !is_null($this->getRecord()->rollback); + } +} diff --git a/Web/Models/Entities/Postable.php b/Web/Models/Entities/Postable.php index b743793f..23981cc1 100644 --- a/Web/Models/Entities/Postable.php +++ b/Web/Models/Entities/Postable.php @@ -34,7 +34,8 @@ abstract class Postable extends Attachable $oid = (int) $this->getRecord()->owner; if(!$real && $this->isAnonymous()) $oid = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["account"]; - + + $oid = abs($oid); if($oid > 0) return (new Users)->get($oid); else diff --git a/Web/Models/Entities/Report.php b/Web/Models/Entities/Report.php new file mode 100644 index 00000000..4070952e --- /dev/null +++ b/Web/Models/Entities/Report.php @@ -0,0 +1,142 @@ +getRecord()->id; + } + + function getStatus(): int + { + return $this->getRecord()->status; + } + + function getContentType(): string + { + return $this->getRecord()->type; + } + + function getReason(): string + { + return $this->getRecord()->reason; + } + + function getTime(): DateTime + { + return new DateTime($this->getRecord()->date); + } + + function isDeleted(): bool + { + if ($this->getRecord()->deleted === 0) + { + return false; + } elseif ($this->getRecord()->deleted === 1) { + return true; + } + } + + function authorId(): int + { + return $this->getRecord()->user_id; + } + + function getUser(): User + { + return (new Users)->get((int) $this->getRecord()->user_id); + } + + function getContentId(): int + { + return (int) $this->getRecord()->target_id; + } + + function getContentObject() + { + if ($this->getContentType() == "post") return (new Posts)->get($this->getContentId()); + else if ($this->getContentType() == "photo") return (new Photos)->get($this->getContentId()); + else if ($this->getContentType() == "video") return (new Videos)->get($this->getContentId()); + else if ($this->getContentType() == "group") return (new Clubs)->get($this->getContentId()); + else if ($this->getContentType() == "comment") return (new Comments)->get($this->getContentId()); + else if ($this->getContentType() == "note") return (new Notes)->get($this->getContentId()); + else if ($this->getContentType() == "app") return (new Applications)->get($this->getContentId()); + else if ($this->getContentType() == "user") return (new Users)->get($this->getContentId()); + else return null; + } + + function getAuthor(): RowModel + { + return (new Posts)->get($this->getContentId())->getOwner(); + } + + function getReportAuthor(): User + { + return (new Users)->get($this->getRecord()->user_id); + } + + function banUser($initiator) + { + $reason = $this->getContentType() !== "user" ? ("**content-" . $this->getContentType() . "-" . $this->getContentId() . "**") : ("Подозрительная активность"); + $this->getAuthor()->ban($reason, false, time() + $this->getAuthor()->getNewBanTime(), $initiator); + } + + function deleteContent() + { + if ($this->getContentType() !== "user") { + $pubTime = $this->getContentObject()->getPublicationTime(); + $name = $this->getContentObject()->getName(); + $this->getAuthor()->adminNotify("Ваш контент, который вы опубликовали $pubTime ($name) был удалён модераторами инстанса. За повторные или серьёзные нарушения вас могут заблокировать."); + $this->getContentObject()->delete($this->getContentType() !== "app"); + } + + $this->delete(); + } + + function getDuplicates(): \Traversable + { + return (new Reports)->getDuplicates($this->getContentType(), $this->getContentId(), $this->getId()); + } + + function getDuplicatesCount(): int + { + return count(iterator_to_array($this->getDuplicates())); + } + + function hasDuplicates(): bool + { + return $this->getDuplicatesCount() > 0; + } + + function getContentName(): string + { + if (method_exists($this->getContentObject(), "getCanonicalName")) + return $this->getContentObject()->getCanonicalName(); + + return $this->getContentType() . " #" . $this->getContentId(); + } + + public function delete(bool $softly = true): void + { + if ($this->hasDuplicates()) { + foreach ($this->getDuplicates() as $duplicate) { + $duplicate->setDeleted(1); + $duplicate->save(); + } + } + + $this->setDeleted(1); + $this->save(); + } +} diff --git a/Web/Models/Entities/User.php b/Web/Models/Entities/User.php index db5cf055..e5b8da06 100644 --- a/Web/Models/Entities/User.php +++ b/Web/Models/Entities/User.php @@ -5,7 +5,7 @@ use openvk\Web\Themes\{Themepack, Themepacks}; use openvk\Web\Util\DateTime; use openvk\Web\Models\RowModel; use openvk\Web\Models\Entities\{Photo, Message, Correspondence, Gift}; -use openvk\Web\Models\Repositories\{Photos, Users, Clubs, Albums, Gifts, Notifications}; +use openvk\Web\Models\Repositories\{Applications, Bans, Comments, Notes, Posts, Users, Clubs, Albums, Gifts, Notifications, Videos, Photos}; use openvk\Web\Models\Exceptions\InvalidUserNameException; use Nette\Database\Table\ActiveRow; use Chandler\Database\DatabaseConnection; @@ -241,11 +241,60 @@ class User extends RowModel return $this->getRecord()->alert; } - function getBanReason(): ?string + function getTextForContentBan(string $type): string + { + switch ($type) { + case "post": return "за размещение от Вашего лица таких записей:"; + case "photo": return "за размещение от Вашего лица таких фотографий:"; + case "video": return "за размещение от Вашего лица таких видеозаписей:"; + case "group": return "за подозрительное вступление от Вашего лица в группу:"; + case "comment": return "за размещение от Вашего лица таких комментариев:"; + case "note": return "за размещение от Вашего лица таких заметок:"; + case "app": return "за создание от Вашего имени подозрительных приложений."; + default: return "за размещение от Вашего лица такого контента:"; + } + } + + function getRawBanReason(): ?string { return $this->getRecord()->block_reason; } + function getBanReason(?string $for = null) + { + $ban = (new Bans)->get((int) $this->getRecord()->block_reason); + if (!$ban || $ban->isOver()) return null; + + $reason = $ban->getReason(); + + preg_match('/\*\*content-(post|photo|video|group|comment|note|app|noSpamTemplate|user)-(\d+)\*\*$/', $reason, $matches); + if (sizeof($matches) === 3) { + $content_type = $matches[1]; $content_id = (int) $matches[2]; + if (in_array($content_type, ["noSpamTemplate", "user"])) { + $reason = "Подозрительная активность"; + } else { + if ($for !== "banned") { + $reason = "Подозрительная активность"; + } else { + $reason = [$this->getTextForContentBan($content_type), $content_type]; + switch ($content_type) { + case "post": $reason[] = (new Posts)->get($content_id); break; + case "photo": $reason[] = (new Photos)->get($content_id); break; + case "video": $reason[] = (new Videos)->get($content_id); break; + case "group": $reason[] = (new Clubs)->get($content_id); break; + case "comment": $reason[] = (new Comments)->get($content_id); break; + case "note": $reason[] = (new Notes)->get($content_id); break; + case "app": $reason[] = (new Applications)->get($content_id); break; + case "user": $reason[] = (new Users)->get($content_id); break; + default: $reason[] = null; + } + } + } + } + + return $reason; + } + function getBanInSupportReason(): ?string { return $this->getRecord()->block_in_support_reason; @@ -833,7 +882,7 @@ class User extends RowModel ]); } - function ban(string $reason, bool $deleteSubscriptions = true, ?int $unban_time = NULL): void + function ban(string $reason, bool $deleteSubscriptions = true, $unban_time = NULL, ?int $initiator = NULL): void { if($deleteSubscriptions) { $subs = DatabaseConnection::i()->getContext()->table("subscriptions"); @@ -846,8 +895,33 @@ class User extends RowModel $subs->delete(); } - $this->setBlock_Reason($reason); - $this->setUnblock_time($unban_time); + $iat = time(); + $ban = new Ban; + $ban->setUser($this->getId()); + $ban->setReason($reason); + $ban->setInitiator($initiator); + $ban->setIat($iat); + $ban->setExp($unban_time !== "permanent" ? $unban_time : 0); + $ban->setTime($unban_time === "permanent" ? 0 : ($unban_time ? ($unban_time - $iat) : 0)); + $ban->save(); + + $this->setBlock_Reason($ban->getId()); + // $this->setUnblock_time($unban_time); + $this->save(); + } + + function unban(int $removed_by): void + { + $ban = (new Bans)->get((int) $this->getRawBanReason()); + if (!$ban || $ban->isOver()) + return; + + $ban->setRemoved_Manually(true); + $ban->setRemoved_By($removed_by); + $ban->save(); + + $this->setBlock_Reason(NULL); + // $user->setUnblock_time(NULL); $this->save(); } @@ -1099,7 +1173,11 @@ class User extends RowModel function getUnbanTime(): ?string { - return !is_null($this->getRecord()->unblock_time) ? date('d.m.Y', $this->getRecord()->unblock_time) : NULL; + $ban = (new Bans)->get((int) $this->getRecord()->block_reason); + if (!$ban || $ban->isOver() || $ban->isPermanent()) return null; + if ($this->canUnbanThemself()) return tr("today"); + + return date('d.m.Y', $ban->getEndTime()); } function canUnbanThemself(): bool @@ -1107,10 +1185,40 @@ class User extends RowModel if (!$this->isBanned()) return false; - if ($this->getRecord()->unblock_time > time() || $this->getRecord()->unblock_time == 0) - return false; + $ban = (new Bans)->get((int) $this->getRecord()->block_reason); + if (!$ban || $ban->isOver() || $ban->isPermanent()) return false; - return true; + return $ban->getEndTime() <= time() && !$ban->isPermanent(); + } + + function getNewBanTime() + { + $bans = iterator_to_array((new Bans)->getByUser($this->getid())); + if (!$bans || count($bans) === 0) + return 0; + + $last_ban = end($bans); + if (!$last_ban) return 0; + + if ($last_ban->isPermanent()) return "permanent"; + + $values = [0, 3600, 7200, 86400, 172800, 604800, 1209600, 3024000, 9072000]; + $response = 0; + $i = 0; + + foreach ($values as $value) { + $i++; + if ($last_ban->getTime() === 0 && $value === 0) continue; + if ($last_ban->getTime() < $value) { + $response = $value; + break; + } else if ($last_ban->getTime() >= $value) { + if ($i < count($values)) continue; + $response = "permanent"; + break; + } + } + return $response; } function toVkApiStruct(): object diff --git a/Web/Models/Repositories/Bans.php b/Web/Models/Repositories/Bans.php new file mode 100644 index 00000000..7123459d --- /dev/null +++ b/Web/Models/Repositories/Bans.php @@ -0,0 +1,33 @@ +context = DB::i()->getContext(); + $this->bans = $this->context->table("bans"); + } + + function toBan(?ActiveRow $ar): ?Ban + { + return is_null($ar) ? NULL : new Ban($ar); + } + + function get(int $id): ?Ban + { + return $this->toBan($this->bans->get($id)); + } + + function getByUser(int $user_id): \Traversable + { + foreach ($this->bans->where("user", $user_id) as $ban) + yield new Ban($ban); + } +} \ No newline at end of file diff --git a/Web/Models/Repositories/ChandlerUsers.php b/Web/Models/Repositories/ChandlerUsers.php index a827afac..510e5860 100644 --- a/Web/Models/Repositories/ChandlerUsers.php +++ b/Web/Models/Repositories/ChandlerUsers.php @@ -28,7 +28,8 @@ class ChandlerUsers function getById(string $UUID): ?ChandlerUser { - return new ChandlerUser($this->users->where("id", $UUID)->fetch()); + $user = $this->users->where("id", $UUID)->fetch(); + return $user ? new ChandlerUser($user) : NULL; } function getList(int $page = 1): \Traversable diff --git a/Web/Models/Repositories/NoSpamLogs.php b/Web/Models/Repositories/NoSpamLogs.php new file mode 100644 index 00000000..f8dd4980 --- /dev/null +++ b/Web/Models/Repositories/NoSpamLogs.php @@ -0,0 +1,34 @@ +context = DatabaseConnection::i()->getContext(); + $this->noSpamLogs = $this->context->table("noSpam_templates"); + } + + private function toNoSpamLog(?ActiveRow $ar): ?NoSpamLog + { + return is_null($ar) ? NULL : new NoSpamLog($ar); + } + + function get(int $id): ?NoSpamLog + { + return $this->toNoSpamLog($this->noSpamLogs->get($id)); + } + + function getList(array $filter = []): \Traversable + { + foreach ($this->noSpamLogs->where($filter)->order("`id` DESC") as $log) + yield new NoSpamLog($log); + } +} diff --git a/Web/Models/Repositories/Reports.php b/Web/Models/Repositories/Reports.php new file mode 100644 index 00000000..edce8980 --- /dev/null +++ b/Web/Models/Repositories/Reports.php @@ -0,0 +1,67 @@ +context = DatabaseConnection::i()->getContext(); + $this->reports = $this->context->table("reports"); + } + + private function toReport(?ActiveRow $ar): ?Report + { + return is_null($ar) ? NULL : new Report($ar); + } + + function getReports(int $state = 0, int $page = 1, ?string $type = NULL, ?bool $pagination = true): \Traversable + { + $filter = ["deleted" => 0]; + if ($type) $filter["type"] = $type; + + $reports = $this->reports->where($filter)->order("created DESC")->group("target_id, type"); + if ($pagination) + $reports = $reports->page($page, 15); + + foreach($reports as $t) + yield new Report($t); + } + + function getReportsCount(int $state = 0): int + { + return sizeof($this->reports->where(["deleted" => 0, "type" => $state])->group("target_id, type")); + } + + function get(int $id): ?Report + { + return $this->toReport($this->reports->get($id)); + } + + function getByContentId(int $id): ?Report + { + $post = $this->reports->where(["deleted" => 0, "content_id" => $id])->fetch(); + + if($post) + return new Report($post); + else + return null; + } + + function getDuplicates(string $type, int $target_id, ?int $orig = NULL, ?int $user_id = NULL): \Traversable + { + $filter = ["deleted" => 0, "type" => $type, "target_id" => $target_id]; + if ($orig) $filter[] = "id != $orig"; + if ($user_id) $filter["user_id"] = $user_id; + + foreach ($this->reports->where($filter) as $report) + yield new Report($report); + } + + use \Nette\SmartObject; +} diff --git a/Web/Models/Repositories/Users.php b/Web/Models/Repositories/Users.php index 6c165aa3..de0d341d 100644 --- a/Web/Models/Repositories/Users.php +++ b/Web/Models/Repositories/Users.php @@ -44,9 +44,9 @@ class Users return $alias->getUser(); } - function getByChandlerUser(ChandlerUser $user): ?User + function getByChandlerUser(?ChandlerUser $user): ?User { - return $this->toUser($this->users->where("user", $user->getId())->fetch()); + return $user ? $this->toUser($this->users->where("user", $user->getId())->fetch()) : NULL; } function find(string $query, array $pars = [], string $sort = "id DESC"): Util\EntityStream diff --git a/Web/Presenters/AdminPresenter.php b/Web/Presenters/AdminPresenter.php index bba2ef31..f5c40bcc 100644 --- a/Web/Presenters/AdminPresenter.php +++ b/Web/Presenters/AdminPresenter.php @@ -360,13 +360,19 @@ final class AdminPresenter extends OpenVKPresenter { $this->assertNoCSRF(); + if (str_contains($this->queryParam("reason"), "*")) + exit(json_encode([ "error" => "Incorrect reason" ])); + $unban_time = strtotime($this->queryParam("date")) ?: NULL; $user = $this->users->get($id); if(!$user) exit(json_encode([ "error" => "User does not exist" ])); - - $user->ban($this->queryParam("reason"), true, $unban_time); + + if ($this->queryParam("incr")) + $unban_time = time() + $user->getNewBanTime(); + + $user->ban($this->queryParam("reason"), true, $unban_time, $this->user->identity->getId()); exit(json_encode([ "success" => true, "reason" => $this->queryParam("reason") ])); } @@ -377,9 +383,17 @@ final class AdminPresenter extends OpenVKPresenter $user = $this->users->get($id); if(!$user) exit(json_encode([ "error" => "User does not exist" ])); - + + $ban = (new Bans)->get((int)$user->getRawBanReason()); + if (!$ban || $ban->isOver()) + exit(json_encode([ "error" => "User is not banned" ])); + + $ban->setRemoved_Manually(true); + $ban->setRemoved_By($this->user->identity->getId()); + $ban->save(); + $user->setBlock_Reason(NULL); - $user->setUnblock_time(NULL); + // $user->setUnblock_time(NULL); $user->save(); exit(json_encode([ "success" => true ])); } @@ -465,6 +479,14 @@ final class AdminPresenter extends OpenVKPresenter $this->redirect("/admin/bannedLinks"); } + function renderBansHistory(int $user_id) :void + { + $user = (new Users)->get($user_id); + if (!$user) $this->notFound(); + + $this->template->bans = (new Bans)->getByUser($user_id); + } + function renderChandlerGroups(): void { $this->template->groups = (new ChandlerGroups)->getList(); diff --git a/Web/Presenters/AuthPresenter.php b/Web/Presenters/AuthPresenter.php index f569dd63..23b55dc9 100644 --- a/Web/Presenters/AuthPresenter.php +++ b/Web/Presenters/AuthPresenter.php @@ -1,7 +1,7 @@ flashFail("err", tr("error"), tr("forbidden")); $user = $this->users->get($this->user->id); + $ban = (new Bans)->get((int)$user->getRawBanReason()); + if (!$ban || $ban->isOver() || $ban->isPermanent()) + $this->flashFail("err", tr("error"), tr("forbidden")); + + $ban->setRemoved_Manually(2); + $ban->setRemoved_By($this->user->identity->getId()); + $ban->save(); $user->setBlock_Reason(NULL); - $user->setUnblock_Time(NULL); + // $user->setUnblock_Time(NULL); $user->save(); $this->flashFail("succ", tr("banned_unban_title"), tr("banned_unban_description")); diff --git a/Web/Presenters/BlobPresenter.php b/Web/Presenters/BlobPresenter.php index 970b8f19..7bb3e2be 100644 --- a/Web/Presenters/BlobPresenter.php +++ b/Web/Presenters/BlobPresenter.php @@ -3,6 +3,8 @@ namespace openvk\Web\Presenters; final class BlobPresenter extends OpenVKPresenter { + protected $banTolerant = true; + private function getDirName($dir): string { if(gettype($dir) === "integer") { diff --git a/Web/Presenters/NoSpamPresenter.php b/Web/Presenters/NoSpamPresenter.php new file mode 100644 index 00000000..1560ba63 --- /dev/null +++ b/Web/Presenters/NoSpamPresenter.php @@ -0,0 +1,377 @@ +assertUserLoggedIn(); + $this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0); + + $targetDir = __DIR__ . '/../Models/Entities/'; + $mode = in_array($this->queryParam("act"), ["form", "templates", "rollback", "reports"]) ? $this->queryParam("act") : "form"; + + if ($mode === "form") { + $this->template->_template = "NoSpam/Index"; + $foundClasses = []; + foreach (Finder::findFiles('*.php')->from($targetDir) as $file) { + $content = file_get_contents($file->getPathname()); + $namespacePattern = '/namespace\s+([^\s;]+)/'; + $classPattern = '/class\s+([^\s{]+)/'; + preg_match($namespacePattern, $content, $namespaceMatches); + preg_match($classPattern, $content, $classMatches); + + if (isset($namespaceMatches[1]) && isset($classMatches[1])) { + $classNamespace = trim($namespaceMatches[1]); + $className = trim($classMatches[1]); + $fullClassName = $classNamespace . '\\' . $className; + + if ($classNamespace === NoSpamPresenter::ENTITIES_NAMESPACE && class_exists($fullClassName)) { + $foundClasses[] = $className; + } + } + } + + $models = []; + + foreach ($foundClasses as $class) { + $r = new \ReflectionClass(NoSpamPresenter::ENTITIES_NAMESPACE . "\\$class"); + if (!$r->isAbstract() && $r->getName() !== NoSpamPresenter::ENTITIES_NAMESPACE . "\\Correspondence") + $models[] = $class; + } + $this->template->models = $models; + } else if ($mode === "templates") { + $this->template->_template = "NoSpam/Templates.xml"; + $filter = []; + if ($this->queryParam("id")) { + $filter["id"] = (int)$this->queryParam("id"); + } + $this->template->templates = iterator_to_array((new NoSpamLogs)->getList($filter)); + } else if ($mode === "reports") { + $this->redirect("/scumfeed"); + } else { + $template = (new NoSpamLogs)->get((int)$this->postParam("id")); + if (!$template || $template->isRollbacked()) + $this->returnJson(["success" => false, "error" => "Шаблон не найден"]); + + $model = NoSpamPresenter::ENTITIES_NAMESPACE . "\\" . $template->getModel(); + $items = $template->getItems(); + if (count($items) > 0) { + $db = DatabaseConnection::i()->getContext(); + + $unbanned_ids = []; + foreach ($items as $_item) { + try { + $item = new $model; + $table_name = $item->getTableName(); + $item = $db->table($table_name)->get((int)$_item); + if (!$item) continue; + + $item = new $model($item); + + if (key_exists("deleted", $item->unwrap()) && $item->isDeleted()) { + $item->setDeleted(0); + $item->save(); + } + + if (in_array($template->getTypeRaw(), [2, 3])) { + $owner = NULL; + $methods = ["getOwner", "getUser", "getRecipient", "getInitiator"]; + + if (method_exists($item, "ban")) { + $owner = $item; + } else { + foreach ($methods as $method) { + if (method_exists($item, $method)) { + $owner = $item->$method(); + break; + } + } + } + + $_id = ($owner instanceof Club ? $owner->getId() * -1 : $owner->getId()); + + if (!in_array($_id, $unbanned_ids)) { + $owner->unban($this->user->id); + $unbanned_ids[] = $_id; + } + } + } catch (\Throwable $e) { + $this->returnJson(["success" => false, "error" => $e->getMessage()]); + } + } + } else { + $this->returnJson(["success" => false, "error" => "Объекты не найдены"]); + } + + $template->setRollback(true); + $template->save(); + + $this->returnJson(["success" => true]); + } + } + + function renderSearch(): void + { + $this->assertUserLoggedIn(); + $this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0); + $this->assertNoCSRF(); + $this->willExecuteWriteAction(); + + function searchByAdditionalParams(?string $table = NULL, ?string $where = NULL, ?string $ip = NULL, ?string $useragent = NULL, ?int $ts = NULL, ?int $te = NULL, $user = NULL) + { + $db = DatabaseConnection::i()->getContext(); + if ($table && ($ip || $useragent || $ts || $te || $user)) { + $conditions = []; + + if ($ip) $conditions[] = "`ip` REGEXP '$ip'"; + if ($useragent) $conditions[] = "`useragent` REGEXP '$useragent'"; + if ($ts) $conditions[] = "`ts` < $ts"; + if ($te) $conditions[] = "`ts` > $te"; + if ($user) { + $users = new Users; + + $_user = $users->getByChandlerUser((new ChandlerUsers)->getById($user)) + ?? $users->get((int)$user) + ?? $users->getByAddress($user) + ?? NULL; + + if ($_user) { + $conditions[] = "`user` = '" . $_user->getChandlerGUID() . "'"; + } + } + + $whereStart = "WHERE `object_table` = '$table'"; + if ($table === "profiles") { + $whereStart .= "AND `type` = 0"; + } + + $conditions = count($conditions) > 0 ? "AND (" . implode(" AND ", $conditions) . ")" : ""; + $response = []; + + 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(); + + if (!$object) continue; + 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"]) { + $response[] = $object; + } + } + } + } + } + + return $response; + } + } + + try { + $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"); + + 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; + } + + $model = new $model_name; + + $c = new \ReflectionClass($model_name); + if ($c->isAbstract() || $c->getName() == NoSpamPresenter::ENTITIES_NAMESPACE . "\\Correspondence") { + continue; + } + + $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'"; + } + } + $conditions = implode(" OR ", $conditions); + + $where = ($this->postParam("where") ? " AND ($conditions)" : "($conditions)"); + if ($need_deleted) $where .= " AND (`deleted` = 0)"; + } + + $rows = []; + if ($ip || $useragent || $ts || $te || $user) { + $rows = searchByAdditionalParams($table, $where, $ip, $useragent, $ts, $te, $user); + } + + if (count($rows) === 0) { + if (!$searchTerm) { + 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 (!$where) { + $rows = []; + } else { + $result = $db->query("SELECT * FROM `$table` WHERE $where"); + $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("Подозрительная активность"); + } + + $banned_ids[] = $_id; + } + } + } + + if (in_array((int)$this->postParam("ban"), [1, 3])) + $object->delete(); + } + + $processed++; + } + } + + $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/OpenVKPresenter.php b/Web/Presenters/OpenVKPresenter.php old mode 100755 new mode 100644 index b4444bda..80ab0621 --- a/Web/Presenters/OpenVKPresenter.php +++ b/Web/Presenters/OpenVKPresenter.php @@ -7,7 +7,7 @@ use Chandler\Security\Authenticator; use Latte\Engine as TemplatingEngine; use openvk\Web\Models\Entities\IP; use openvk\Web\Themes\Themepacks; -use openvk\Web\Models\Repositories\{CurrentUser, IPs, Users, APITokens, Tickets}; +use openvk\Web\Models\Repositories\{IPs, Users, APITokens, Tickets, Reports, CurrentUser}; use WhichBrowser; abstract class OpenVKPresenter extends SimplePresenter @@ -260,8 +260,10 @@ abstract class OpenVKPresenter extends SimplePresenter } $this->template->ticketAnsweredCount = (new Tickets)->getTicketsCountByUserId($this->user->id, 1); - if($user->can("write")->model("openvk\Web\Models\Entities\TicketReply")->whichBelongsTo(0)) + if($user->can("write")->model("openvk\Web\Models\Entities\TicketReply")->whichBelongsTo(0)) { $this->template->helpdeskTicketNotAnsweredCount = (new Tickets)->getTicketCount(0); + $this->template->reportNotAnsweredCount = (new Reports)->getReportsCount(0); + } } header("X-OpenVK-User-Validated: $userValidated"); diff --git a/Web/Presenters/ReportPresenter.php b/Web/Presenters/ReportPresenter.php new file mode 100644 index 00000000..68d27861 --- /dev/null +++ b/Web/Presenters/ReportPresenter.php @@ -0,0 +1,151 @@ +reports = $reports; + + parent::__construct(); + } + + function renderList(): void + { + $this->assertUserLoggedIn(); + $this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0); + if ($_SERVER["REQUEST_METHOD"] === "POST") + $this->assertNoCSRF(); + + $act = in_array($this->queryParam("act"), ["post", "photo", "video", "group", "comment", "note", "app", "user"]) ? $this->queryParam("act") : NULL; + + if (!$this->queryParam("orig")) { + $this->template->reports = $this->reports->getReports(0, (int)($this->queryParam("p") ?? 1), $act, $_SERVER["REQUEST_METHOD"] !== "POST"); + $this->template->count = $this->reports->getReportsCount(); + } else { + $orig = $this->reports->get((int) $this->queryParam("orig")); + if (!$orig) $this->redirect("/scumfeed"); + + $this->template->reports = $orig->getDuplicates(); + $this->template->count = $orig->getDuplicatesCount(); + $this->template->orig = $orig->getId(); + } + $this->template->paginatorConf = (object) [ + "count" => $this->template->count, + "page" => $this->queryParam("p") ?? 1, + "amount" => NULL, + "perPage" => 15, + ]; + $this->template->mode = $act ?? "all"; + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $reports = []; + foreach ($this->reports->getReports(0, 0, $act, false) as $report) { + $reports[] = [ + "id" => $report->getId(), + "author" => [ + "id" => $report->getReportAuthor()->getId(), + "url" => $report->getReportAuthor()->getURL(), + "name" => $report->getReportAuthor()->getCanonicalName(), + "is_female" => $report->getReportAuthor()->isFemale() + ], + "content" => [ + "name" => $report->getContentName(), + "type" => $report->getContentType(), + "id" => $report->getContentId(), + "url" => $report->getContentType() === "user" ? (new Users)->get((int) $report->getContentId())->getURL() : NULL + ], + "duplicates" => $report->getDuplicatesCount(), + ]; + } + $this->returnJson(["reports" => $reports]); + } + } + + function renderView(int $id): void + { + $this->assertUserLoggedIn(); + $this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0); + + $report = $this->reports->get($id); + if(!$report || $report->isDeleted()) + $this->notFound(); + + $this->template->report = $report; + } + + function renderCreate(int $id): void + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + + if(!$id) + exit(json_encode([ "error" => tr("error_segmentation") ])); + + if(in_array($this->queryParam("type"), ["post", "photo", "video", "group", "comment", "note", "app", "user"])) { + if (count(iterator_to_array($this->reports->getDuplicates($this->queryParam("type"), $id, NULL, $this->user->id))) <= 0) { + $report = new Report; + $report->setUser_id($this->user->id); + $report->setTarget_id($id); + $report->setType($this->queryParam("type")); + $report->setReason($this->queryParam("reason")); + $report->setCreated(time()); + $report->save(); + } + + exit(json_encode([ "reason" => $this->queryParam("reason") ])); + } else { + exit(json_encode([ "error" => "Unable to submit a report on this content type" ])); + } + } + + function renderAction(int $id): void + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + $this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0); + + $report = $this->reports->get($id); + if(!$report || $report->isDeleted()) $this->notFound(); + + if ($this->postParam("ban")) { + $report->deleteContent(); + $report->banUser($this->user->identity->getId()); + + $this->flash("suc", "Смэрть...", "Пользователь успешно забанен."); + } else if ($this->postParam("delete")) { + $report->deleteContent(); + + $this->flash("suc", "Нехай живе!", "Контент удалён, а пользователю прилетело предупреждение."); + } else if ($this->postParam("ignore")) { + $report->delete(); + + $this->flash("suc", "Нехай живе!", "Жалоба проигнорирована."); + } else if ($this->postParam("banClubOwner") || $this->postParam("banClub")) { + if ($report->getContentType() !== "group") + $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); + + $club = $report->getContentObject(); + if (!$club || $club->isBanned()) + $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); + + if ($this->postParam("banClubOwner")) { + $club->getOwner()->ban("**content-" . $report->getContentType() . "-" . $report->getContentId() . "**", false, $club->getOwner()->getNewBanTime(), $this->user->identity->getId()); + } else { + $club->ban("**content-" . $report->getContentType() . "-" . $report->getContentId() . "**"); + } + + $report->delete(); + + $this->flash("suc", "Смэрть...", ($this->postParam("banClubOwner") ? "Создатель сообщества успешно забанен." : "Сообщество успешно забанено")); + } + + $this->redirect("/scumfeed"); + } +} diff --git a/Web/Presenters/templates/@banned.xml b/Web/Presenters/templates/@banned.xml index 48c29ddb..7640838c 100644 --- a/Web/Presenters/templates/@banned.xml +++ b/Web/Presenters/templates/@banned.xml @@ -10,8 +10,19 @@ {_banned_alt}

- {tr("banned_1", htmlentities($thisUser->getCanonicalName()))|noescape}
- {tr("banned_2", htmlentities($thisUser->getBanReason()))|noescape} + {var $ban = $thisUser->getBanReason("banned")} + {if is_string($ban)} + {tr("banned_1", htmlentities($thisUser->getCanonicalName()))|noescape}
+ {tr("banned_2", htmlentities($thisUser->getBanReason()))|noescape} + {else} + {tr("banned_1", htmlentities($thisUser->getCanonicalName()))|noescape} +

+ Эта страница была заморожена {$ban[0]|noescape} + {if $ban[1] !== "app"} + {include "Report/ViewContent.xml", type => $ban[1], object => $ban[2]} + {/if} +
+ {/if} {if !$thisUser->getUnbanTime()} {_banned_perm} diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml index 1718c499..c79a8e89 100644 --- a/Web/Presenters/templates/@layout.xml +++ b/Web/Presenters/templates/@layout.xml @@ -209,6 +209,25 @@ ({$helpdeskTicketNotAnsweredCount}) {/if} + {tr("reports")} + {if $reportNotAnsweredCount > 0} + ({$reportNotAnsweredCount}) + {/if} + + + noSpam + + {$menuItem["name"]} +
+ + {$club->getName()} {strpos($menuItem["name"], "@") === 0 ? tr(substr($menuItem["name"], 1)) : $menuItem["name"]} @@ -283,8 +302,13 @@ {if !OPENVK_ROOT_CONF['openvk']['preferences']['security']['disablePasswordRestoring']}{_forgot_password}{/if} {/ifset} +
- + {ifset $thisUser} + {if !$thisUser->isBanned()} + + {/if} + {/ifset}
diff --git a/Web/Presenters/templates/@listView.xml b/Web/Presenters/templates/@listView.xml index 34739b59..f2342a85 100644 --- a/Web/Presenters/templates/@listView.xml +++ b/Web/Presenters/templates/@listView.xml @@ -12,16 +12,24 @@ {include size, x => $dat} {/ifset} + {ifset before_content} + {include before_content, x => $dat} + {/ifset} + {ifset specpage} {include specpage, x => $dat} {else}
{var $data = is_array($iterator) ? $iterator : iterator_to_array($iterator)} + {ifset top} + {include top, x => $dat} + {/ifset} + {if sizeof($data) > 0}
- + - - - - - - - - - - -
diff --git a/Web/Presenters/templates/Admin/BansHistory.xml b/Web/Presenters/templates/Admin/BansHistory.xml new file mode 100644 index 00000000..c0dc1b64 --- /dev/null +++ b/Web/Presenters/templates/Admin/BansHistory.xml @@ -0,0 +1,86 @@ +{extends "./@layout.xml"} + +{block title} + История блокировок +{/block} + +{block heading} + {include title} +{/block} + +{block content} + + + + + + + + + + + + + + + + + + + + + + + + + +
IDЗабаненныйИнициаторНачалоКонецВремяПричинаСнята
{$ban->getId()} + + + {$ban->getUser()->getCanonicalName()} + + + + {$ban->getUser()->getCanonicalName()} + + + {_admin_banned} + + + + + {$ban->getInitiator()->getCanonicalName()} + + + + {$ban->getInitiator()->getCanonicalName()} + + {_admin_banned} + + {date('d.m.Y в H:i:s', $ban->getStartTime())}{date('d.m.Y в H:i:s', $ban->getEndTime())}{$ban->getTime()} + {$ban->getReason()} + + {if $ban->isRemovedManually()} + + + {$ban->whoRemoved()->getCanonicalName()} + + + + {$ban->whoRemoved()->getCanonicalName()} + + + {_admin_banned} + + {else} + Активная блокировка + {/if} +
+{/block} \ No newline at end of file diff --git a/Web/Presenters/templates/Apps/Play.xml b/Web/Presenters/templates/Apps/Play.xml index 91cfe5d5..facaa273 100644 --- a/Web/Presenters/templates/Apps/Play.xml +++ b/Web/Presenters/templates/Apps/Play.xml @@ -1,4 +1,5 @@ {extends "../@layout.xml"} +{var $canReport = $owner->getId() !== $thisUser->getId()} {block title} {$name} @@ -6,6 +7,7 @@ {block header} {$name} +
Пожаловаться {/block} {block content} @@ -33,5 +35,29 @@ window.appOrigin = {$origin}; + + {script "js/al_games.js"} {/block} diff --git a/Web/Presenters/templates/Group/View.xml b/Web/Presenters/templates/Group/View.xml index 10d1f0dd..42cdbde3 100644 --- a/Web/Presenters/templates/Group/View.xml +++ b/Web/Presenters/templates/Group/View.xml @@ -141,6 +141,34 @@ {/if} + {var $canReport = $thisUser->getId() != $club->getOwner()->getId()} + {if $canReport} + {_report} + + + {/if}
diff --git a/Web/Presenters/templates/NoSpam/Index.xml b/Web/Presenters/templates/NoSpam/Index.xml new file mode 100644 index 00000000..746f86d5 --- /dev/null +++ b/Web/Presenters/templates/NoSpam/Index.xml @@ -0,0 +1,315 @@ +{extends "../@layout.xml"} + +{block title}noSpam{/block} +{block header}{include title}{/block} + +{block content} + +
{include "Tabs.xml", mode => "form"}
+
+
+
+ + + + + + + +
+ Раздел: + +
+ +
+
+
{_photo}: - -
-
- - -
- - - + + + + +
+
+
+
+

{_uploading_photos_from_computer}

+ +
+ {_admin_limits} +
    +
  • {_supported_formats}
  • +
  • {_max_load_photos}
  • +
+ +
+ +
+ +
+ {_tip}: {_tip_ctrl} +
+
+
+
+ +
+ + +
+ + + + +{/block} + +{block bodyScripts} + {script "js/al_photos.js"} {/block} diff --git a/Web/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", ``) + } + + xhr.onload = () => { + let result = JSON.parse(xhr.responseText) + + if(result.success) { + u("#loader").remove() + let photosArr = result.photos + + for(photo of photosArr) { + let table = document.querySelector(".insertPhotos") + + table.insertAdjacentHTML("beforeend", ` +
+
+ ${tr("description")}: + +
+
+ + ${tr("delete")} + +
+
+ `) + } + + document.getElementById("endUploading").style.display = "block" + } else { + u("#loader").remove() + MessageBox(tr("error"), escapeHtml(result.flash.message) ?? tr("error_uploading_photo"), [tr("ok")], [() => {Function.noop}]) + } + } + + xhr.send(photos) +}) + +$(document).on("click", "#endUploading", (e) => { + let table = document.querySelector("#photos") + let data = new FormData() + let arr = new Map(); + for(el of table.querySelectorAll("div#photo")) { + arr.set(el.dataset.id, el.querySelector("textarea").value) + } + + data.append("photos", JSON.stringify(Object.fromEntries(arr))) + data.append("hash", u("meta[name=csrf]").attr("value")) + + let xhr = new XMLHttpRequest() + // в самом вк на каждое изменение описания отправляется свой запрос, но тут мы экономим запросы + xhr.open("POST", "/photos/upload?act=finish&album="+document.getElementById("album").value) + + xhr.onloadstart = () => { + e.currentTarget.setAttribute("disabled", "disabled") + } + + xhr.onerror = () => { + MessageBox(tr("error"), tr("error_uploading_photo"), [tr("ok")], [() => {Function.noop}]) + } + + xhr.onload = () => { + let result = JSON.parse(xhr.responseText) + + if(!result.success) { + MessageBox(tr("error"), escapeHtml(result.flash.message), [tr("ok")], [() => {Function.noop}]) + } else { + document.querySelector(".page_content .insertPhotos").innerHTML = "" + document.getElementById("endUploading").style.display = "none" + + NewNotification(tr("photos_successfully_uploaded"), tr("click_to_go_to_album"), null, () => {window.location.assign(`/album${result.owner}_${result.album}`)}) + + document.querySelector(".whiteBox").style.display = "block" + document.querySelector(".insertAgain").append(document.getElementById("fakeButton")) + } + + e.currentTarget.removeAttribute("disabled") + } + + xhr.send(data) +}) + +$(document).on("click", "#deletePhoto", (e) => { + let data = new FormData() + data.append("hash", u("meta[name=csrf]").attr("value")) + + let xhr = new XMLHttpRequest() + xhr.open("POST", `/photo${e.currentTarget.dataset.owner}_${e.currentTarget.dataset.id}/delete`) + + xhr.onloadstart = () => { + e.currentTarget.closest("div#photo").classList.add("lagged") + } + + xhr.onerror = () => { + MessageBox(tr("error"), tr("unknown_error"), [tr("ok")], [() => {Function.noop}]) + } + + xhr.onload = () => { + u(e.currentTarget.closest("div#photo")).remove() + + if(document.querySelectorAll("div#photo").length < 1) { + document.getElementById("endUploading").style.display = "none" + document.querySelector(".whiteBox").style.display = "block" + document.querySelector(".insertAgain").append(document.getElementById("fakeButton")) + } + } + + xhr.send(data) +}) + +$(document).on("dragover drop", (e) => { + e.preventDefault() + + return false; +}) + +$(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 020/231] =?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} {_delete} + + {_changes_history} + {_report}
- {_open_original}
{/block} diff --git a/Web/Presenters/templates/Photos/UnlinkPhoto.xml b/Web/Presenters/templates/Photos/UnlinkPhoto.xml index 54498e06..80a4ad2e 100644 --- a/Web/Presenters/templates/Photos/UnlinkPhoto.xml +++ b/Web/Presenters/templates/Photos/UnlinkPhoto.xml @@ -1,20 +1,20 @@ {extends "../@layout.xml"} -{block title}Удалить фотографию?{/block} +{block title}{_delete_photo}{/block} {block header} - Удаление фотографии + {_delete_photo} {/block} {block content} - Вы уверены что хотите удалить эту фотографию? + {_sure_deleting_photo}

- Нет + {_no}   - +
{/block} diff --git a/Web/Presenters/templates/User/Edit.xml b/Web/Presenters/templates/User/Edit.xml index 6df070ea..b8e8398c 100644 --- a/Web/Presenters/templates/User/Edit.xml +++ b/Web/Presenters/templates/User/Edit.xml @@ -344,9 +344,9 @@
- +
- +
diff --git a/Web/Presenters/templates/User/Settings.xml b/Web/Presenters/templates/User/Settings.xml index 9d9a2b25..c8f0f61b 100644 --- a/Web/Presenters/templates/User/Settings.xml +++ b/Web/Presenters/templates/User/Settings.xml @@ -190,13 +190,13 @@ {if $graffiti} diff --git a/Web/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 zcmVPx#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!FLk!hNt9F6jm=ks||Nqlp_j z>h(Iit_!MEDqJd+s8T&)v3JK~q*5u^whfOzgOZpDu~-ZmDXbS(saC5oom5X*?nfVqY;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 = ` + + +
+ ` + + let form = e.currentTarget.closest("form") + + MessageBox(tr("selecting_video"), body, [tr("close")], [Function.noop]); + + // styles for messageboxx + document.querySelector(".ovk-diag-body").style.padding = "0" + document.querySelector(".ovk-diag-cont").style.width = "580px" + document.querySelector(".ovk-diag-body").style.height = "335px" + + async function insertVideos(page, query = "") { + document.querySelector(".videosInsert").insertAdjacentHTML("beforeend", ``) + + let vidoses + let noVideosText = tr("no_videos") + if(query == "") { + vidoses = await API.Wall.getVideos(page) + } else { + vidoses = await API.Wall.searchVideos(page, query) + noVideosText = tr("no_videos_results") + } + + if(vidoses.count < 1) { + document.querySelector(".videosInsert").innerHTML = `${noVideosText}` + } + + let pagesCount = Math.ceil(Number(vidoses.count) / 8) + u("#loader").remove() + let insert = document.querySelector(".videosInsert") + + for(const vid of vidoses.items) { + let isAttached = (form.querySelector("input[name='videos']").value.includes(`${vid.video.owner_id}_${vid.video.id},`)) + + insert.insertAdjacentHTML("beforeend", ` +
+ + + + + + + + +
+ +
+ ${escapeHtml(vid.video.title)} +
+
+
+ + + ${ovk_proc_strtr(escapeHtml(vid.video.title), 30)} + + +
+

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

+ ${escapeHtml(vid.video.author_name ?? "")} +
+
+ `) + } + + if(page < pagesCount) { + document.querySelector(".videosInsert").insertAdjacentHTML("beforeend", ` +
+ more... +
`) + } + } + + $(".videosInsert").on("click", "#showMoreVideos", (e) => { + u(e.currentTarget).remove() + insertVideos(Number(e.currentTarget.dataset.page), document.querySelector(".topGrayBlock #vquery").value) + }) + + $(".topGrayBlock #vquery").on("change", async (e) => { + await new Promise(r => setTimeout(r, 1000)); + + if(e.currentTarget.value === document.querySelector(".topGrayBlock #vquery").value) { + document.querySelector(".videosInsert").innerHTML = "" + insertVideos(1, e.currentTarget.value) + return; + } else { + console.info("skipping") + } + }) + + insertVideos(1) + + function insertAttachment(id) { + let videos = form.querySelector("input[name='videos']") + + if(!videos.value.includes(id + ",")) { + if(videos.value.split(",").length > 10) { + NewNotification(tr("error"), tr("max_attached_videos")) + return false + } + + form.querySelector("input[name='videos']").value += (id + ",") + + console.info(id + " attached") + return true + } else { + form.querySelector("input[name='videos']").value = form.querySelector("input[name='videos']").value.replace(id + ",", "") + + console.info(id + " detached") + return false + } + } + + $(".videosInsert").on("click", "#attachvid", (ev) => { + // откреплено от псто + if(!insertAttachment(ev.currentTarget.dataset.attachmentdata)) { + u(`.post-has-videos .post-has-video[data-id='${ev.currentTarget.dataset.attachmentdata}']`).remove() + ev.currentTarget.innerHTML = tr("attach") + } else { + ev.currentTarget.innerHTML = tr("detach") + + form.querySelector(".post-has-videos").insertAdjacentHTML("beforeend", ` +
+ ${tr("video")} "${ovk_proc_strtr(escapeHtml(ev.currentTarget.dataset.name), 20)}" +
+ `) + + u(`#unattachVideo[data-id='${ev.currentTarget.dataset.attachmentdata}']`).on("click", (e) => { + let id = ev.currentTarget.dataset.attachmentdata + form.querySelector("input[name='videos']").value = form.querySelector("input[name='videos']").value.replace(id + ",", "") + + console.info(id + " detached") + + u(e.currentTarget).remove() + }) + } + }) +}) + $(document).on("click", "#editPost", (e) => { let post = e.currentTarget.closest("table") let content = post.querySelector(".text") 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( `
@@ -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 026/231] 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 Date: Fri, 22 Sep 2023 23:56:18 +0300 Subject: [PATCH 027/231] 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 Date: Sat, 23 Sep 2023 01:10:03 +0300 Subject: [PATCH 028/231] 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 Date: Sat, 23 Sep 2023 01:11:00 +0300 Subject: [PATCH 029/231] 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 Date: Sat, 23 Sep 2023 01:18:33 +0300 Subject: [PATCH 030/231] 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 Date: Fri, 29 Sep 2023 18:47:53 +0300 Subject: [PATCH 031/231] 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 @@ 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 032/231] 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 @@ {_block_params}: - {_comment_as_group}
- + @@ -73,7 +77,7 @@ {_attach} - + canBeModifiedBy($thisUser)}data-club="{$club->getId()}"{/if}> {_photo} @@ -101,14 +105,11 @@ {if $graffiti} diff --git a/Web/Util/Makima/Makima.php b/Web/Util/Makima/Makima.php new file mode 100644 index 00000000..e31bfa42 --- /dev/null +++ b/Web/Util/Makima/Makima.php @@ -0,0 +1,305 @@ +photos = $photos; + } + + private function getOrientation(Photo $photo, &$ratio): int + { + [$width, $height] = $photo->getDimensions(); + $ratio = $width / $height; + if($ratio >= 1.2) + return Makima::ORIENT_WIDE; + else if($ratio >= 0.8) + return Makima::ORIENT_REGULAR; + else + return Makima::ORIENT_SLIM; + } + + private function calculateMultiThumbsHeight(array $ratios, float $w, float $m): float + { + return ($w - (sizeof($ratios) - 1) * $m) / array_sum($ratios); + } + + private function extractSubArr(array $arr, int $from, int $to): array + { + return array_slice($arr, $from, sizeof($arr) - $from - (sizeof($arr) - $to)); + } + + function computeMasonryLayout(float $maxWidth, float $maxHeight): MasonryLayout + { + $orients = []; + $ratios = []; + $count = sizeof($this->photos); + $result = new MasonryLayout; + + foreach($this->photos as $photo) { + $orients[] = $this->getOrientation($photo, $ratio); + $ratios[] = $ratio; + } + + $avgRatio = array_sum($ratios) / sizeof($ratios); + if($maxWidth < 0) + $maxWidth = $maxHeight = 510; + + $maxRatio = $maxWidth / $maxHeight; + $marginWidth = $marginHeight = 2; + + switch($count) { + case 2: + if( + $orients == [Makima::ORIENT_WIDE, Makima::ORIENT_WIDE] # two wide pics + && $avgRatio > (1.4 * $maxRatio) && abs($ratios[0] - $ratios[1]) < 0.2 # that can be positioned on top of each other + ) { + $computedHeight = ceil( min( $maxWidth / $ratios[0], min( $maxWidth / $ratios[1], ($maxHeight - $marginHeight) / 2 ) ) ); + + $result->colSizes = [1]; + $result->rowSizes = [1, 1]; + $result->width = ceil($maxWidth); + $result->height = $computedHeight; + $result->tiles = [new ThumbTile(1, 1, $maxWidth, $computedHeight), new ThumbTile(1, 1, $maxWidth, $computedHeight)]; + } else if( + $orients == [Makima::ORIENT_WIDE, Makima::ORIENT_WIDE] + || $orients == [Makima::ORIENT_REGULAR, Makima::ORIENT_REGULAR] # two normal pics of same ratio + ) { + $computedWidth = ($maxWidth - $marginWidth) / 2; + $height = min( $computedWidth / $ratios[0], min( $computedWidth / $ratios[1], $maxHeight ) ); + + $result->colSizes = [1, 1]; + $result->rowSizes = [1]; + $result->width = ceil($maxWidth); + $result->height = ceil($height); + $result->tiles = [new ThumbTile(1, 1, $computedWidth, $height), new ThumbTile(1, 1, $computedWidth, $height)]; + } else /* next to each other, different ratios */ { + $w0 = ( + ($maxWidth - $marginWidth) / $ratios[1] / ( (1 / $ratios[0]) + (1 / $ratios[1]) ) + ); + $w1 = $maxWidth - $w0 - $marginWidth; + $h = min($maxHeight, min($w0 / $ratios[0], $w1 / $ratios[1])); + + $result->colSizes = [ceil($w0), ceil($w1)]; + $result->rowSizes = [1]; + $result->width = ceil($w0 + $w1 + $marginWidth); + $result->height = ceil($h); + $result->tiles = [new ThumbTile(1, 1, $w0, $h), new ThumbTile(1, 1, $w1, $h)]; + } + break; + case 3: + # Three wide photos, we will put two of them below and one on top + if($orients == [Makima::ORIENT_WIDE, Makima::ORIENT_WIDE, Makima::ORIENT_WIDE]) { + $hCover = min($maxWidth / $ratios[0], ($maxHeight - $marginHeight) * (2 / 3)); + $w2 = ($maxWidth - $marginWidth) / 2; + $h = min($maxHeight - $hCover - $marginHeight, min($w2 / $ratios[1], $w2 / $ratios[2])); + + $result->colSizes = [1, 1]; + $result->rowSizes = [ceil($hCover), ceil($h)]; + $result->width = ceil($maxWidth); + $result->height = ceil($marginHeight + $hCover + $h); + $result->tiles = [ + new ThumbTile(2, 1, $maxWidth, $hCover), + new ThumbTile(1, 1, $w2, $h), new ThumbTile(1, 1, $w2, $h), + ]; + } else /* Photos have different sizes or are not wide, so we will put one to left and two to the right */ { + $wCover = min($maxHeight * $ratios[0], ($maxWidth - $marginWidth) * (3 / 4)); + $h1 = ($ratios[1] * ($maxHeight - $marginHeight) / ($ratios[2] + $ratios[1])); + $h0 = $maxHeight - $marginHeight - $h1; + $w = min($maxWidth - $marginWidth - $wCover, min($h1 * $ratios[2], $h0 * $ratios[1])); + + $result->colSizes = [ceil($wCover), ceil($w)]; + $result->rowSizes = [ceil($h0), ceil($h1)]; + $result->width = ceil($w + $wCover + $marginWidth); + $result->height = ceil($maxHeight); + $result->tiles = [ + new ThumbTile(1, 2, $wCover, $maxHeight), new ThumbTile(1, 1, $w, $h0), + new ThumbTile(1, 1, $w, $h1), + ]; + } + break; + case 4: + # Four wide photos, we will put one to the top and rest below + if($orients == [Makima::ORIENT_WIDE, Makima::ORIENT_WIDE, Makima::ORIENT_WIDE, Makima::ORIENT_WIDE]) { + $hCover = min($maxWidth / $ratios[0], ($maxHeight - $marginHeight) / (2 / 3)); + $h = ($maxWidth - 2 * $marginWidth) / (array_sum($ratios) - $ratios[0]); + $w0 = $h * $ratios[1]; + $w1 = $h * $ratios[2]; + $w2 = $h * $ratios[3]; + $h = min($maxHeight - $marginHeight - $hCover, $h); + + $result->colSizes = [ceil($w0), ceil($w1), ceil($w2)]; + $result->rowSizes = [ceil($hCover), ceil($h)]; + $result->width = ceil($maxWidth); + $result->height = ceil($hCover + $marginHeight + $h); + $result->tiles = [ + new ThumbTile(3, 1, $maxWidth, $hCover), + new ThumbTile(1, 1, $w0, $h), new ThumbTile(1, 1, $w1, $h), new ThumbTile(1, 1, $w2, $h), + ]; + } else /* Four photos, we will put one to the left and rest to the right */ { + $wCover = min($maxHeight * $ratios[0], ($maxWidth - $marginWidth) * (2 / 3)); + $w = ($maxHeight - 2 * $marginHeight) / (1 / $ratios[1] + 1 / $ratios[2] + 1 / $ratios[3]); + $h0 = $w / $ratios[1]; + $h1 = $w / $ratios[2]; + $h2 = $w / $ratios[3] + $marginHeight; + $w = min($w, $maxWidth - $marginWidth - $wCover); + + $result->colSizes = [ceil($wCover), ceil($w)]; + $result->rowSizes = [ceil($h0), ceil($h1), ceil($h2)]; + $result->width = ceil($wCover + $marginWidth + $w); + $result->height = ceil($maxHeight); + $result->tiles = [ + new ThumbTile(1, 3, $wCover, $maxHeight), new ThumbTile(1, 1, $w, $h0), + new ThumbTile(1, 1, $w, $h1), + new ThumbTile(1, 1, $w, $h1), + ]; + } + break; + default: + // как лопать пузырики + $ratiosCropped = []; + if($avgRatio > 1.1) { + foreach($ratios as $ratio) + $ratiosCropped[] = max($ratio, 1.0); + } else { + foreach($ratios as $ratio) + $ratiosCropped[] = min($ratio, 1.0); + } + + $tries = []; + + $firstLine; + $secondLine; + $thirdLine; + + # Try one line: + $tries[$firstLine = $count] = [$this->calculateMultiThumbsHeight($ratiosCropped, $maxWidth, $marginWidth)]; + + # Try two lines: + for($firstLine = 1; $firstLine < ($count - 1); $firstLine++) { + $secondLine = $count - $firstLine; + $key = "$firstLine&$secondLine"; + $tries[$key] = [ + $this->calculateMultiThumbsHeight(array_slice($ratiosCropped, 0, $firstLine), $maxWidth, $marginWidth), + $this->calculateMultiThumbsHeight(array_slice($ratiosCropped, $firstLine), $maxWidth, $marginWidth), + ]; + } + + # Try three lines: + for($firstLine = 1; $firstLine < ($count - 2); $firstLine++) { + for($secondLine = 1; $secondLine < ($count - $firstLine - 1); $secondLine++) { + $thirdLine = $count - $firstLine - $secondLine; + $key = "$firstLine&$secondLine&$thirdLine"; + $tries[$key] = [ + $this->calculateMultiThumbsHeight(array_slice($ratiosCropped, 0, $firstLine), $maxWidth, $marginWidth), + $this->calculateMultiThumbsHeight($this->extractSubArr($ratiosCropped, $firstLine, $firstLine + $secondLine), $maxWidth, $marginWidth), + $this->calculateMultiThumbsHeight($this->extractSubArr($ratiosCropped, $firstLine + $secondLine, sizeof($ratiosCropped)), $maxWidth, $marginWidth), + ]; + } + } + + # Now let's find the most optimal configuration: + $optimalConfiguration = $optimalDifference = NULL; + foreach($tries as $config => $heights) { + $config = explode('&', (string) $config); # да да стринговые ключи пхп даже со стриктайпами автокастует к инту (см. 187) + $confH = $marginHeight * (sizeof($heights) - 1); + foreach($heights as $h) + $confH += $h; + + $confDiff = abs($confH - $maxHeight); + if(sizeof($config) > 1) + if($config[0] > $config[1] || sizeof($config) >= 2 && $config[1] > $config[2]) + $confDiff *= 1.1; + + if(!$optimalConfiguration || $confDigff < $optimalDifference) { + $optimalConfiguration = $config; + $optimalDifference = $confDiff; + } + } + + $thumbsRemain = $this->photos; + $ratiosRemain = $ratiosCropped; + $optHeights = $tries[implode('&', $optimalConfiguration)]; + $k = 0; + + $result->width = ceil($maxWidth); + $result->rowSizes = [sizeof($optHeights)]; + $result->tiles = []; + + $totalHeight = 0.0; + $gridLineOffsets = []; + $rowTiles = []; // vector> + + for($i = 0; $i < sizeof($optimalConfiguration); $i++) { + $lineChunksNum = $optimalConfiguration[$i]; + $lineThumbs = []; + for($j = 0; $j < $lineChunksNum; $j++) + $lineThumbs[] = array_shift($thumbsRemain); + + $lineHeight = $optHeights[$i]; + $totalHeight += $lineHeight; + + $result->rowSizes[$i] = ceil($lineHeight); + + $totalWidth = 0; + $row = []; + for($j = 0; $j < sizeof($lineThumbs); $j++) { + $thumbRatio = array_shift($ratiosRemain); + if($j == sizeof($lineThumbs) - 1) + $w = $maxWidth - $totalWidth; + else + $w = $thumbRatio * $lineHeight; + + $totalWidth += ceil($w); + if($j < (sizeof($lineThumbs) - 1) && !in_array($totalWidth, $gridLineOffsets)) + $gridLineOffsets[] = $totalWidth; + + $tile = new ThumbTile(1, 1, $w, $lineHeight); + $result->tiles[$k++] = $row[] = $tile; + } + + $rowTiles[] = $row; + } + + sort($gridLineOffsets, SORT_NUMERIC); + $gridLineOffsets[] = $maxWidth; + + $result->colSizes = [$gridLineOffsets[0]]; + for($i = sizeof($gridLineOffsets) - 1; $i > 0; $i--) + $result->colSizes[$i] = $gridLineOffsets[$i] - $gridLineOffsets[$i - 1]; + + foreach($rowTiles as $row) { + $columnOffset = 0; + foreach($row as $tile) { + $startColumn = $columnOffset; + $width = 0; + $tile->colSpan = 0; + for($i = $startColumn; $i < sizeof($result->colSizes); $i++) { + $width += $result->colSizes[$i]; + $tile->colSpan++; + if($width == $tile->width) + break; + } + + $columnOffset += $tile->colSpan; + } + } + + $result->height = ceil($totalHeight + $marginHeight * (sizeof($optHeights) - 1)); + break; + } + + return $result; + } +} diff --git a/Web/Util/Makima/MasonryLayout.php b/Web/Util/Makima/MasonryLayout.php new file mode 100644 index 00000000..b23aa483 --- /dev/null +++ b/Web/Util/Makima/MasonryLayout.php @@ -0,0 +1,10 @@ +width, $this->height, $this->rowSpan, $this->colSpan] = [ceil($w), ceil($h), $rs, $cs]; + } +} diff --git a/Web/routes.yml b/Web/routes.yml index 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"), "
", [tr("save"), tr("cancel")], [function() { canvas.getImage({includeWatermark: false}).toBlob(blob => { let fName = "Graffiti-" + Math.ceil(performance.now()).toString() + ".jpeg"; let image = new File([blob], fName, {type: "image/jpeg", lastModified: new Date().getTime()}); - let trans = new DataTransfer(); - trans.items.add(image); - let fileSelect = document.querySelector("#post-buttons" + id + " input[name='_pic_attachment']"); - fileSelect.files = trans.files; - - u(fileSelect).trigger("change"); - u("#post-buttons" + id + " #write textarea").trigger("focusin"); + fastUploadImage(id, image) }, "image/jpeg", 0.92); canvas.teardown(); @@ -75,6 +52,79 @@ function initGraffiti(id) { }); } +function fastUploadImage(textareaId, file) { + // uploading images + + if(!file.type.startsWith('image/')) { + MessageBox(tr("error"), tr("only_images_accepted", escapeHtml(file.name)), [tr("ok")], [() => {Function.noop}]) + return; + } + + // 🤓🤓🤓 + if(file.size > 5 * 1024 * 1024) { + MessageBox(tr("error"), tr("max_filesize", 5), [tr("ok")], [() => {Function.noop}]) + return; + } + + let imagesCount = document.querySelector("#post-buttons" + textareaId + " input[name='photos']").value.split(",").length + + if(imagesCount > 10) { + MessageBox(tr("error"), tr("too_many_photos"), [tr("ok")], [() => {Function.noop}]) + return + } + + let xhr = new XMLHttpRequest + let data = new FormData + + data.append("photo_0", file) + data.append("count", 1) + data.append("hash", u("meta[name=csrf]").attr("value")) + + xhr.open("POST", "/photos/upload") + + xhr.onloadstart = () => { + document.querySelector("#post-buttons"+textareaId+" .upload").insertAdjacentHTML("beforeend", ``) + } + + xhr.onload = () => { + let response = JSON.parse(xhr.responseText) + + appendImage(response, textareaId) + } + + xhr.send(data) +} + +// append image after uploading via /photos/upload +function appendImage(response, textareaId) { + if(!response.success) { + MessageBox(tr("error"), (tr("error_uploading_photo") + response.flash.message), [tr("ok")], [() => {Function.noop}]) + } else { + let form = document.querySelector("#post-buttons"+textareaId) + let photosInput = form.querySelector("input[name='photos']") + let photosIndicator = form.querySelector(".upload") + + for(const phot of response.photos) { + let id = phot.owner + "_" + phot.vid + + photosInput.value += (id + ",") + + u(photosIndicator).append(u(` +
+ × + +
+ `)) + + u(photosIndicator.querySelector(`.upload #aP[data-id='${id}'] .upload-delete`)).on("click", () => { + photosInput.value = photosInput.value.replace(id + ",", "") + u(form.querySelector(`.upload #aP[data-id='${id}']`)).remove() + }) + } + } + u(`#post-buttons${textareaId} .upload #loader`).remove() +} + u(".post-like-button").on("click", function(e) { e.preventDefault(); @@ -97,11 +147,12 @@ u(".post-like-button").on("click", function(e) { function setupWallPostInputHandlers(id) { u("#wall-post-input" + id).on("paste", function(e) { + // Если вы находитесь на странице с постом с id 11, то копирование произойдёт джва раза. + // Оч ржачный баг, но вот как его исправить, я, если честно, не знаю. + if(e.clipboardData.files.length === 1) { - var input = u("#post-buttons" + id + " input[name=_pic_attachment]").nodes[0]; - input.files = e.clipboardData.files; - - u(input).trigger("change"); + fastUploadImage(id, e.clipboardData.files[0]) + return; } }); @@ -116,6 +167,183 @@ function setupWallPostInputHandlers(id) { // revert to original size if it is larger (possibly changed by user) // textArea.style.height = (newHeight > originalHeight ? (newHeight + boost) : originalHeight) + "px"; }); + + u("#wall-post-input" + id).on("dragover", function(e) { + e.preventDefault() + + // todo add animation + return; + }); + + $("#wall-post-input" + id).on("drop", function(e) { + e.originalEvent.dataTransfer.dropEffect = 'move'; + fastUploadImage(id, e.originalEvent.dataTransfer.files[0]) + return; + }); +} + +function OpenMiniature(e, photo, post, photo_id, type = "post") { + /* + костыли но смешные однако + */ + e.preventDefault(); + + if(u(".ovk-photo-view").length > 0) u(".ovk-photo-view-dimmer").remove(); + + // Значения для переключения фоток + + let json; + + let imagesCount = 0; + let imagesIndex = 0; + + let tempDetailsSection = []; + + let dialog = u( + `
+
+
+ + + + +
+
+
+
+ +
+
+ +
+
+
`); + u("body").addClass("dimmed").append(dialog); + document.querySelector("html").style.overflowY = "hidden" + + let button = u("#ovk-photo-close"); + + button.on("click", function(e) { + let __closeDialog = () => { + u("body").removeClass("dimmed"); + u(".ovk-photo-view-dimmer").remove(); + document.querySelector("html").style.overflowY = "scroll" + }; + + __closeDialog(); + }); + + function __reloadTitleBar() { + u("#photo_com_title_photos").last().innerHTML = imagesCount > 1 ? tr("photo_x_from_y", imagesIndex, imagesCount) : tr("photo"); + } + + function __loadDetails(photo_id, index) { + if(tempDetailsSection[index] == null) { + u(".ovk-photo-details").last().innerHTML = ''; + ky("/photo" + photo_id, { + hooks: { + afterResponse: [ + async (_request, _options, response) => { + let parser = new DOMParser(); + let body = parser.parseFromString(await response.text(), "text/html"); + + let element = u(body.getElementsByClassName("ovk-photo-details")).last(); + + tempDetailsSection[index] = element.innerHTML; + + if(index == imagesIndex) { + u(".ovk-photo-details").last().innerHTML = element.innerHTML; + } + + document.querySelectorAll(".ovk-photo-details .bsdn").forEach(bsdnInitElement) + document.querySelectorAll(".ovk-photo-details script").forEach(scr => { + // stolen from #953 + let newScr = document.createElement('script') + + if(scr.src) { + newScr.src = scr.src + } else { + newScr.textContent = scr.textContent + } + + document.querySelector(".ovk-photo-details").appendChild(newScr); + }) + } + ] + } + }); + } else { + u(".ovk-photo-details").last().innerHTML = tempDetailsSection[index]; + } + } + + function __slidePhoto(direction) { + /* direction = 1 - right + direction = 0 - left */ + if(json == undefined) { + console.log("Да подожди ты. Куда торопишься?"); + } else { + if(imagesIndex >= imagesCount && direction == 1) { + imagesIndex = 1; + } else if(imagesIndex <= 1 && direction == 0) { + imagesIndex = imagesCount; + } else if(direction == 1) { + imagesIndex++; + } else if(direction == 0) { + imagesIndex--; + } + + let photoURL = json.body[imagesIndex - 1].url; + + u("#ovk-photo-img").last().src = photoURL; + __reloadTitleBar(); + __loadDetails(json.body[imagesIndex - 1].id, imagesIndex); + } + } + + let slideLeft = u(".ovk-photo-slide-left"); + + slideLeft.on("click", (e) => { + __slidePhoto(0); + }); + + let slideRight = u(".ovk-photo-slide-right"); + + slideRight.on("click", (e) => { + __slidePhoto(1); + }); + + let data = new FormData() + data.append('parentType', type); + ky.post("/iapi/getPhotosFromPost/" + (type == "post" ? post : "1_"+post), { + hooks: { + afterResponse: [ + async (_request, _options, response) => { + json = await response.json(); + + imagesCount = json.body.length; + imagesIndex = 0; + // Это всё придётся правда на 1 прибавлять + + json.body.every(element => { + imagesIndex++; + if(element.id == photo_id) { + return false; + } else { + return true; + } + }); + + __reloadTitleBar(); + __loadDetails(json.body[imagesIndex - 1].id, imagesIndex); } + ] + }, + body: data + }); + + return u(".ovk-photo-view-dimmer"); } u("#write > form").on("keydown", function(event) { @@ -210,6 +438,7 @@ function addNote(textareaId, nid) u("body").removeClass("dimmed"); u(".ovk-diag-cont").remove(); + document.querySelector("html").style.overflowY = "scroll" } async function attachNote(id) @@ -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 = ` +
+
+ ${tr("upload_new_photo")}: + + + +
+
+ +
+

${tr("is_x_photos", 0)}

+
+
+ ` + + let form = e.currentTarget.closest("form") + + MessageBox(tr("select_photo"), body, [tr("close")], [Function.noop]); + + document.querySelector(".ovk-diag-body").style.padding = "0" + document.querySelector(".ovk-diag-cont").style.width = "630px" + document.querySelector(".ovk-diag-body").style.height = "335px" + + async function insertPhotos(page, album = 0) { + u("#loader").remove() + + let insertPlace = document.querySelector(".photosInsert .photosList") + document.querySelector(".photosInsert").insertAdjacentHTML("beforeend", ``) + + let photos; + + try { + photos = await API.Photos.getPhotos(page, Number(album)) + } catch(e) { + document.querySelector(".photosInsert h4").innerHTML = tr("is_x_photos", -1) + insertPlace.innerHTML = "Invalid album" + console.error(e) + u("#loader").remove() + return; + } + + document.querySelector(".photosInsert h4").innerHTML = tr("is_x_photos", photos.count) + + let pagesCount = Math.ceil(Number(photos.count) / 24) + u("#loader").remove() + + for(const photo of photos.items) { + let isAttached = (form.querySelector("input[name='photos']").value.includes(`${photo.owner_id}_${photo.id},`)) + + insertPlace.insertAdjacentHTML("beforeend", ` +
+ + ... + +
+ `) + } + + if(page < pagesCount) { + insertPlace.insertAdjacentHTML("beforeend", ` +
+ more... +
`) + } + } + + insertPhotos(1) + + let albums = await API.Photos.getAlbums(Number(e.currentTarget.dataset.club ?? 0)) + + for(const alb of albums.items) { + let sel = document.querySelector(".ovk-diag-body #albumSelect") + + sel.insertAdjacentHTML("beforeend", ``) + } + + $(".photosInsert").on("click", "#showMorePhotos", (e) => { + u(e.currentTarget).remove() + insertPhotos(Number(e.currentTarget.dataset.page), document.querySelector(".topGrayBlock #albumSelect").value) + }) + + $(".topGrayBlock #albumSelect").on("change", (evv) => { + document.querySelector(".photosInsert .photosList").innerHTML = "" + + insertPhotos(1, evv.currentTarget.value) + }) + + function insertAttachment(id) { + let photos = form.querySelector("input[name='photos']") + + if(!photos.value.includes(id + ",")) { + if(photos.value.split(",").length > 10) { + NewNotification(tr("error"), tr("max_attached_photos")) + return false + } + + form.querySelector("input[name='photos']").value += (id + ",") + + console.info(id + " attached") + return true + } else { + form.querySelector("input[name='photos']").value = form.querySelector("input[name='photos']").value.replace(id + ",", "") + + console.info(id + " detached") + return false + } + } + + $(".photosList").on("click", ".album-photo", (ev) => { + ev.preventDefault() + + if(!insertAttachment(ev.currentTarget.dataset.attachmentdata)) { + u(form.querySelector(`.upload #aP[data-id='${ev.currentTarget.dataset.attachmentdata}']`)).remove() + ev.currentTarget.querySelector("img").style.backgroundColor = "white" + } else { + ev.currentTarget.querySelector("img").style.backgroundColor = "#646464" + let id = ev.currentTarget.dataset.attachmentdata + + u(form.querySelector(`.upload`)).append(u(` +
+ × + +
+ `)); + + u(`.upload #aP[data-id='${ev.currentTarget.dataset.attachmentdata}'] .upload-delete`).on("click", () => { + form.querySelector("input[name='photos']").value = form.querySelector("input[name='photos']").value.replace(id + ",", "") + u(form.querySelector(`.upload #aP[data-id='${ev.currentTarget.dataset.attachmentdata}']`)).remove() + }) + } + }) + + u("#fastFotosUplod").on("change", (evn) => { + let xhr = new XMLHttpRequest() + xhr.open("POST", "/photos/upload") + + let formdata = new FormData() + let iterator = 0 + + for(const fille of evn.currentTarget.files) { + if(!fille.type.startsWith('image/')) { + continue; + } + + if(fille.size > 5 * 1024 * 1024) { + continue; + } + + if(evn.currentTarget.files.length >= 10) { + NewNotification(tr("error"), tr("max_attached_photos")) + return; + } + + formdata.append("photo_"+iterator, fille) + iterator += 1 + } + + xhr.onloadstart = () => { + evn.currentTarget.parentNode.insertAdjacentHTML("beforeend", ``) + } + + xhr.onload = () => { + let result = JSON.parse(xhr.responseText) + + u("#loader").remove() + if(result.success) { + for(const pht of result.photos) { + let id = pht.owner + "_" + pht.vid + + if(!insertAttachment(id)) { + return + } + + u(form.querySelector(`.upload`)).append(u(` +
+ × + +
+ `)); + + u(`.upload #aP[data-id='${pht.owner + "_" + pht.vid}'] .upload-delete`).on("click", () => { + form.querySelector("input[name='photos']").value = form.querySelector("input[name='photos']").value.replace(id + ",", "") + u(form.querySelector(`.upload #aP[data-id='${id}']`)).remove() + }) + } + + u("body").removeClass("dimmed"); + u(".ovk-diag-cont").remove(); + document.querySelector("html").style.overflowY = "scroll" + } else { + // todo: https://vk.com/wall-32295218_78593 + alert(result.flash.message) + } + } + + formdata.append("hash", u("meta[name=csrf]").attr("value")) + formdata.append("count", iterator) + + xhr.send(formdata) + }) +}) diff --git a/Web/static/js/messagebox.js b/Web/static/js/messagebox.js index 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 = ""; formHtml += ""; formHtml += ""; 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" = "Нет результатов."; From ab1c6dc84356728294228aa404d31b85ef5d89ee Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Wed, 11 Oct 2023 18:21:30 +0300 Subject: [PATCH 035/231] Fix for deleting photos that don't have albums --- Web/Presenters/PhotosPresenter.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Web/Presenters/PhotosPresenter.php b/Web/Presenters/PhotosPresenter.php index 0a8b87e4..aeb8ba1e 100644 --- a/Web/Presenters/PhotosPresenter.php +++ b/Web/Presenters/PhotosPresenter.php @@ -336,7 +336,10 @@ final class PhotosPresenter extends OpenVKPresenter if(is_null($this->user) || $this->user->id != $ownerId) $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); - $redirect = $photo->getAlbum()->getOwner() instanceof User ? "/id0" : "/club" . $ownerId; + if(!is_null($album = $photo->getAlbum())) + $redirect = $album->getOwner() instanceof User ? "/id0" : "/club" . $ownerId; + else + $redirect = "/id0"; $photo->isolate(); $photo->delete(); From e3311fbf972d03cf5d6f02d991be6407ed3dfe66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BA=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D1=87=D1=83=D0=BA?= =?UTF-8?q?=D1=87=D0=B0?= <147275844+JBLHRD@users.noreply.github.com> Date: Mon, 16 Oct 2023 00:51:50 +0200 Subject: [PATCH 036/231] Locales: Update ukrainian localization (#993) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add localization uk.strings * Чорт забирай! * LanguageTool №1 * Чорт забирай! №2 --- locales/uk.strings | 311 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 309 insertions(+), 2 deletions(-) diff --git a/locales/uk.strings b/locales/uk.strings index 7ccfb367..b2b8a914 100644 --- a/locales/uk.strings +++ b/locales/uk.strings @@ -134,6 +134,10 @@ "updated_at" = "Оновлено $1"; "user_banned" = "На жаль, нам довелося заблокувати сторінку користувача $1."; "user_banned_comment" = "Коментар модератора:"; +"verified_page" = "Верифікована сторінка"; +"user_is_blocked" = "Користувача заблоковано"; +"before" = "до"; +"forever" = "назавжди"; /* Wall */ @@ -184,6 +188,7 @@ "nsfw_warning" = "Даний запис може містити контент 18+"; "report" = "Поскаржитися"; "attach" = "Прикріпити"; +"detach" = "Відкріпити"; "attach_photo" = "Прикріпити фото"; "attach_photo" = "Прикріпити відео"; "draw_graffiti" = "Намалювати графіті"; @@ -193,6 +198,8 @@ "version_incompatibility" = "Не вдалося показати це вкладення. Можливо, база даних несумісна з поточною версією OpenVK."; "graffiti" = "Графіті"; "reply" = "Відповісти"; +"post_is_ad" = "Цей пост було проплачено Держдепом США"; +"edited_short" = "ред."; /* Friends */ @@ -317,14 +324,21 @@ "search_group" = "Пошук групи"; "search_by_groups" = "Пошук за групами"; "search_group_desc" = "Тут Ви можете переглянути існуючи групи та обрати групу до вподоби."; +"group_banned" = "Спільнота $1 заблокована." /* Albums */ "create" = "Створити"; +"album" = "Альбом"; "albums" = "Альбоми"; +"photos" = "фотографій"; +"photo" = "Фотографія"; "create_album" = "Створити альбом"; "edit_album" = "Редагувати альбом"; +"edit_photo" = "Змінити фотографію"; "creating_album" = "Створення альбому"; +"delete_photo" = "Видалити фотографію"; +"sure_deleting_photo" = "Ви впевнені, що бажаєте видалити цю світлину?"; "upload_photo" = "Завантажити фотографію"; "photo" = "Фотографія"; "upload_button" = "Завантажити"; @@ -366,6 +380,39 @@ "upd_f" = "оновила фотографію на своїй сторінці"; "upd_g" = "оновило фотографію групи"; +"add_photos" = "Додати фотографії"; +"upload_picts" = "Завантажити фотографії"; +"end_uploading" = "Завершити завантаження"; +"photos_successfully_uploaded" = "Фотографії було завантажено"; +"click_to_go_to_album" = "Натисніть, щоб перейти до альбому."; +"error_uploading_photo" = "Помилка завантаження фотографії"; +"too_many_pictures" = "Не більше 10 фотографій"; + +"drag_files_here" = "Перетягніть файли сюди"; +"only_images_accepted" = "Файл \"$1\" не є зображенням"; +"max_filesize" = "Максимальний розмір файлу — $1 мегабайт"; + +"uploading_photos_from_computer" = "Завантаження фотографій з Вашого ПК"; +"supported_formats" = "Підтримувані формати зображень: JPG, PNG й GIF."; +"max_load_photos" = "Ви можете завантажити до 10 фотографій за один раз."; +"tip" = "Порада"; +"tip_ctrl" = "щоб обрати кілька фотографій одразу, утримуйте клавішу Ctrl під час вибору файлів в OS Windows або клавішу CMD у Mac OS."; +"album_poster" = "Обкладинка альбому"; +"select_photo" = "Оберіть фотографію"; +"upload_new_photo" = "Завантажте нову світлину"; + +"is_x_photos_zero" = "Усього 0 фотографій."; +"is_x_photos_one" = "Всього 1 фотографія."; +"is_x_photos_few" = "Всього $1 фотографій."; +"is_x_photos_many" = "Всього $1 фотографій."; +"is_x_photos_other" = "Всього $1 фотографій."; + +"all_photos" = "Всі фотографії"; +"error_uploading_photo" = "Помилка завантаження фотографії. Текст помилки: "; +"too_many_photos" = "Надто багато фотографій."; + +"photo_x_from_y" = "Фотографія $1 з $2"; + /* Notes */ "notes" = "Нотатки"; @@ -395,6 +442,18 @@ /* Notes: Article Viewer */ "aw_legacy_ui" = "Класичне дієвидло"; +"select_note" = "Вибір нотатки"; +"no_notes" = "Ви не маєте жодної нотатки"; + +"error_attaching_note" = "Не вдалося закріпити нотатку"; + +"select_or_create_new" = "Оберіть існуючу нотатку або створіть нову"; + +"notes_closed" = "Ви не можете прикріпити нотатку до запису, оскільки ваші нотатки видно тільки вам.

Ви можете змінити це в налаштуваннях."; +"do_not_attach_note" = "Не прикріплювати нотатку"; +"something" = "Щось"; +"supports_xhtml" = "з (X)HTML підтримується."; + /* Menus */ "edit_button" = "ред."; @@ -489,7 +548,7 @@ "privacy_value_friends" = "Друзі"; "privacy_value_friends_dative" = "Друзям"; "privacy_value_only_me" = "Тільки я"; -"privacy_value_only_me_dative" = "Тільки мені"; +"privacy_value_only_me_dative" = "Тільки мені та Кирилу Буданову"; "privacy_value_nobody" = "Ніхто"; "your_email_address" = "Адрес Вашої електронної пошти"; "your_page_address" = "Адрес Вашої сторінки"; @@ -581,6 +640,9 @@ "two_factor_authentication_backup_codes_1" = "Резервні коди дозволяють підтверджувати вхід, коли у вас немає доступу до телефону, наприклад, у подорожі."; "two_factor_authentication_backup_codes_2" = "У вас є ще 10 кодів, кожним кодом можна скористатися тільки один раз. Надрукуйте їх, приберіть в надійне місце і використовуйте, коли будуть потрібні коди для підтвердження входу."; "two_factor_authentication_backup_codes_3" = "Ви можете отримати нові коди, якщо вони закінчуються. Дійсні тільки останні створені резервні коди."; +"viewing_backup_codes" = "Перегляд резервних кодів"; +"disable_2fa" = "Вимкнути 2FA"; +"viewing" = "Переглянути"; /* Sorting */ @@ -606,6 +668,14 @@ "videos_many" = "$1 відеозаписів"; "videos_other" = "$1 відеозаписів"; "view_video" = "Перегляд"; +"change_video" = "Редагувати відеозапис"; +"unknown_video" = "ЦЕЙ ВІДЕОЗАПИС НЕ ПІДТРИМУЄТЬСЯ В ЦІЙ ВЕРСІЇ OPENVK."; +"selecting_video" = "Вибір відеозаписів"; +"upload_new_video" = "Завантажити нове відео"; +"max_attached_videos" = "Максимум 10 відеозаписів"; +"max_attached_photos" = "Максимум 10 фотографій"; +"no_videos" = "Ви не маєте відео."; +"no_videos_results" = "Немає результатів."; /* Notifications */ @@ -642,6 +712,7 @@ "nt_mention_in_video" = "в обговоренні відеозапису"; "nt_mention_in_note" = "в обговоренні під"; "nt_mention_in_topic" = "в обговоренні"; +"nt_sent_gift" = "відправив вам подарунок"; /* Time */ @@ -815,6 +886,7 @@ "support_new" = "Нове звернення"; "support_new_title" = "Введіть тему вашого звернення"; "support_new_content" = "Опишіть проблему чи пропозицію"; +"reports" = "Скарги"; "support_rate_good_answer" = "Це хороша відповідь"; "support_rate_bad_answer" = "Це погана відповідь"; "support_good_answer_user" = "Ви залишили позитивний відгук."; @@ -825,6 +897,26 @@ "support_rated_bad" = "Ви залишили негативний відгук про відповідь."; "wrong_parameters" = "Неправильні параметри запиту."; "fast_answers" = "Швидкі відповіді"; +"ignore_report" = "Ігнорувати скаргу"; +"report_number" = "Скарга №"; +"list_of_reports" = "Лист скарг"; +"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" = "Автор"; @@ -834,6 +926,12 @@ "ticket_changed_comment" = "Зміни набудуть чинності через кілька секунд."; "banned_in_support_1" = "Вибачте, $1, але тепер вам не можна створювати звернення."; "banned_in_support_2" = "Підстава: $1. Цього разу нам довелося забрати у вас цю можливість назавжди."; +"you_can_close_this_ticket_1" = "Якщо ви не маєте запитань, Ви можете "; +"you_can_close_this_ticket_2" = "закрити це звернення"; +"agent_profile_created_1" = "Профіль створено"; +"agent_profile_created_2" = "Тепер користувачі бачать Ваш псевдонім і аватар замість стандартного поличчя та ID."; +"agent_profile_edited" = "Профіль відредагований"; +"agent_profile" = "Картка агента"; /* Invite */ @@ -958,6 +1056,7 @@ "error_repost_fail" = "Не вдалося поділитися записом"; "error_data_too_big" = "Атрибут '$1' не може бути довше $2 $3"; "forbidden" = "Помилка доступу"; +"unknown_error" = "Невідома помилка"; "forbidden_comment" = "Налаштування приватності цього користувача не дозволяють дивитися на його сторінку."; "changes_saved" = "Зміни збережені"; "changes_saved_comment" = "Нові дані з'являться на вашій сторінці"; @@ -993,6 +1092,96 @@ "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" = "АХАХАХА ЛОШАРА ПІЗДУЙ НА ЗАРОБІТКИ У ПОЛЬЩУ"; +/* трррр шкібіді доп доп доп доп єс єс єс) */ + +"description_too_long" = "Опис надто довгий."; + +"gift_sent" = "Подарунок відправлено"; +"gift_sent_desc" = "Ви відправили $1 за $2 голосів"; + +"error_on_server_side" = "Виникла помилка на боці сервера. Зверніться до системного адміністратора."; +"error_no_group_name" = "Ви не ввели назву групи."; + +"success_action" = "Операція пройшла успішно"; +"connection_error" = "Помилка з'єднання"; +"connection_error_desc" = "Не вдалося з'єднатися до служби телеметрії"; + +"error_when_uploading_photo" = "Не вдалося зберегти фотографію."; + +"new_changes_desc" = "Нові дані з'являться у вашій групі."; +"comment_is_changed" = "Коментар до адміністратора змінено"; +"comment_is_deleted" = "Коментар до адміністратора видалено"; +"comment_is_too_long" = "Коментар надто довгий ($1 символів замість 36 символів)"; +"x_no_more_admin" = "$1 більше не є адміністратором."; +"x_is_admin" = "$1 призначено адміністратором."; + +"x_is_now_hidden" = "Тепер $1 буде відображатися як звичайний підписник усім, окрім інших адміністраторів"; +"x_is_now_showed" = "Тепер $1 буде відображатися як звичайний адміністратор."; + +"note_is_deleted" = "Нотатка видалена"; +"note_x_is_now_deleted" = "Нотатка \"$1\" була успішно видалена."; +"new_data_accepted" = "Нові дані прийняті."; + +"album_is_deleted" = "Альбом видалено"; +"album_x_is_deleted" = "Альбом $1 було видалено."; + +"error_adding_to_deleted" = "Не вдалося зберегти фотографію у DELETED."; +"error_adding_to_x" = "Не вдалося зберегти фотографію в $1."; +"no_photo" = "Нема фотографій"; + +"select_file" = "Оберіть файл"; +"new_description_will_appear" = "Оновлений опис з'явиться на сторінці з фото."; +"photo_is_deleted" = "Фотографія видалена"; +"photo_is_deleted_desc" = "Ця світлина була успішно видалена."; + +"no_video" = "Немає відеозапису"; +"no_video_desc" = "Оберіть файл або вкажіть URL."; +"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" = "Спільноту успішно заблоковано"; /* Admin actions */ @@ -1000,14 +1189,18 @@ "manage_user_action" = "Керування користувачем"; "manage_group_action" = "Керування групою"; "ban_user_action" = "Заблокувати користувача"; +"blocks" = "Блокування"; +"last_actions" = "Останні дії"; "unban_user_action" = "Розблокувати користувача"; "warn_user_action" = "Попередити користувача"; "ban_in_support_user_action" = "Заблокувати у тех.підтримці"; "unban_in_support_user_action" = "Розблокувати у тех.підтримці"; +"changes_history" = "Історія редагування"; /* Admin panel */ "admin" = "Адмін панель"; +"sandbox_for_developers" = "Sandbox для розробників"; "admin_ownerid" = "ID власника"; "admin_author" = "Автор"; "admin_name" = "Ім'я"; @@ -1082,6 +1275,40 @@ "admin_banned_link_initiator" = "Ініціатор"; "admin_banned_link_not_specified" = "Посилання не зазначено"; "admin_banned_link_not_found" = "Посилання не знайдено"; +"admin_gift_moved_successfully" = "Подарунок успішно переміщено"; +"admin_gift_moved_to_recycle" = "Тепер подарунок у кошику."; +"logs" = "Логи"; +"logs_anything" = "Будь-яке"; +"logs_adding" = "Створення"; +"logs_editing" = "Редагування"; +"logs_removing" = "Видалення"; +"logs_restoring" = "Відновлення"; +"logs_added" = "додав"; +"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) */ @@ -1130,7 +1357,9 @@ "transfer" = "Передати"; "close" = "Закрити"; "warning" = "Увага"; -"question_confirm" = "Цю дію не можна скасувати. Ви дійсно впевнені, що хочете зробити?"; +"question_confirm" = "Цю дію не можна скасувати. Ви переконані що хочете це зробити?"; +"confirm_m" = "Підтвердити"; +"action_successfully" = "Операція виконана успішно"; /* User alerts */ @@ -1144,6 +1373,8 @@ /* Away */ +"transition_is_blocked" = "Перехід за посиланням заборонений"; +"caution" = "Попередження"; "url_is_banned" = "Перехід неможливий"; "url_is_banned_comment" = "Адміністрація $1 не рекомендує переходити за цим посиланням."; "url_is_banned_comment_r" = "Адміністрація $1 не рекомендує переходити за цим посиланням.

Підстава: $2"; @@ -1151,6 +1382,8 @@ "url_is_banned_title" = "Посилання на підозрілий сайт"; "url_is_banned_proceed" = "Перейти за посиланням"; +"recently" = "Нещодавно"; + /* Chandler */ "c_user_removed_from_group" = "Користувача було видалено з групи"; @@ -1422,6 +1655,43 @@ "closed_group_post" = "Цей допис з приватної групи"; "deleted_target_comment" = "Цей коментар належить до видаленого допису"; +"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" = "Друзі"; @@ -1438,3 +1708,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 fef0203aa4b2cd9abf54171bad424670b18638d9 Mon Sep 17 00:00:00 2001 From: Dmitry Tretyakov <76806170+tretdm@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:02:39 +0700 Subject: [PATCH 037/231] UPD code --- Web/Presenters/PhotosPresenter.php | 5 +- locales/uk.strings | 311 +---------------------------- 2 files changed, 3 insertions(+), 313 deletions(-) diff --git a/Web/Presenters/PhotosPresenter.php b/Web/Presenters/PhotosPresenter.php index aeb8ba1e..0a8b87e4 100644 --- a/Web/Presenters/PhotosPresenter.php +++ b/Web/Presenters/PhotosPresenter.php @@ -336,10 +336,7 @@ final class PhotosPresenter extends OpenVKPresenter if(is_null($this->user) || $this->user->id != $ownerId) $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); - if(!is_null($album = $photo->getAlbum())) - $redirect = $album->getOwner() instanceof User ? "/id0" : "/club" . $ownerId; - else - $redirect = "/id0"; + $redirect = $photo->getAlbum()->getOwner() instanceof User ? "/id0" : "/club" . $ownerId; $photo->isolate(); $photo->delete(); diff --git a/locales/uk.strings b/locales/uk.strings index b2b8a914..7ccfb367 100644 --- a/locales/uk.strings +++ b/locales/uk.strings @@ -134,10 +134,6 @@ "updated_at" = "Оновлено $1"; "user_banned" = "На жаль, нам довелося заблокувати сторінку користувача $1."; "user_banned_comment" = "Коментар модератора:"; -"verified_page" = "Верифікована сторінка"; -"user_is_blocked" = "Користувача заблоковано"; -"before" = "до"; -"forever" = "назавжди"; /* Wall */ @@ -188,7 +184,6 @@ "nsfw_warning" = "Даний запис може містити контент 18+"; "report" = "Поскаржитися"; "attach" = "Прикріпити"; -"detach" = "Відкріпити"; "attach_photo" = "Прикріпити фото"; "attach_photo" = "Прикріпити відео"; "draw_graffiti" = "Намалювати графіті"; @@ -198,8 +193,6 @@ "version_incompatibility" = "Не вдалося показати це вкладення. Можливо, база даних несумісна з поточною версією OpenVK."; "graffiti" = "Графіті"; "reply" = "Відповісти"; -"post_is_ad" = "Цей пост було проплачено Держдепом США"; -"edited_short" = "ред."; /* Friends */ @@ -324,21 +317,14 @@ "search_group" = "Пошук групи"; "search_by_groups" = "Пошук за групами"; "search_group_desc" = "Тут Ви можете переглянути існуючи групи та обрати групу до вподоби."; -"group_banned" = "Спільнота $1 заблокована." /* Albums */ "create" = "Створити"; -"album" = "Альбом"; "albums" = "Альбоми"; -"photos" = "фотографій"; -"photo" = "Фотографія"; "create_album" = "Створити альбом"; "edit_album" = "Редагувати альбом"; -"edit_photo" = "Змінити фотографію"; "creating_album" = "Створення альбому"; -"delete_photo" = "Видалити фотографію"; -"sure_deleting_photo" = "Ви впевнені, що бажаєте видалити цю світлину?"; "upload_photo" = "Завантажити фотографію"; "photo" = "Фотографія"; "upload_button" = "Завантажити"; @@ -380,39 +366,6 @@ "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 під час вибору файлів в OS Windows або клавішу CMD у Mac OS."; -"album_poster" = "Обкладинка альбому"; -"select_photo" = "Оберіть фотографію"; -"upload_new_photo" = "Завантажте нову світлину"; - -"is_x_photos_zero" = "Усього 0 фотографій."; -"is_x_photos_one" = "Всього 1 фотографія."; -"is_x_photos_few" = "Всього $1 фотографій."; -"is_x_photos_many" = "Всього $1 фотографій."; -"is_x_photos_other" = "Всього $1 фотографій."; - -"all_photos" = "Всі фотографії"; -"error_uploading_photo" = "Помилка завантаження фотографії. Текст помилки: "; -"too_many_photos" = "Надто багато фотографій."; - -"photo_x_from_y" = "Фотографія $1 з $2"; - /* Notes */ "notes" = "Нотатки"; @@ -442,18 +395,6 @@ /* Notes: Article Viewer */ "aw_legacy_ui" = "Класичне дієвидло"; -"select_note" = "Вибір нотатки"; -"no_notes" = "Ви не маєте жодної нотатки"; - -"error_attaching_note" = "Не вдалося закріпити нотатку"; - -"select_or_create_new" = "Оберіть існуючу нотатку або створіть нову"; - -"notes_closed" = "Ви не можете прикріпити нотатку до запису, оскільки ваші нотатки видно тільки вам.

Ви можете змінити це в налаштуваннях."; -"do_not_attach_note" = "Не прикріплювати нотатку"; -"something" = "Щось"; -"supports_xhtml" = "з (X)HTML підтримується."; - /* Menus */ "edit_button" = "ред."; @@ -548,7 +489,7 @@ "privacy_value_friends" = "Друзі"; "privacy_value_friends_dative" = "Друзям"; "privacy_value_only_me" = "Тільки я"; -"privacy_value_only_me_dative" = "Тільки мені та Кирилу Буданову"; +"privacy_value_only_me_dative" = "Тільки мені"; "privacy_value_nobody" = "Ніхто"; "your_email_address" = "Адрес Вашої електронної пошти"; "your_page_address" = "Адрес Вашої сторінки"; @@ -640,9 +581,6 @@ "two_factor_authentication_backup_codes_1" = "Резервні коди дозволяють підтверджувати вхід, коли у вас немає доступу до телефону, наприклад, у подорожі."; "two_factor_authentication_backup_codes_2" = "У вас є ще 10 кодів, кожним кодом можна скористатися тільки один раз. Надрукуйте їх, приберіть в надійне місце і використовуйте, коли будуть потрібні коди для підтвердження входу."; "two_factor_authentication_backup_codes_3" = "Ви можете отримати нові коди, якщо вони закінчуються. Дійсні тільки останні створені резервні коди."; -"viewing_backup_codes" = "Перегляд резервних кодів"; -"disable_2fa" = "Вимкнути 2FA"; -"viewing" = "Переглянути"; /* Sorting */ @@ -668,14 +606,6 @@ "videos_many" = "$1 відеозаписів"; "videos_other" = "$1 відеозаписів"; "view_video" = "Перегляд"; -"change_video" = "Редагувати відеозапис"; -"unknown_video" = "ЦЕЙ ВІДЕОЗАПИС НЕ ПІДТРИМУЄТЬСЯ В ЦІЙ ВЕРСІЇ OPENVK."; -"selecting_video" = "Вибір відеозаписів"; -"upload_new_video" = "Завантажити нове відео"; -"max_attached_videos" = "Максимум 10 відеозаписів"; -"max_attached_photos" = "Максимум 10 фотографій"; -"no_videos" = "Ви не маєте відео."; -"no_videos_results" = "Немає результатів."; /* Notifications */ @@ -712,7 +642,6 @@ "nt_mention_in_video" = "в обговоренні відеозапису"; "nt_mention_in_note" = "в обговоренні під"; "nt_mention_in_topic" = "в обговоренні"; -"nt_sent_gift" = "відправив вам подарунок"; /* Time */ @@ -886,7 +815,6 @@ "support_new" = "Нове звернення"; "support_new_title" = "Введіть тему вашого звернення"; "support_new_content" = "Опишіть проблему чи пропозицію"; -"reports" = "Скарги"; "support_rate_good_answer" = "Це хороша відповідь"; "support_rate_bad_answer" = "Це погана відповідь"; "support_good_answer_user" = "Ви залишили позитивний відгук."; @@ -897,26 +825,6 @@ "support_rated_bad" = "Ви залишили негативний відгук про відповідь."; "wrong_parameters" = "Неправильні параметри запиту."; "fast_answers" = "Швидкі відповіді"; -"ignore_report" = "Ігнорувати скаргу"; -"report_number" = "Скарга №"; -"list_of_reports" = "Лист скарг"; -"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" = "Автор"; @@ -926,12 +834,6 @@ "ticket_changed_comment" = "Зміни набудуть чинності через кілька секунд."; "banned_in_support_1" = "Вибачте, $1, але тепер вам не можна створювати звернення."; "banned_in_support_2" = "Підстава: $1. Цього разу нам довелося забрати у вас цю можливість назавжди."; -"you_can_close_this_ticket_1" = "Якщо ви не маєте запитань, Ви можете "; -"you_can_close_this_ticket_2" = "закрити це звернення"; -"agent_profile_created_1" = "Профіль створено"; -"agent_profile_created_2" = "Тепер користувачі бачать Ваш псевдонім і аватар замість стандартного поличчя та ID."; -"agent_profile_edited" = "Профіль відредагований"; -"agent_profile" = "Картка агента"; /* Invite */ @@ -1056,7 +958,6 @@ "error_repost_fail" = "Не вдалося поділитися записом"; "error_data_too_big" = "Атрибут '$1' не може бути довше $2 $3"; "forbidden" = "Помилка доступу"; -"unknown_error" = "Невідома помилка"; "forbidden_comment" = "Налаштування приватності цього користувача не дозволяють дивитися на його сторінку."; "changes_saved" = "Зміни збережені"; "changes_saved_comment" = "Нові дані з'являться на вашій сторінці"; @@ -1092,96 +993,6 @@ "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" = "АХАХАХА ЛОШАРА ПІЗДУЙ НА ЗАРОБІТКИ У ПОЛЬЩУ"; -/* трррр шкібіді доп доп доп доп єс єс єс) */ - -"description_too_long" = "Опис надто довгий."; - -"gift_sent" = "Подарунок відправлено"; -"gift_sent_desc" = "Ви відправили $1 за $2 голосів"; - -"error_on_server_side" = "Виникла помилка на боці сервера. Зверніться до системного адміністратора."; -"error_no_group_name" = "Ви не ввели назву групи."; - -"success_action" = "Операція пройшла успішно"; -"connection_error" = "Помилка з'єднання"; -"connection_error_desc" = "Не вдалося з'єднатися до служби телеметрії"; - -"error_when_uploading_photo" = "Не вдалося зберегти фотографію."; - -"new_changes_desc" = "Нові дані з'являться у вашій групі."; -"comment_is_changed" = "Коментар до адміністратора змінено"; -"comment_is_deleted" = "Коментар до адміністратора видалено"; -"comment_is_too_long" = "Коментар надто довгий ($1 символів замість 36 символів)"; -"x_no_more_admin" = "$1 більше не є адміністратором."; -"x_is_admin" = "$1 призначено адміністратором."; - -"x_is_now_hidden" = "Тепер $1 буде відображатися як звичайний підписник усім, окрім інших адміністраторів"; -"x_is_now_showed" = "Тепер $1 буде відображатися як звичайний адміністратор."; - -"note_is_deleted" = "Нотатка видалена"; -"note_x_is_now_deleted" = "Нотатка \"$1\" була успішно видалена."; -"new_data_accepted" = "Нові дані прийняті."; - -"album_is_deleted" = "Альбом видалено"; -"album_x_is_deleted" = "Альбом $1 було видалено."; - -"error_adding_to_deleted" = "Не вдалося зберегти фотографію у DELETED."; -"error_adding_to_x" = "Не вдалося зберегти фотографію в $1."; -"no_photo" = "Нема фотографій"; - -"select_file" = "Оберіть файл"; -"new_description_will_appear" = "Оновлений опис з'явиться на сторінці з фото."; -"photo_is_deleted" = "Фотографія видалена"; -"photo_is_deleted_desc" = "Ця світлина була успішно видалена."; - -"no_video" = "Немає відеозапису"; -"no_video_desc" = "Оберіть файл або вкажіть URL."; -"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" = "Спільноту успішно заблоковано"; /* Admin actions */ @@ -1189,18 +1000,14 @@ "manage_user_action" = "Керування користувачем"; "manage_group_action" = "Керування групою"; "ban_user_action" = "Заблокувати користувача"; -"blocks" = "Блокування"; -"last_actions" = "Останні дії"; "unban_user_action" = "Розблокувати користувача"; "warn_user_action" = "Попередити користувача"; "ban_in_support_user_action" = "Заблокувати у тех.підтримці"; "unban_in_support_user_action" = "Розблокувати у тех.підтримці"; -"changes_history" = "Історія редагування"; /* Admin panel */ "admin" = "Адмін панель"; -"sandbox_for_developers" = "Sandbox для розробників"; "admin_ownerid" = "ID власника"; "admin_author" = "Автор"; "admin_name" = "Ім'я"; @@ -1275,40 +1082,6 @@ "admin_banned_link_initiator" = "Ініціатор"; "admin_banned_link_not_specified" = "Посилання не зазначено"; "admin_banned_link_not_found" = "Посилання не знайдено"; -"admin_gift_moved_successfully" = "Подарунок успішно переміщено"; -"admin_gift_moved_to_recycle" = "Тепер подарунок у кошику."; -"logs" = "Логи"; -"logs_anything" = "Будь-яке"; -"logs_adding" = "Створення"; -"logs_editing" = "Редагування"; -"logs_removing" = "Видалення"; -"logs_restoring" = "Відновлення"; -"logs_added" = "додав"; -"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) */ @@ -1357,9 +1130,7 @@ "transfer" = "Передати"; "close" = "Закрити"; "warning" = "Увага"; -"question_confirm" = "Цю дію не можна скасувати. Ви переконані що хочете це зробити?"; -"confirm_m" = "Підтвердити"; -"action_successfully" = "Операція виконана успішно"; +"question_confirm" = "Цю дію не можна скасувати. Ви дійсно впевнені, що хочете зробити?"; /* User alerts */ @@ -1373,8 +1144,6 @@ /* Away */ -"transition_is_blocked" = "Перехід за посиланням заборонений"; -"caution" = "Попередження"; "url_is_banned" = "Перехід неможливий"; "url_is_banned_comment" = "Адміністрація $1 не рекомендує переходити за цим посиланням."; "url_is_banned_comment_r" = "Адміністрація $1 не рекомендує переходити за цим посиланням.

Підстава: $2"; @@ -1382,8 +1151,6 @@ "url_is_banned_title" = "Посилання на підозрілий сайт"; "url_is_banned_proceed" = "Перейти за посиланням"; -"recently" = "Нещодавно"; - /* Chandler */ "c_user_removed_from_group" = "Користувача було видалено з групи"; @@ -1655,43 +1422,6 @@ "closed_group_post" = "Цей допис з приватної групи"; "deleted_target_comment" = "Цей коментар належить до видаленого допису"; -"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" = "Друзі"; @@ -1708,40 +1438,3 @@ "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 1c3d4d84291c5010d3e9942bbc82f8529e71b5c5 Mon Sep 17 00:00:00 2001 From: Dmitry Tretyakov <76806170+tretdm@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:18:59 +0700 Subject: [PATCH 038/231] Revert "UPD code" This reverts commit fef0203aa4b2cd9abf54171bad424670b18638d9. --- Web/Presenters/PhotosPresenter.php | 5 +- locales/uk.strings | 311 ++++++++++++++++++++++++++++- 2 files changed, 313 insertions(+), 3 deletions(-) diff --git a/Web/Presenters/PhotosPresenter.php b/Web/Presenters/PhotosPresenter.php index 0a8b87e4..aeb8ba1e 100644 --- a/Web/Presenters/PhotosPresenter.php +++ b/Web/Presenters/PhotosPresenter.php @@ -336,7 +336,10 @@ final class PhotosPresenter extends OpenVKPresenter if(is_null($this->user) || $this->user->id != $ownerId) $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); - $redirect = $photo->getAlbum()->getOwner() instanceof User ? "/id0" : "/club" . $ownerId; + if(!is_null($album = $photo->getAlbum())) + $redirect = $album->getOwner() instanceof User ? "/id0" : "/club" . $ownerId; + else + $redirect = "/id0"; $photo->isolate(); $photo->delete(); diff --git a/locales/uk.strings b/locales/uk.strings index 7ccfb367..b2b8a914 100644 --- a/locales/uk.strings +++ b/locales/uk.strings @@ -134,6 +134,10 @@ "updated_at" = "Оновлено $1"; "user_banned" = "На жаль, нам довелося заблокувати сторінку користувача $1."; "user_banned_comment" = "Коментар модератора:"; +"verified_page" = "Верифікована сторінка"; +"user_is_blocked" = "Користувача заблоковано"; +"before" = "до"; +"forever" = "назавжди"; /* Wall */ @@ -184,6 +188,7 @@ "nsfw_warning" = "Даний запис може містити контент 18+"; "report" = "Поскаржитися"; "attach" = "Прикріпити"; +"detach" = "Відкріпити"; "attach_photo" = "Прикріпити фото"; "attach_photo" = "Прикріпити відео"; "draw_graffiti" = "Намалювати графіті"; @@ -193,6 +198,8 @@ "version_incompatibility" = "Не вдалося показати це вкладення. Можливо, база даних несумісна з поточною версією OpenVK."; "graffiti" = "Графіті"; "reply" = "Відповісти"; +"post_is_ad" = "Цей пост було проплачено Держдепом США"; +"edited_short" = "ред."; /* Friends */ @@ -317,14 +324,21 @@ "search_group" = "Пошук групи"; "search_by_groups" = "Пошук за групами"; "search_group_desc" = "Тут Ви можете переглянути існуючи групи та обрати групу до вподоби."; +"group_banned" = "Спільнота $1 заблокована." /* Albums */ "create" = "Створити"; +"album" = "Альбом"; "albums" = "Альбоми"; +"photos" = "фотографій"; +"photo" = "Фотографія"; "create_album" = "Створити альбом"; "edit_album" = "Редагувати альбом"; +"edit_photo" = "Змінити фотографію"; "creating_album" = "Створення альбому"; +"delete_photo" = "Видалити фотографію"; +"sure_deleting_photo" = "Ви впевнені, що бажаєте видалити цю світлину?"; "upload_photo" = "Завантажити фотографію"; "photo" = "Фотографія"; "upload_button" = "Завантажити"; @@ -366,6 +380,39 @@ "upd_f" = "оновила фотографію на своїй сторінці"; "upd_g" = "оновило фотографію групи"; +"add_photos" = "Додати фотографії"; +"upload_picts" = "Завантажити фотографії"; +"end_uploading" = "Завершити завантаження"; +"photos_successfully_uploaded" = "Фотографії було завантажено"; +"click_to_go_to_album" = "Натисніть, щоб перейти до альбому."; +"error_uploading_photo" = "Помилка завантаження фотографії"; +"too_many_pictures" = "Не більше 10 фотографій"; + +"drag_files_here" = "Перетягніть файли сюди"; +"only_images_accepted" = "Файл \"$1\" не є зображенням"; +"max_filesize" = "Максимальний розмір файлу — $1 мегабайт"; + +"uploading_photos_from_computer" = "Завантаження фотографій з Вашого ПК"; +"supported_formats" = "Підтримувані формати зображень: JPG, PNG й GIF."; +"max_load_photos" = "Ви можете завантажити до 10 фотографій за один раз."; +"tip" = "Порада"; +"tip_ctrl" = "щоб обрати кілька фотографій одразу, утримуйте клавішу Ctrl під час вибору файлів в OS Windows або клавішу CMD у Mac OS."; +"album_poster" = "Обкладинка альбому"; +"select_photo" = "Оберіть фотографію"; +"upload_new_photo" = "Завантажте нову світлину"; + +"is_x_photos_zero" = "Усього 0 фотографій."; +"is_x_photos_one" = "Всього 1 фотографія."; +"is_x_photos_few" = "Всього $1 фотографій."; +"is_x_photos_many" = "Всього $1 фотографій."; +"is_x_photos_other" = "Всього $1 фотографій."; + +"all_photos" = "Всі фотографії"; +"error_uploading_photo" = "Помилка завантаження фотографії. Текст помилки: "; +"too_many_photos" = "Надто багато фотографій."; + +"photo_x_from_y" = "Фотографія $1 з $2"; + /* Notes */ "notes" = "Нотатки"; @@ -395,6 +442,18 @@ /* Notes: Article Viewer */ "aw_legacy_ui" = "Класичне дієвидло"; +"select_note" = "Вибір нотатки"; +"no_notes" = "Ви не маєте жодної нотатки"; + +"error_attaching_note" = "Не вдалося закріпити нотатку"; + +"select_or_create_new" = "Оберіть існуючу нотатку або створіть нову"; + +"notes_closed" = "Ви не можете прикріпити нотатку до запису, оскільки ваші нотатки видно тільки вам.

Ви можете змінити це в налаштуваннях."; +"do_not_attach_note" = "Не прикріплювати нотатку"; +"something" = "Щось"; +"supports_xhtml" = "з (X)HTML підтримується."; + /* Menus */ "edit_button" = "ред."; @@ -489,7 +548,7 @@ "privacy_value_friends" = "Друзі"; "privacy_value_friends_dative" = "Друзям"; "privacy_value_only_me" = "Тільки я"; -"privacy_value_only_me_dative" = "Тільки мені"; +"privacy_value_only_me_dative" = "Тільки мені та Кирилу Буданову"; "privacy_value_nobody" = "Ніхто"; "your_email_address" = "Адрес Вашої електронної пошти"; "your_page_address" = "Адрес Вашої сторінки"; @@ -581,6 +640,9 @@ "two_factor_authentication_backup_codes_1" = "Резервні коди дозволяють підтверджувати вхід, коли у вас немає доступу до телефону, наприклад, у подорожі."; "two_factor_authentication_backup_codes_2" = "У вас є ще 10 кодів, кожним кодом можна скористатися тільки один раз. Надрукуйте їх, приберіть в надійне місце і використовуйте, коли будуть потрібні коди для підтвердження входу."; "two_factor_authentication_backup_codes_3" = "Ви можете отримати нові коди, якщо вони закінчуються. Дійсні тільки останні створені резервні коди."; +"viewing_backup_codes" = "Перегляд резервних кодів"; +"disable_2fa" = "Вимкнути 2FA"; +"viewing" = "Переглянути"; /* Sorting */ @@ -606,6 +668,14 @@ "videos_many" = "$1 відеозаписів"; "videos_other" = "$1 відеозаписів"; "view_video" = "Перегляд"; +"change_video" = "Редагувати відеозапис"; +"unknown_video" = "ЦЕЙ ВІДЕОЗАПИС НЕ ПІДТРИМУЄТЬСЯ В ЦІЙ ВЕРСІЇ OPENVK."; +"selecting_video" = "Вибір відеозаписів"; +"upload_new_video" = "Завантажити нове відео"; +"max_attached_videos" = "Максимум 10 відеозаписів"; +"max_attached_photos" = "Максимум 10 фотографій"; +"no_videos" = "Ви не маєте відео."; +"no_videos_results" = "Немає результатів."; /* Notifications */ @@ -642,6 +712,7 @@ "nt_mention_in_video" = "в обговоренні відеозапису"; "nt_mention_in_note" = "в обговоренні під"; "nt_mention_in_topic" = "в обговоренні"; +"nt_sent_gift" = "відправив вам подарунок"; /* Time */ @@ -815,6 +886,7 @@ "support_new" = "Нове звернення"; "support_new_title" = "Введіть тему вашого звернення"; "support_new_content" = "Опишіть проблему чи пропозицію"; +"reports" = "Скарги"; "support_rate_good_answer" = "Це хороша відповідь"; "support_rate_bad_answer" = "Це погана відповідь"; "support_good_answer_user" = "Ви залишили позитивний відгук."; @@ -825,6 +897,26 @@ "support_rated_bad" = "Ви залишили негативний відгук про відповідь."; "wrong_parameters" = "Неправильні параметри запиту."; "fast_answers" = "Швидкі відповіді"; +"ignore_report" = "Ігнорувати скаргу"; +"report_number" = "Скарга №"; +"list_of_reports" = "Лист скарг"; +"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" = "Автор"; @@ -834,6 +926,12 @@ "ticket_changed_comment" = "Зміни набудуть чинності через кілька секунд."; "banned_in_support_1" = "Вибачте, $1, але тепер вам не можна створювати звернення."; "banned_in_support_2" = "Підстава: $1. Цього разу нам довелося забрати у вас цю можливість назавжди."; +"you_can_close_this_ticket_1" = "Якщо ви не маєте запитань, Ви можете "; +"you_can_close_this_ticket_2" = "закрити це звернення"; +"agent_profile_created_1" = "Профіль створено"; +"agent_profile_created_2" = "Тепер користувачі бачать Ваш псевдонім і аватар замість стандартного поличчя та ID."; +"agent_profile_edited" = "Профіль відредагований"; +"agent_profile" = "Картка агента"; /* Invite */ @@ -958,6 +1056,7 @@ "error_repost_fail" = "Не вдалося поділитися записом"; "error_data_too_big" = "Атрибут '$1' не може бути довше $2 $3"; "forbidden" = "Помилка доступу"; +"unknown_error" = "Невідома помилка"; "forbidden_comment" = "Налаштування приватності цього користувача не дозволяють дивитися на його сторінку."; "changes_saved" = "Зміни збережені"; "changes_saved_comment" = "Нові дані з'являться на вашій сторінці"; @@ -993,6 +1092,96 @@ "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" = "АХАХАХА ЛОШАРА ПІЗДУЙ НА ЗАРОБІТКИ У ПОЛЬЩУ"; +/* трррр шкібіді доп доп доп доп єс єс єс) */ + +"description_too_long" = "Опис надто довгий."; + +"gift_sent" = "Подарунок відправлено"; +"gift_sent_desc" = "Ви відправили $1 за $2 голосів"; + +"error_on_server_side" = "Виникла помилка на боці сервера. Зверніться до системного адміністратора."; +"error_no_group_name" = "Ви не ввели назву групи."; + +"success_action" = "Операція пройшла успішно"; +"connection_error" = "Помилка з'єднання"; +"connection_error_desc" = "Не вдалося з'єднатися до служби телеметрії"; + +"error_when_uploading_photo" = "Не вдалося зберегти фотографію."; + +"new_changes_desc" = "Нові дані з'являться у вашій групі."; +"comment_is_changed" = "Коментар до адміністратора змінено"; +"comment_is_deleted" = "Коментар до адміністратора видалено"; +"comment_is_too_long" = "Коментар надто довгий ($1 символів замість 36 символів)"; +"x_no_more_admin" = "$1 більше не є адміністратором."; +"x_is_admin" = "$1 призначено адміністратором."; + +"x_is_now_hidden" = "Тепер $1 буде відображатися як звичайний підписник усім, окрім інших адміністраторів"; +"x_is_now_showed" = "Тепер $1 буде відображатися як звичайний адміністратор."; + +"note_is_deleted" = "Нотатка видалена"; +"note_x_is_now_deleted" = "Нотатка \"$1\" була успішно видалена."; +"new_data_accepted" = "Нові дані прийняті."; + +"album_is_deleted" = "Альбом видалено"; +"album_x_is_deleted" = "Альбом $1 було видалено."; + +"error_adding_to_deleted" = "Не вдалося зберегти фотографію у DELETED."; +"error_adding_to_x" = "Не вдалося зберегти фотографію в $1."; +"no_photo" = "Нема фотографій"; + +"select_file" = "Оберіть файл"; +"new_description_will_appear" = "Оновлений опис з'явиться на сторінці з фото."; +"photo_is_deleted" = "Фотографія видалена"; +"photo_is_deleted_desc" = "Ця світлина була успішно видалена."; + +"no_video" = "Немає відеозапису"; +"no_video_desc" = "Оберіть файл або вкажіть URL."; +"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" = "Спільноту успішно заблоковано"; /* Admin actions */ @@ -1000,14 +1189,18 @@ "manage_user_action" = "Керування користувачем"; "manage_group_action" = "Керування групою"; "ban_user_action" = "Заблокувати користувача"; +"blocks" = "Блокування"; +"last_actions" = "Останні дії"; "unban_user_action" = "Розблокувати користувача"; "warn_user_action" = "Попередити користувача"; "ban_in_support_user_action" = "Заблокувати у тех.підтримці"; "unban_in_support_user_action" = "Розблокувати у тех.підтримці"; +"changes_history" = "Історія редагування"; /* Admin panel */ "admin" = "Адмін панель"; +"sandbox_for_developers" = "Sandbox для розробників"; "admin_ownerid" = "ID власника"; "admin_author" = "Автор"; "admin_name" = "Ім'я"; @@ -1082,6 +1275,40 @@ "admin_banned_link_initiator" = "Ініціатор"; "admin_banned_link_not_specified" = "Посилання не зазначено"; "admin_banned_link_not_found" = "Посилання не знайдено"; +"admin_gift_moved_successfully" = "Подарунок успішно переміщено"; +"admin_gift_moved_to_recycle" = "Тепер подарунок у кошику."; +"logs" = "Логи"; +"logs_anything" = "Будь-яке"; +"logs_adding" = "Створення"; +"logs_editing" = "Редагування"; +"logs_removing" = "Видалення"; +"logs_restoring" = "Відновлення"; +"logs_added" = "додав"; +"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) */ @@ -1130,7 +1357,9 @@ "transfer" = "Передати"; "close" = "Закрити"; "warning" = "Увага"; -"question_confirm" = "Цю дію не можна скасувати. Ви дійсно впевнені, що хочете зробити?"; +"question_confirm" = "Цю дію не можна скасувати. Ви переконані що хочете це зробити?"; +"confirm_m" = "Підтвердити"; +"action_successfully" = "Операція виконана успішно"; /* User alerts */ @@ -1144,6 +1373,8 @@ /* Away */ +"transition_is_blocked" = "Перехід за посиланням заборонений"; +"caution" = "Попередження"; "url_is_banned" = "Перехід неможливий"; "url_is_banned_comment" = "Адміністрація $1 не рекомендує переходити за цим посиланням."; "url_is_banned_comment_r" = "Адміністрація $1 не рекомендує переходити за цим посиланням.

Підстава: $2"; @@ -1151,6 +1382,8 @@ "url_is_banned_title" = "Посилання на підозрілий сайт"; "url_is_banned_proceed" = "Перейти за посиланням"; +"recently" = "Нещодавно"; + /* Chandler */ "c_user_removed_from_group" = "Користувача було видалено з групи"; @@ -1422,6 +1655,43 @@ "closed_group_post" = "Цей допис з приватної групи"; "deleted_target_comment" = "Цей коментар належить до видаленого допису"; +"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" = "Друзі"; @@ -1438,3 +1708,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 e3b9fb9f41f4c2bc236dd040914d611b18798fd5 Mon Sep 17 00:00:00 2001 From: IsamiRi <53663257+isamirivers@users.noreply.github.com> Date: Mon, 23 Oct 2023 22:28:41 +0300 Subject: [PATCH 039/231] Fix for #995 (#996) --- Web/static/css/main.css | 1 + 1 file changed, 1 insertion(+) diff --git a/Web/static/css/main.css b/Web/static/css/main.css index 47d67317..7b63d6f6 100644 --- a/Web/static/css/main.css +++ b/Web/static/css/main.css @@ -405,6 +405,7 @@ h1 { width: 200px; text-align: left; cursor: pointer; + font-family: tahoma, verdana, arial, sans-serif; } .profile_link_form { From 49a7047773f30c5dea1da1076e69746edb497ece Mon Sep 17 00:00:00 2001 From: Dmitry Tretyakov <76806170+tretdm@users.noreply.github.com> Date: Wed, 25 Oct 2023 16:42:26 +0700 Subject: [PATCH 040/231] Patch array objects for PHP 8.1 (#999) * Patch array objects for PHP 8 * Reswitch getting counts * Update WallPresenter.php * Update WallPresenter.php * Fix --- Web/Presenters/WallPresenter.php | 10 +++++----- Web/Presenters/templates/Admin/Logs.xml | 2 +- Web/Presenters/templates/Search/Index.xml | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Web/Presenters/WallPresenter.php b/Web/Presenters/WallPresenter.php index 2f9d611d..f7928db8 100644 --- a/Web/Presenters/WallPresenter.php +++ b/Web/Presenters/WallPresenter.php @@ -74,7 +74,7 @@ final class WallPresenter extends OpenVKPresenter $this->template->paginatorConf = (object) [ "count" => $this->template->count, "page" => (int) ($_GET["p"] ?? 1), - "amount" => sizeof($this->template->posts), + "amount" => $this->template->posts->getRowCount(), "perPage" => OPENVK_DEFAULT_PER_PAGE, ]; @@ -152,9 +152,9 @@ final class WallPresenter extends OpenVKPresenter ->where("deleted", 0) ->order("created DESC"); $this->template->paginatorConf = (object) [ - "count" => sizeof($posts), + "count" => $posts->getRowCount(), "page" => (int) ($_GET["p"] ?? 1), - "amount" => sizeof($posts->page((int) ($_GET["p"] ?? 1), $perPage)), + "amount" => $posts->page((int) ($_GET["p"] ?? 1), $perPage)->getRowCount(), "perPage" => $perPage, ]; $this->template->posts = []; @@ -182,7 +182,7 @@ final class WallPresenter extends OpenVKPresenter $this->template->paginatorConf = (object) [ "count" => $count, "page" => (int) ($_GET["p"] ?? 1), - "amount" => sizeof($posts), + "amount" => $posts->getRowCount(), "perPage" => $pPage, ]; foreach($posts as $post) @@ -332,7 +332,7 @@ final class WallPresenter extends OpenVKPresenter foreach($photos as $photo) $post->attach($photo); - if(sizeof($videos) > 0) + if($videos->count() > 0) foreach($videos as $vid) $post->attach($vid); diff --git a/Web/Presenters/templates/Admin/Logs.xml b/Web/Presenters/templates/Admin/Logs.xml index ab5e62f5..8b279008 100644 --- a/Web/Presenters/templates/Admin/Logs.xml +++ b/Web/Presenters/templates/Admin/Logs.xml @@ -9,7 +9,7 @@ {/block} {block content} - {var $amount = sizeof($logs)} + {var $amount = $logs->getRowCount()}
+ {var $data = is_array($iterator) ? $iterator : iterator_to_array($iterator)} + {include "../components/paginator.xml", conf => (object) [ "page" => $page, "count" => $count, @@ -153,7 +155,6 @@
{include searchOptions} - {var $data = is_array($iterator) ? $iterator : iterator_to_array($iterator)}
{if sizeof($data) > 0} {if $type == "users" || $type == "groups" || $type == "apps"} From 390b4f6c24799330eabd2957eafb89491e614204 Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Sat, 28 Oct 2023 14:25:54 +0300 Subject: [PATCH 055/231] Fix followers list in group --- Web/Presenters/templates/Group/Followers.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Web/Presenters/templates/Group/Followers.xml b/Web/Presenters/templates/Group/Followers.xml index 482d156a..67e6af9c 100644 --- a/Web/Presenters/templates/Group/Followers.xml +++ b/Web/Presenters/templates/Group/Followers.xml @@ -64,7 +64,7 @@ {_role}: - {$club->getOwner()->getId() == $user->getId() ? !$club->isOwnerHidden() || $club->canBeModifiedBy($thisUser) : !is_null($manager) ? tr("administrator") : tr("follower")} + {($club->getOwner()->getId() == $user->getId() ? !$club->isOwnerHidden() || $club->canBeModifiedBy($thisUser) : !is_null($manager)) ? tr("administrator") : tr("follower")} From e2dee72c69940a7f69055f065600d46ccee22a85 Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Sat, 28 Oct 2023 14:28:19 +0300 Subject: [PATCH 056/231] Fix notes list --- Web/Presenters/templates/Notes/List.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Web/Presenters/templates/Notes/List.xml b/Web/Presenters/templates/Notes/List.xml index 36c48fd3..7f2fcf7c 100644 --- a/Web/Presenters/templates/Notes/List.xml +++ b/Web/Presenters/templates/Notes/List.xml @@ -90,7 +90,7 @@
- {if sizeof($dat->getCommentsCount()) > 0} + {if $dat->getCommentsCount() > 0} {_comments} ({$dat->getCommentsCount()}) {else} {_no_comments} From e48b696aeb4364246f8e9e167097c7801ed4955a Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Sat, 28 Oct 2023 14:33:52 +0300 Subject: [PATCH 057/231] Maybe fix tickets list --- Web/Presenters/SupportPresenter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Web/Presenters/SupportPresenter.php b/Web/Presenters/SupportPresenter.php index 8163dbf8..b834d712 100644 --- a/Web/Presenters/SupportPresenter.php +++ b/Web/Presenters/SupportPresenter.php @@ -67,7 +67,7 @@ final class SupportPresenter extends OpenVKPresenter $this->template->count = $this->tickets->getTicketsCountByUserId($this->user->id); if($this->template->mode === "list") { $this->template->page = (int) ($this->queryParam("p") ?? 1); - $this->template->tickets = $this->tickets->getTicketsByUserId($this->user->id, $this->template->page); + $this->template->tickets = iterator_to_array($this->tickets->getTicketsByUserId($this->user->id, $this->template->page)); } if($this->template->mode === "new") From f57a470c5d6c3a5b70595a03ec6fb0dca0e81f57 Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Sat, 28 Oct 2023 14:37:28 +0300 Subject: [PATCH 058/231] Fix gifts template (stolen from #512) --- Web/Presenters/OpenVKPresenter.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Web/Presenters/OpenVKPresenter.php b/Web/Presenters/OpenVKPresenter.php index 80ab0621..a171c2e3 100644 --- a/Web/Presenters/OpenVKPresenter.php +++ b/Web/Presenters/OpenVKPresenter.php @@ -198,6 +198,9 @@ abstract class OpenVKPresenter extends SimplePresenter { $user = Authenticator::i()->getUser(); + if(!$this->template) + $this->template = new \stdClass; + $this->template->isXmas = intval(date('d')) >= 1 && date('m') == 12 || intval(date('d')) <= 15 && date('m') == 1 ? true : false; $this->template->isTimezoned = Session::i()->get("_timezoneOffset"); From 3f1c9cd5ff3543fed935a40a6707eaa141592c7b Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Sat, 28 Oct 2023 15:47:46 +0300 Subject: [PATCH 059/231] Fix token creation in php 8.1 --- Web/Models/Entities/APIToken.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Web/Models/Entities/APIToken.php b/Web/Models/Entities/APIToken.php index f5744ec3..0cc53148 100644 --- a/Web/Models/Entities/APIToken.php +++ b/Web/Models/Entities/APIToken.php @@ -48,7 +48,7 @@ class APIToken extends RowModel $this->delete(); } - function save(): void + function save(?bool $log = false): void { if(is_null($this->getRecord())) $this->stateChanges("secret", bin2hex(openssl_random_pseudo_bytes(36))); From f492353ae8a8a7074709c108996a748c3249af65 Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Sun, 29 Oct 2023 13:05:47 +0300 Subject: [PATCH 060/231] Some php 8.2 fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix privacy settings, friends pagination and возможно fix messages. Resolves #1007 and resolves #1006 --- Web/Models/Entities/Correspondence.php | 2 +- Web/Presenters/MessengerPresenter.php | 2 +- Web/Presenters/UserPresenter.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Web/Models/Entities/Correspondence.php b/Web/Models/Entities/Correspondence.php index e4b7d96e..a972425e 100644 --- a/Web/Models/Entities/Correspondence.php +++ b/Web/Models/Entities/Correspondence.php @@ -131,7 +131,7 @@ class Correspondence */ function getPreviewMessage(): ?Message { - $messages = $this->getMessages(1, NULL, 1); + $messages = $this->getMessages(1, NULL, 1, 0); return $messages[0] ?? NULL; } diff --git a/Web/Presenters/MessengerPresenter.php b/Web/Presenters/MessengerPresenter.php index d5ffb988..e04e1adc 100644 --- a/Web/Presenters/MessengerPresenter.php +++ b/Web/Presenters/MessengerPresenter.php @@ -128,7 +128,7 @@ final class MessengerPresenter extends OpenVKPresenter $messages = []; $correspondence = new Correspondence($this->user->identity, $correspondent); - foreach($correspondence->getMessages(1, $lastMsg === 0 ? NULL : $lastMsg) as $message) + foreach($correspondence->getMessages(1, $lastMsg === 0 ? NULL : $lastMsg, NULL, 0) as $message) $messages[] = $message->simplify(); header("Content-Type: application/json"); diff --git a/Web/Presenters/UserPresenter.php b/Web/Presenters/UserPresenter.php index 51ddc6aa..e645f888 100644 --- a/Web/Presenters/UserPresenter.php +++ b/Web/Presenters/UserPresenter.php @@ -55,7 +55,7 @@ final class UserPresenter extends OpenVKPresenter $this->assertUserLoggedIn(); $user = $this->users->get($id); - $page = abs($this->queryParam("p") ?? 1); + $page = abs((int)($this->queryParam("p") ?? 1)); if(!$user) $this->notFound(); elseif (!$user->getPrivacyPermission('friends.read', $this->user->identity ?? NULL)) @@ -433,7 +433,7 @@ final class UserPresenter extends OpenVKPresenter ]; foreach($settings as $setting) { $input = $this->postParam(str_replace(".", "_", $setting)); - $user->setPrivacySetting($setting, min(3, abs($input ?? $user->getPrivacySetting($setting)))); + $user->setPrivacySetting($setting, min(3, (int)abs((int)$input ?? $user->getPrivacySetting($setting)))); } } else if($_GET['act'] === "finance.top-up") { $token = $this->postParam("key0") . $this->postParam("key1") . $this->postParam("key2") . $this->postParam("key3"); From abc307bfadab93845b1b845724b3d519f52c34f8 Mon Sep 17 00:00:00 2001 From: Dmitry Tretyakov <76806170+tretdm@users.noreply.github.com> Date: Sun, 29 Oct 2023 21:41:36 +0700 Subject: [PATCH 061/231] Update PHP 8 testing status --- README_RU.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_RU.md b/README_RU.md index cc4f672f..6da17408 100644 --- a/README_RU.md +++ b/README_RU.md @@ -30,7 +30,7 @@ _[English](README.md)_ 1. Установите PHP 7.4, веб-сервер, Composer, Node.js, Yarn и [Chandler](https://github.com/openvk/chandler) -* PHP 8 еще **не** тестировался, поэтому не стоит ожидать, что он будет работать (UPD: он не работает). +* PHP 8 is still being tested; performance on this version of PHP is not yet guaranteed. 2. Установите MySQL-совместимую базу данных. From 46f7e7ceebc11b009f11bed3a4b2df2cb097064e Mon Sep 17 00:00:00 2001 From: Dmitry Tretyakov <76806170+tretdm@users.noreply.github.com> Date: Sun, 29 Oct 2023 21:42:55 +0700 Subject: [PATCH 062/231] Update PHP 8 testing status --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 152e05bf..934ff0b7 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ If you want, you can add your instance to the list above so that people can regi 1. Install PHP 7.4, web-server, Composer, Node.js, Yarn and [Chandler](https://github.com/openvk/chandler) -* PHP 8.1 is supported too, however it was not tested carefully, so be aware. +* PHP 8 is still being tested; the functionality of the engine on this version of PHP is not yet guaranteed. 2. Install MySQL-compatible database. From 1a17a245c5dab15bf04e0b7bd4950f33fd580ed5 Mon Sep 17 00:00:00 2001 From: Dmitry Tretyakov <76806170+tretdm@users.noreply.github.com> Date: Sun, 29 Oct 2023 21:55:02 +0700 Subject: [PATCH 063/231] i forgor momento --- README_RU.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_RU.md b/README_RU.md index 6da17408..7de91c39 100644 --- a/README_RU.md +++ b/README_RU.md @@ -30,7 +30,7 @@ _[English](README.md)_ 1. Установите PHP 7.4, веб-сервер, Composer, Node.js, Yarn и [Chandler](https://github.com/openvk/chandler) -* PHP 8 is still being tested; performance on this version of PHP is not yet guaranteed. +* PHP 8 пока ещё тестируется, работоспособность движка на этой версии PHP пока не гарантируется. 2. Установите MySQL-совместимую базу данных. From 9a4309ae01588e9bc2e9926bd538016f32e4bf76 Mon Sep 17 00:00:00 2001 From: Vladimir Barinov Date: Tue, 31 Oct 2023 00:13:36 +0300 Subject: [PATCH 064/231] =?UTF-8?q?=D1=89=D0=B0=D1=81=20=D0=B4=D0=BE=D0=BB?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=20=D0=BD=D0=BE=D1=83=D1=81=D0=BF=D0=B0=D0=BC?= =?UTF-8?q?=20=D0=B7=D0=B0=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D1=82=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Web/Models/Entities/PasswordReset.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Web/Models/Entities/PasswordReset.php b/Web/Models/Entities/PasswordReset.php index cf0e73ea..372c63f8 100644 --- a/Web/Models/Entities/PasswordReset.php +++ b/Web/Models/Entities/PasswordReset.php @@ -54,11 +54,11 @@ class PasswordReset extends RowModel } } - function save(): void + function save(?bool $log = false): void { $this->stateChanges("key", base64_encode(openssl_random_pseudo_bytes(46))); $this->stateChanges("timestamp", time()); - parent::save(); + parent::save($log); } } From fd11dfcdd919854f2d5c4834413a0a1a81b01bbe Mon Sep 17 00:00:00 2001 From: Vladimir Barinov Date: Tue, 31 Oct 2023 00:14:18 +0300 Subject: [PATCH 065/231] =?UTF-8?q?=D0=B2=D1=8B=20=D0=B4=D0=BE=D0=BB=D0=B1?= =?UTF-8?q?=D0=BE=D1=91=D0=B1=D1=8B=20=D0=B1=D0=BB=D1=8F=D1=82=D1=8C=20?= =?UTF-8?q?=D0=B2=D1=8B=20=D0=BD=D0=B0=D1=85=D1=83=D1=8F=20=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D0=B2=D0=B8=D0=BB=D0=B8=20=D0=B4=D0=B5=D0=B1=D0=B0?= =?UTF-8?q?=D0=B3=20=D1=84=D0=B8=D1=87=D1=83=20=D0=B2=20=D1=80=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Web/Models/Repositories/Messages.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Web/Models/Repositories/Messages.php b/Web/Models/Repositories/Messages.php index 4c870806..538338ed 100644 --- a/Web/Models/Repositories/Messages.php +++ b/Web/Models/Repositories/Messages.php @@ -52,7 +52,6 @@ class Messages $query = file_get_contents(__DIR__ . "/../sql/get-correspondencies-count.tsql"); DatabaseConnection::i()->getConnection()->query(file_get_contents(__DIR__ . "/../sql/mysql-msg-fix.tsql")); $count = DatabaseConnection::i()->getConnection()->query($query, $id, $class, $id, $class)->fetch()->cnt; - bdump($count); return $count; } } From a2473c68fe8522a8fc39a95ab01c1e66eb8e6c2e Mon Sep 17 00:00:00 2001 From: veselcraft Date: Wed, 1 Nov 2023 14:52:24 +0300 Subject: [PATCH 066/231] VKAPI: PHP 8.2 fixes --- Web/Presenters/VKAPIPresenter.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Web/Presenters/VKAPIPresenter.php b/Web/Presenters/VKAPIPresenter.php index 431379c5..963c9ccc 100644 --- a/Web/Presenters/VKAPIPresenter.php +++ b/Web/Presenters/VKAPIPresenter.php @@ -233,8 +233,13 @@ final class VKAPIPresenter extends OpenVKPresenter $this->badMethodCall($object, $method, $parameter->getName()); } - settype($val, $parameter->getType()->getName()); - $params[] = $val; + try { + settype($val, $parameter->getType()->getName()); + $params[] = $val; + } catch (\Throwable $e) { + // Just ignore the exception, since + // some args are intended for internal use + } } define("VKAPI_DECL_VER", $this->requestParam("v") ?? "4.100", false); From 49d62543ba2374a61948176fc36392627e9881df Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Sun, 5 Nov 2023 15:19:06 +0300 Subject: [PATCH 067/231] Fix 500 in photos.get when album does not exists --- VKAPI/Handlers/Photos.php | 13 +++++-------- Web/Models/Repositories/Albums.php | 4 ++-- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/VKAPI/Handlers/Photos.php b/VKAPI/Handlers/Photos.php index e3e9abac..bb1a22f0 100644 --- a/VKAPI/Handlers/Photos.php +++ b/VKAPI/Handlers/Photos.php @@ -432,13 +432,11 @@ final class Photos extends VKAPIRequestHandler if(empty($photo_ids)) { $album = (new Albums)->getAlbumByOwnerAndId($owner_id, $album_id); - if(!$album->getOwner()->getPrivacyPermission('photos.read', $this->getUser())) { - $this->fail(21, "This user chose to hide his albums."); - } - - if(!$album || $album->isDeleted()) { + if(!$album || $album->isDeleted()) $this->fail(21, "Invalid album"); - } + + if(!$album->getOwner()->getPrivacyPermission('photos.read', $this->getUser())) + $this->fail(21, "This user chose to hide his albums."); $photos = array_slice(iterator_to_array($album->getPhotos(1, $count + $offset)), $offset); $res["count"] = sizeof($photos); @@ -456,8 +454,7 @@ final class Photos extends VKAPIRequestHandler "items" => [] ]; - foreach($photos as $photo) - { + foreach($photos as $photo) { $id = explode("_", $photo); $phot = (new PhotosRepo)->getByOwnerAndVID((int)$id[0], (int)$id[1]); diff --git a/Web/Models/Repositories/Albums.php b/Web/Models/Repositories/Albums.php index b38ee462..f99848c4 100644 --- a/Web/Models/Repositories/Albums.php +++ b/Web/Models/Repositories/Albums.php @@ -130,7 +130,7 @@ class Albums "owner" => $owner, "id" => $id ])->fetch(); - - return new Album($album); + + return $album ? new Album($album) : NULL; } } From c9665ac77d7a7601e18e602e0346af0d4405bf31 Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Tue, 7 Nov 2023 20:32:04 +0300 Subject: [PATCH 068/231] Resolves #1017 --- Web/Models/Entities/User.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Web/Models/Entities/User.php b/Web/Models/Entities/User.php index aaf00ec9..dc227039 100644 --- a/Web/Models/Entities/User.php +++ b/Web/Models/Entities/User.php @@ -720,8 +720,8 @@ class User extends RowModel for($i = 0; $i < 10 - $this->get2faBackupCodeCount(); $i++) { $codes[] = [ - owner => $this->getId(), - code => random_int(10000000, 99999999) + "owner" => $this->getId(), + "code" => random_int(10000000, 99999999) ]; } From 9d7a465d0d993319bf21b4af941d07b1228f9ef4 Mon Sep 17 00:00:00 2001 From: celestora Date: Sat, 11 Nov 2023 23:41:07 +0200 Subject: [PATCH 069/231] Music, finally! (#512) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add audio upload feature * Add audio embed thing * Move bullet.gif to ovk * Draft some music API methods * Add support for base64 ids to Audios.getById * Disallow having more than 65k audios in playlist * Add playlist model * Draft some playlist-related API methods * Fix behabiour of album-related methods Generators f***** me in le a** * Add IDv3 autofill * Add sql dumps i forgor to upload it xdddd * Add playlists sql * Fix audio upload not working on Windows 11 because Windows is the worst operating system which doesn't work properly under any circumstances * Fix cocksex in audio.get yes * Интерфейсы * Interface updade * Update en.strings * Add audio queue * Make repeat button work * Some improvements to audio queue * Фгвшщ йгугу шьзкщмуьутеы * Make shuffle and "наушники" buttons work, add f... avicons when playing audio, save some values (like volume and last played track) to localstorage, add ability to toggle time type in player, fix uploading audios with cover (maybe) and add dragndrop to upload page * Add funny tip with time when hover track div * Add something * Add audios picker & move track in smal player вниз * Summary (required) Description * [WIP] Add calls, stories and clips. Изменены фавиконки (поменьше стали) У миниплеера ползунок теперь в стиле bsdn и большого плеера, добавлен ползунок громкости Добавлена кнопка добавления аудио в группу (у миниплеера) Если вы смотрите аудио группы, которой можете управлять, появляется кнопка "удалить аудио из группы" Снизу плейлиста в списке теперь показывается автор. При прикреплении аудиозаписей к посту теперь есть поиск "по композиции" и "по исполнителю" Добавил explicit.svg, который я забыл добавить в предыдущем коммите. Вкладочки немного переделаны При наведении на кнопки "трек вперёд" или "трек назад" показывается название предыдущего или следующего трека соответственно * 1 new commit to master: [WIP]: Add audios - Теперь группа может разрешать загружать всем треки в неё - Теперь треки загружаются на сервер ajax'ом, и так можно очень много аудио загружать - Вёрстка списка плейлистов изменена, теперь она на гридах - Немного изменено апи, теперь метод editAlbum сохраняет новую информацию ee объект плейлистов теперь возвращают реальное время - Удалены лишние пути из routes.yml - При переключении страниц теперь если на текущей странице есть играющий трек, он нормально подсвечивается - Из init-db.sql удалены таблицы аудиозаписей - В Groups.getSettings и groups.edit теперь есть информация о аудиозаписях * (смешное название коммита) - Теперь на странице пользователя/группы показываются три случайные песни, а не первые три как раньше - Теперь пробел на странице аудио не перемещает вас в низ страницы - Оптимизирован мини-плеер, теперь он инициализируется при любом нажатии на него, а не при наведении - Теперь при завершении проигрывания трека в мини-плеере он ищет другой трек рядом, и если находит то воспроизводит. Будет удобно для постов с подборками треков - Поиск теперь показывает 14 результатов - Теперь при возникновении ошибки загрузки аудио она нормально отображается - Вместе с плеером на странице с аудиозаписями теперь двигаются и вкладки - Добавление аудио в группу по идее должно нормально работать * Implement playlists listens - У плейлистов теперь есть прослушивания в общем. - Прослушивания у большого плеера теперь засчитываются, если трек был дослушан до конца - В объекте плейлистов теперь возвращается listens и cover_url - Получение плееров через /audios/context переписано, повторяющийся код удалён, правда сильно количество строк сократить не получилось - Теперь цвета плеера темнее, а иконка проигрывания изменена - Теперь, если очередь из треков кончилась, то плеер перенаправляет вас в начало очереди. * php 8.2 fixxxxxxxxxxxxxxxxxxxxxxx * Implement audiostatuses Добавлены аудиостатусы (у пользователей), блок с друзьями, слушающих музыку на странице аудиозаписей, объект status_audio в users.get, улучшены настройки приватности и ещё что-то * ? - Переделан метод в классе user для получения друзей с проигрываемыми песнями. Теперь среди них могут появляться и группы (хз стоит ли оставлять это или нет). Так же больше не показываются удалённые пользователи - Трек у плеера теперь двигается немного плавнее. Ещё теперь нету смешных багов с подсказкой времени, когда можно было увести её за экран или промотать дальше трека. Переключить повторение трека теперь можно нажатием кнопки R. - Длинное название трека больше не сносит время - Наверное, теперь аудиозаписи нормально отображаются в темах midnight и modern - Аудиозаписи больше не крашаются, если пользователь неавторизован. - Немного переделан миниплеер. - В миниплеере теперь громкость берётся из локалсторейджа. - Улучшено редактирование аудиозаписей. Теперь данные в дата атрибуты нормально сохраняются, а так же слова песни и метка "explicit" меняются - Удалён css, оставшийся ещё от public technical preview 1, а так же путь /audios{num} - При наведении на трек теперь пропадает время, и на его месте появляются кнопки - Стандартная аватарка в midnight теперь инвертируется - В админке в редактировании аудио теперь показывается дата редактирования, дата создания, длина и оригинальный файл аудио. Так же на странице редактирования больше нет вылетов, если вы задали несуществующий аккаунт * ! - Добавлены строки для мобильной темы - Добавлено предупреждение перед полным удалением плейлиста - Нажатие кнопки M = нажатие кнопки наушников - В классе апи Audio поставлены willExecuteWriteAction, ещё теперь нельзя получить число аудиозаписей у пользователей, которые их закрыли. Ещё теперь нельзя получать uploaded_only аудиозаписи у тех ну вы поняли короче. - При наведении на длинное название песни оно теперь показывается полностью - Надо ещё что-то сюда написать, так что: При редактировании аудиозаписи название окна теперь не "Редактировать", а "Редактировать аудиозапись", а вместо кнопки OK кнопка "Сохранить" * . - Добавлен тур по аудиозаписям, но пока без скриншотов. - "Мои Аудиозаписи" в меню теперь располагаются под Моими Видеозаписями для канона - В настройках приватности "кто может видеть мои аудиозаписи" теперь располагаются под "кто может видеть мои видеозаписи" - В настройках внешнего вида мои аудиозаписи тоже под видео - Изменён на странице аудиозаписей. Теперь показывается "Аудиозаписи" + имя пользователя в родительном падеже. А если это группа, то "Аудиозаписи группы". То же самое с плейлистами - Исправлены ссылка в ссылке на странице с плейлистами - При наведении на название песни больше не сносится иконка explicit - Добавлена максимальная длина названия и описания плейлиста при редактировании. * М - Долокализована админка (точно помню, что уже делал это, но ладно) - Удалён лишний пункт "audios" в getLeftMenuItemStatus (реально) - Если. У плеера есть параметр "hideButtons", то при наведении на него не пропадает время. - На странице редактирования/создания плейлиста если у песни длинное название, то оно да похуй короче. Ну в общем лучше стало - Там где нужно, добавлена строка в конце файла - Возвращена строка "photo" в английской локали (я её случайно удалил :+1: ) * у - У изъятых аудиозаписей больше не показывается кнопка "добавить в группу". Так же при нажатии на кнопку удаления из коллекции окно не всплывает. - "Удаление аудио из группы" тоже лучше работать стало с изъятыми аудио. * з - В пикере аудиозаписей "more..." заменено на "показать больше аудиозаписей" - Если включен режим показа оставшегося времени, то при окончании песни больше не показывается "--1:--1" - В пикере аудиозаписей, если у вас нет аудиозаписей и вы ничего не искали, показывается "Вы ещё не добавляли аудиозаписей" - <hr>'ы стали серыми - Добавлены title'ы у кнопок в большом плеере - Проставлены alt'ы у плейлистов * Musique: linux saport) назар хуйню релизнул кста, плейерс клаб два не слушайте не рекомендую * Update and rename gamma-00000-disco.sql to 00041-music.sql * Update 00041-music.sql --------- Co-authored-by: Ilya Prokopenko <dsrev@protonmail.com> Co-authored-by: n1rwana <aydashkin@vk.com> Co-authored-by: lalka2018 <99399973+lalka2016@users.noreply.github.com> Co-authored-by: veselcraft <veselcraft@icloud.com> Co-authored-by: DeathPleiad <43928323+Parad1seF0x@users.noreply.github.com> --- VKAPI/Handlers/Audio.php | 794 ++++++++- VKAPI/Handlers/Groups.php | 32 +- VKAPI/Handlers/Status.php | 24 +- VKAPI/Handlers/Users.php | 6 + VKAPI/Handlers/Wall.php | 25 + Web/Models/Entities/Audio.php | 469 ++++++ Web/Models/Entities/Club.php | 38 +- Web/Models/Entities/MediaCollection.php | 72 +- Web/Models/Entities/Playlist.php | 256 +++ Web/Models/Entities/Report.php | 3 +- Web/Models/Entities/Traits/TAudioStatuses.php | 38 + Web/Models/Entities/Traits/TOwnable.php | 6 + Web/Models/Entities/User.php | 55 +- Web/Models/Entities/Video.php | 2 +- Web/Models/Repositories/Audios.php | 296 ++++ Web/Models/shell/processAudio.ps1 | 39 + Web/Models/shell/processAudio.sh | 35 + Web/Presenters/AdminPresenter.php | 77 +- Web/Presenters/AudioPresenter.php | 696 ++++++++ Web/Presenters/BlobPresenter.php | 4 +- Web/Presenters/CommentPresenter.php | 26 +- Web/Presenters/GroupPresenter.php | 5 +- Web/Presenters/ReportPresenter.php | 4 +- Web/Presenters/SearchPresenter.php | 31 +- Web/Presenters/UserPresenter.php | 11 +- Web/Presenters/WallPresenter.php | 26 +- Web/Presenters/templates/@layout.xml | 14 +- Web/Presenters/templates/About/Tour.xml | 24 +- Web/Presenters/templates/Admin/@layout.xml | 3 + Web/Presenters/templates/Admin/EditMusic.xml | 81 + .../templates/Admin/EditPlaylist.xml | 54 + Web/Presenters/templates/Admin/Music.xml | 135 ++ .../templates/Audio/ApiGetContext.xml | 7 + .../templates/Audio/EditPlaylist.xml | 95 ++ Web/Presenters/templates/Audio/Embed.xml | 20 + Web/Presenters/templates/Audio/List.xml | 126 ++ .../templates/Audio/NewPlaylist.xml | 109 ++ Web/Presenters/templates/Audio/Playlist.xml | 81 + Web/Presenters/templates/Audio/Upload.xml | 212 +++ Web/Presenters/templates/Audio/bigplayer.xml | 56 + Web/Presenters/templates/Audio/player.xml | 69 + Web/Presenters/templates/Audio/tabs.xml | 40 + Web/Presenters/templates/Group/Edit.xml | 9 + Web/Presenters/templates/Group/View.xml | 19 + .../templates/Photos/UploadPhoto.xml | 2 +- Web/Presenters/templates/Report/Tabs.xml | 3 + .../templates/Report/ViewContent.xml | 2 + Web/Presenters/templates/Search/Index.xml | 42 +- Web/Presenters/templates/User/Edit.xml | 7 + Web/Presenters/templates/User/Settings.xml | 24 + Web/Presenters/templates/User/View.xml | 46 +- Web/Presenters/templates/_includeCSS.xml | 4 +- .../templates/components/attachment.xml | 4 + .../templates/components/textArea.xml | 7 + Web/di.yml | 2 + Web/routes.yml | 52 +- Web/static/css/audios.css | 661 ++++++++ Web/static/css/main.css | 229 +-- Web/static/img/audio.png | Bin 0 -> 560 bytes Web/static/img/audios_controls.png | Bin 0 -> 3706 bytes Web/static/img/bullet.gif | Bin 0 -> 53 bytes Web/static/img/explicit.svg | 4 + Web/static/img/favicons/favicon24_paused.png | Bin 0 -> 932 bytes Web/static/img/favicons/favicon24_playing.png | Bin 0 -> 1081 bytes Web/static/img/play_buttons.gif | Bin 0 -> 103 bytes Web/static/img/progressbar.gif | Bin 0 -> 1018 bytes Web/static/img/song.jpg | Bin 0 -> 2481 bytes Web/static/img/tour/audios.png | Bin 0 -> 6234 bytes Web/static/img/tour/audios_playlists.png | Bin 0 -> 6234 bytes Web/static/img/tour/audios_search.png | Bin 0 -> 6234 bytes Web/static/img/tour/audios_upload.png | Bin 0 -> 6234 bytes Web/static/js/al_music.js | 1433 +++++++++++++++++ Web/static/js/al_playlists.js | 113 ++ Web/static/js/al_wall.js | 24 +- Web/static/js/package.json | 2 + Web/static/js/yarn.lock | 67 + bootstrap.php | 27 + composer.json | 1 + install/init-static-db.sql | 21 - install/sqls/00041-music.sql | 100 ++ locales/en.strings | 165 +- locales/ru.strings | 165 +- openvk-example.yml | 2 + themepacks/midnight/stylesheet.css | 127 +- themepacks/openvk_modern/stylesheet.css | 47 +- 85 files changed, 7333 insertions(+), 274 deletions(-) create mode 100644 Web/Models/Entities/Audio.php create mode 100644 Web/Models/Entities/Playlist.php create mode 100644 Web/Models/Entities/Traits/TAudioStatuses.php create mode 100644 Web/Models/Repositories/Audios.php create mode 100644 Web/Models/shell/processAudio.ps1 create mode 100644 Web/Models/shell/processAudio.sh create mode 100644 Web/Presenters/AudioPresenter.php create mode 100644 Web/Presenters/templates/Admin/EditMusic.xml create mode 100644 Web/Presenters/templates/Admin/EditPlaylist.xml create mode 100644 Web/Presenters/templates/Admin/Music.xml create mode 100644 Web/Presenters/templates/Audio/ApiGetContext.xml create mode 100644 Web/Presenters/templates/Audio/EditPlaylist.xml create mode 100644 Web/Presenters/templates/Audio/Embed.xml create mode 100644 Web/Presenters/templates/Audio/List.xml create mode 100644 Web/Presenters/templates/Audio/NewPlaylist.xml create mode 100644 Web/Presenters/templates/Audio/Playlist.xml create mode 100644 Web/Presenters/templates/Audio/Upload.xml create mode 100644 Web/Presenters/templates/Audio/bigplayer.xml create mode 100644 Web/Presenters/templates/Audio/player.xml create mode 100644 Web/Presenters/templates/Audio/tabs.xml create mode 100644 Web/static/css/audios.css create mode 100644 Web/static/img/audio.png create mode 100644 Web/static/img/audios_controls.png create mode 100644 Web/static/img/bullet.gif create mode 100644 Web/static/img/explicit.svg create mode 100644 Web/static/img/favicons/favicon24_paused.png create mode 100644 Web/static/img/favicons/favicon24_playing.png create mode 100644 Web/static/img/play_buttons.gif create mode 100644 Web/static/img/progressbar.gif create mode 100644 Web/static/img/song.jpg create mode 100644 Web/static/img/tour/audios.png create mode 100644 Web/static/img/tour/audios_playlists.png create mode 100644 Web/static/img/tour/audios_search.png create mode 100644 Web/static/img/tour/audios_upload.png create mode 100644 Web/static/js/al_music.js create mode 100644 Web/static/js/al_playlists.js create mode 100644 install/sqls/00041-music.sql diff --git a/VKAPI/Handlers/Audio.php b/VKAPI/Handlers/Audio.php index 3fa68e72..413a2a3a 100644 --- a/VKAPI/Handlers/Audio.php +++ b/VKAPI/Handlers/Audio.php @@ -1,22 +1,788 @@ <?php declare(strict_types=1); namespace openvk\VKAPI\Handlers; +use Chandler\Database\DatabaseConnection; +use openvk\Web\Models\Entities\Audio as AEntity; +use openvk\Web\Models\Entities\Playlist; +use openvk\Web\Models\Repositories\Audios; +use openvk\Web\Models\Repositories\Clubs; +use openvk\Web\Models\Repositories\Util\EntityStream; final class Audio extends VKAPIRequestHandler { - function get(): object + private function toSafeAudioStruct(?AEntity $audio, ?string $hash = NULL, bool $need_user = false): object + { + if(!$audio) + $this->fail(0404, "Audio not found"); + else if(!$audio->canBeViewedBy($this->getUser())) + $this->fail(201, "Access denied to audio(" . $audio->getPrettyId() . ")"); + + # рофлан ебало + $privApi = $hash && $GLOBALS["csrfCheck"]; + $audioObj = $audio->toVkApiStruct($this->getUser()); + if(!$privApi) { + $audioObj->manifest = false; + $audioObj->keys = false; + } + + if($need_user) { + $user = (new \openvk\Web\Models\Repositories\Users)->get($audio->getOwner()->getId()); + $audioObj->user = (object) [ + "id" => $user->getId(), + "photo" => $user->getAvatarUrl(), + "name" => $user->getCanonicalName(), + "name_gen" => $user->getCanonicalName(), + ]; + } + + return $audioObj; + } + + private function streamToResponse(EntityStream $es, int $offset, int $count, ?string $hash = NULL): object + { + $items = []; + foreach($es->offsetLimit($offset, $count) as $audio) { + $items[] = $this->toSafeAudioStruct($audio, $hash); + } + + return (object) [ + "count" => sizeof($items), + "items" => $items, + ]; + } + + private function validateGenre(?string& $genre_str, ?int $genre_id): void + { + if(!is_null($genre_str)) { + if(!in_array($genre_str, AEntity::genres)) + $this->fail(8, "Invalid genre_str"); + } else if(!is_null($genre_id)) { + $genre_str = array_flip(AEntity::vkGenres)[$genre_id] ?? NULL; + if(!$genre_str) + $this->fail(8, "Invalid genre ID $genre_id"); + } + } + + private function audioFromAnyId(string $id): ?AEntity + { + $descriptor = explode("_", $id); + if(sizeof($descriptor) === 1) { + if(ctype_digit($descriptor[0])) { + $audio = (new Audios)->get((int) $descriptor[0]); + } else { + $aid = base64_decode($descriptor[0], true); + if(!$aid) + $this->fail(8, "Invalid audio $id"); + + $audio = (new Audios)->get((int) $aid); + } + } else if(sizeof($descriptor) === 2) { + $audio = (new Audios)->getByOwnerAndVID((int) $descriptor[0], (int) $descriptor[1]); + } else { + $this->fail(8, "Invalid audio $id"); + } + + return $audio; + } + + function getById(string $audios, ?string $hash = NULL, int $need_user = 0): object + { + $this->requireUser(); + + $audioIds = array_unique(explode(",", $audios)); + if(sizeof($audioIds) === 1) { + $audio = $this->audioFromAnyId($audioIds[0]); + + return (object) [ + "count" => 1, + "items" => [ + $this->toSafeAudioStruct($audio, $hash, (bool) $need_user), + ], + ]; + } else if(sizeof($audioIds) > 6000) { + $this->fail(1980, "Can't get more than 6000 audios at once"); + } + + $audios = []; + foreach($audioIds as $id) + $audios[] = $this->getById($id, $hash)->items[0]; + + return (object) [ + "count" => sizeof($audios), + "items" => $audios, + ]; + } + + function isLagtrain(string $audio_id): int + { + $this->requireUser(); + + $audio = $this->audioFromAnyId($audio_id); + if(!$audio) + $this->fail(0404, "Audio not found"); + + # Possible information disclosure risks are acceptable :D + return (int) (strpos($audio->getName(), "Lagtrain") !== false); + } + + // TODO stub + function getRecommendations(): object + { + return (object) [ + "count" => 0, + "items" => [], + ]; + } + + function getPopular(?int $genre_id = NULL, ?string $genre_str = NULL, int $offset = 0, int $count = 100, ?string $hash = NULL): object + { + $this->requireUser(); + $this->validateGenre($genre_str, $genre_id); + + $results = (new Audios)->getGlobal(Audios::ORDER_POPULAR, $genre_str); + + return $this->streamToResponse($results, $offset, $count, $hash); + } + + function getFeed(?int $genre_id = NULL, ?string $genre_str = NULL, int $offset = 0, int $count = 100, ?string $hash = NULL): object + { + $this->requireUser(); + $this->validateGenre($genre_str, $genre_id); + + $results = (new Audios)->getGlobal(Audios::ORDER_NEW, $genre_str); + + return $this->streamToResponse($results, $offset, $count, $hash); + } + + function search(string $q, int $auto_complete = 0, int $lyrics = 0, int $performer_only = 0, int $sort = 2, int $search_own = 0, int $offset = 0, int $count = 30, ?string $hash = NULL): object + { + $this->requireUser(); + + if(($auto_complete + $search_own) != 0) + $this->fail(10, "auto_complete and search_own are not supported"); + else if($count > 300 || $count < 1) + $this->fail(8, "count is invalid: $count"); + + $results = (new Audios)->search($q, $sort, (bool) $performer_only, (bool) $lyrics); + + return $this->streamToResponse($results, $offset, $count, $hash); + } + + function getCount(int $owner_id, int $uploaded_only = 0): int + { + $this->requireUser(); + + if($owner_id < 0) { + $owner_id *= -1; + $group = (new Clubs)->get($owner_id); + if(!$group) + $this->fail(0404, "Group not found"); + + return (new Audios)->getClubCollectionSize($group); + } + + $user = (new \openvk\Web\Models\Repositories\Users)->get($owner_id); + if(!$user) + $this->fail(0404, "User not found"); + + if(!$user->getPrivacyPermission("audios.read", $this->getUser())) + $this->fail(15, "Access denied"); + + if($uploaded_only) { + return DatabaseConnection::i()->getContext()->table("audios") + ->where([ + "deleted" => false, + "owner" => $owner_id, + ])->count(); + } + + return (new Audios)->getUserCollectionSize($user); + } + + function get(int $owner_id = 0, int $album_id = 0, string $audio_ids = '', int $need_user = 1, int $offset = 0, int $count = 100, int $uploaded_only = 0, int $need_seed = 0, ?string $shuffle_seed = NULL, int $shuffle = 0, ?string $hash = NULL): object { - $serverUrl = ovk_scheme(true) . $_SERVER["SERVER_NAME"]; - - return (object) [ - "count" => 1, - "items" => [(object) [ - "id" => 1, - "owner_id" => 1, - "artist" => "В ОВК ПОКА НЕТ МУЗЫКИ", - "title" => "ЖДИТЕ :)))", - "duration" => 22, - "url" => $serverUrl . "/assets/packages/static/openvk/audio/nomusic.mp3" - ]] - ]; + $this->requireUser(); + + $shuffleSeed = NULL; + $shuffleSeedStr = NULL; + if($shuffle == 1) { + if(!$shuffle_seed) { + if($need_seed == 1) { + $shuffleSeed = openssl_random_pseudo_bytes(6); + $shuffleSeedStr = base64_encode($shuffleSeed); + $shuffleSeed = hexdec(bin2hex($shuffleSeed)); + } else { + $hOffset = ((int) date("i") * 60) + (int) date("s"); + $thisHour = time() - $hOffset; + $shuffleSeed = $thisHour + $this->getUser()->getId(); + $shuffleSeedStr = base64_encode(hex2bin(dechex($shuffleSeed))); + } + } else { + $shuffleSeed = hexdec(bin2hex(base64_decode($shuffle_seed))); + $shuffleSeedStr = $shuffle_seed; + } + } + + if($album_id != 0) { + $album = (new Audios)->getPlaylist($album_id); + if(!$album) + $this->fail(0404, "album_id invalid"); + else if(!$album->canBeViewedBy($this->getUser())) + $this->fail(600, "Can't open this album for reading"); + + $songs = []; + $list = $album->getAudios($offset, $count, $shuffleSeed); + + foreach($list as $song) + $songs[] = $this->toSafeAudioStruct($song, $hash, $need_user == 1); + + $response = (object) [ + "count" => sizeof($songs), + "items" => $songs, + ]; + if(!is_null($shuffleSeed)) + $response->shuffle_seed = $shuffleSeedStr; + + return $response; + } + + if(!empty($audio_ids)) { + $audio_ids = explode(",", $audio_ids); + if(!$audio_ids) + $this->fail(10, "Audio::get@L0d186:explode(string): Unknown error"); + else if(sizeof($audio_ids) < 1) + $this->fail(8, "Invalid audio_ids syntax"); + + if(!is_null($shuffleSeed)) + $audio_ids = knuth_shuffle($audio_ids, $shuffleSeed); + + $obj = $this->getById(implode(",", $audio_ids), $hash, $need_user); + if(!is_null($shuffleSeed)) + $obj->shuffle_seed = $shuffleSeedStr; + + return $obj; + } + + $dbCtx = DatabaseConnection::i()->getContext(); + if($uploaded_only == 1) { + if($owner_id <= 0) + $this->fail(8, "uploaded_only can only be used with owner_id > 0"); + + $user = (new \openvk\Web\Models\Repositories\Users)->get($owner_id); + + if(!$user) + $this->fail(0602, "Invalid user"); + + if(!$user->getPrivacyPermission("audios.read", $this->getUser())) + $this->fail(15, "Access denied: this user chose to hide his audios"); + + if(!is_null($shuffleSeed)) { + $audio_ids = []; + $query = $dbCtx->table("audios")->select("virtual_id")->where([ + "owner" => $owner_id, + "deleted" => 0, + ]); + + foreach($query as $res) + $audio_ids[] = $res->virtual_id; + + $audio_ids = knuth_shuffle($audio_ids, $shuffleSeed); + $audio_ids = array_slice($audio_ids, $offset, $count); + $audio_q = ""; # audio.getById query + foreach($audio_ids as $aid) + $audio_q .= ",$owner_id" . "_$aid"; + + $obj = $this->getById(substr($audio_q, 1), $hash, $need_user); + $obj->shuffle_seed = $shuffleSeedStr; + + return $obj; + } + + $res = (new Audios)->getByUploader((new \openvk\Web\Models\Repositories\Users)->get($owner_id)); + + return $this->streamToResponse($res, $offset, $count, $hash, $need_user); + } + + $query = $dbCtx->table("audio_relations")->select("audio")->where("entity", $owner_id); + if(!is_null($shuffleSeed)) { + $audio_ids = []; + foreach($query as $aid) + $audio_ids[] = $aid->audio; + + $audio_ids = knuth_shuffle($audio_ids, $shuffleSeed); + $audio_ids = array_slice($audio_ids, $offset, $count); + $audio_q = ""; + foreach($audio_ids as $aid) + $audio_q .= ",$aid"; + + $obj = $this->getById(substr($audio_q, 1), $hash, $need_user); + $obj->shuffle_seed = $shuffleSeedStr; + + return $obj; + } + + $items = []; + + if($owner_id > 0) { + $user = (new \openvk\Web\Models\Repositories\Users)->get($owner_id); + + if(!$user) + $this->fail(50, "Invalid user"); + + if(!$user->getPrivacyPermission("audios.read", $this->getUser())) + $this->fail(15, "Access denied: this user chose to hide his audios"); + } + + $audios = (new Audios)->getByEntityID($owner_id, $offset, $count); + foreach($audios as $audio) + $items[] = $this->toSafeAudioStruct($audio, $hash, $need_user == 1); + + return (object) [ + "count" => sizeof($items), + "items" => $items, + ]; } + + function getLyrics(int $lyrics_id): object + { + $this->requireUser(); + + $audio = (new Audios)->get($lyrics_id); + if(!$audio || !$audio->getLyrics()) + $this->fail(0404, "Not found"); + + if(!$audio->canBeViewedBy($this->getUser())) + $this->fail(201, "Access denied to lyrics"); + + return (object) [ + "lyrics_id" => $lyrics_id, + "text" => preg_replace("%\r\n?%", "\n", $audio->getLyrics()), + ]; + } + + function beacon(int $aid, ?int $gid = NULL): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $audio = (new Audios)->get($aid); + if(!$audio) + $this->fail(0404, "Not Found"); + else if(!$audio->canBeViewedBy($this->getUser())) + $this->fail(201, "Insufficient permissions to listen this audio"); + + $group = NULL; + if(!is_null($gid)) { + $group = (new Clubs)->get($gid); + if(!$group) + $this->fail(0404, "Not Found"); + else if(!$group->canBeModifiedBy($this->getUser())) + $this->fail(203, "Insufficient rights to this group"); + } + + return (int) $audio->listen($group ?? $this->getUser()); + } + + function setBroadcast(string $audio, string $target_ids): array + { + $this->requireUser(); + + [$owner, $aid] = explode("_", $audio); + $song = (new Audios)->getByOwnerAndVID((int) $owner, (int) $aid); + $ids = []; + foreach(explode(",", $target_ids) as $id) { + $id = (int) $id; + if($id > 0) { + if ($id != $this->getUser()->getId()) { + $this->fail(600, "Can't listen on behalf of $id"); + } else { + $ids[] = $id; + $this->beacon($song->getId()); + continue; + } + } + + $group = (new Clubs)->get($id * -1); + if(!$group) + $this->fail(0404, "Not Found"); + else if(!$group->canBeModifiedBy($this->getUser())) + $this->fail(203,"Insufficient rights to this group"); + + $ids[] = $id; + $this->beacon($song ? $song->getId() : 0, $id * -1); + } + + return $ids; + } + + function getBroadcastList(string $filter = "all", int $active = 0, ?string $hash = NULL): object + { + $this->requireUser(); + + if(!in_array($filter, ["all", "friends", "groups"])) + $this->fail(8, "Invalid filter $filter"); + + $broadcastList = $this->getUser()->getBroadcastList($filter); + $items = []; + foreach($broadcastList as $res) { + $struct = $res->toVkApiStruct(); + $status = $res->getCurrentAudioStatus(); + + $struct->status_audio = $status ? $this->toSafeAudioStruct($status) : NULL; + $items[] = $struct; + } + + return (object) [ + "count" => sizeof($items), + "items" => $items, + ]; + } + + function edit(int $owner_id, int $audio_id, ?string $artist = NULL, ?string $title = NULL, ?string $text = NULL, ?int $genre_id = NULL, ?string $genre_str = NULL, int $no_search = 0): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $audio = (new Audios)->getByOwnerAndVID($owner_id, $audio_id); + if(!$audio) + $this->fail(0404, "Not Found"); + else if(!$audio->canBeModifiedBy($this->getUser())) + $this->fail(201, "Insufficient permissions to edit this audio"); + + if(!is_null($genre_id)) { + $genre = array_flip(AEntity::vkGenres)[$genre_id] ?? NULL; + if(!$genre) + $this->fail(8, "Invalid genre ID $genre_id"); + + $audio->setGenre($genre); + } else if(!is_null($genre_str)) { + if(!in_array($genre_str, AEntity::genres)) + $this->fail(8, "Invalid genre ID $genre_str"); + + $audio->setGenre($genre_str); + } + + $lyrics = 0; + if(!is_null($text)) { + $audio->setLyrics($text); + $lyrics = $audio->getId(); + } + + if(!is_null($artist)) + $audio->setPerformer($artist); + + if(!is_null($title)) + $audio->setName($title); + + $audio->setSearchability(!((bool) $no_search)); + $audio->setEdited(time()); + $audio->save(); + + return $lyrics; + } + + function add(int $audio_id, int $owner_id, ?int $group_id = NULL, ?int $album_id = NULL): string + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + if(!is_null($album_id)) + $this->fail(10, "album_id not implemented"); + + // TODO get rid of dups + $to = $this->getUser(); + if(!is_null($group_id)) { + $group = (new Clubs)->get($group_id); + if(!$group) + $this->fail(0404, "Invalid group_id"); + else if(!$group->canBeModifiedBy($this->getUser())) + $this->fail(203, "Insufficient rights to this group"); + + $to = $group; + } + + $audio = (new Audios)->getByOwnerAndVID($owner_id, $audio_id); + if(!$audio) + $this->fail(0404, "Not found"); + else if(!$audio->canBeViewedBy($this->getUser())) + $this->fail(201, "Access denied to audio(owner=$owner_id, vid=$audio_id)"); + + try { + $audio->add($to); + } catch(\OverflowException $ex) { + $this->fail(300, "Album is full"); + } + + return $audio->getPrettyId(); + } + + function delete(int $audio_id, int $owner_id, ?int $group_id = NULL): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $from = $this->getUser(); + if(!is_null($group_id)) { + $group = (new Clubs)->get($group_id); + if(!$group) + $this->fail(0404, "Invalid group_id"); + else if(!$group->canBeModifiedBy($this->getUser())) + $this->fail(203, "Insufficient rights to this group"); + + $from = $group; + } + + $audio = (new Audios)->getByOwnerAndVID($owner_id, $audio_id); + if(!$audio) + $this->fail(0404, "Not found"); + + $audio->remove($from); + + return 1; + } + + function restore(int $audio_id, int $owner_id, ?int $group_id = NULL, ?string $hash = NULL): object + { + $this->requireUser(); + + $vid = $this->add($audio_id, $owner_id, $group_id); + + return $this->getById($vid, $hash)->items[0]; + } + + function getAlbums(int $owner_id = 0, int $offset = 0, int $count = 50, int $drop_private = 1): object + { + $this->requireUser(); + + $owner_id = $owner_id == 0 ? $this->getUser()->getId() : $owner_id; + $playlists = []; + + if($owner_id > 0 && $owner_id != $this->getUser()->getId()) { + $user = (new \openvk\Web\Models\Repositories\Users)->get($owner_id); + + if(!$user->getPrivacyPermission("audios.read", $this->getUser())) + $this->fail(50, "Access to playlists denied"); + } + + foreach((new Audios)->getPlaylistsByEntityId($owner_id, $offset, $count) as $playlist) { + if(!$playlist->canBeViewedBy($this->getUser())) { + if($drop_private == 1) + continue; + + $playlists[] = NULL; + continue; + } + + $playlists[] = $playlist->toVkApiStruct($this->getUser()); + } + + return (object) [ + "count" => sizeof($playlists), + "items" => $playlists, + ]; + } + + function searchAlbums(string $query, int $offset = 0, int $limit = 25, int $drop_private = 0): object + { + $this->requireUser(); + + $playlists = []; + $search = (new Audios)->searchPlaylists($query)->offsetLimit($offset, $limit); + foreach($search as $playlist) { + if(!$playlist->canBeViewedBy($this->getUser())) { + if($drop_private == 0) + $playlists[] = NULL; + + continue; + } + + $playlists[] = $playlist->toVkApiStruct($this->getUser()); + } + + return (object) [ + "count" => sizeof($playlists), + "items" => $playlists, + ]; + } + + function addAlbum(string $title, ?string $description = NULL, int $group_id = 0): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $group = NULL; + if($group_id != 0) { + $group = (new Clubs)->get($group_id); + if(!$group) + $this->fail(0404, "Invalid group_id"); + else if(!$group->canBeModifiedBy($this->getUser())) + $this->fail(600, "Insufficient rights to this group"); + } + + $album = new Playlist; + $album->setName($title); + if(!is_null($group)) + $album->setOwner($group_id * -1); + else + $album->setOwner($this->getUser()->getId()); + + if(!is_null($description)) + $album->setDescription($description); + + $album->save(); + if(!is_null($group)) + $album->bookmark($group); + else + $album->bookmark($this->getUser()); + + return $album->getId(); + } + + function editAlbum(int $album_id, ?string $title = NULL, ?string $description = NULL): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios)->getPlaylist($album_id); + if(!$album) + $this->fail(0404, "Album not found"); + else if(!$album->canBeModifiedBy($this->getUser())) + $this->fail(600, "Insufficient rights to this album"); + + if(!is_null($title)) + $album->setName($title); + + if(!is_null($description)) + $album->setDescription($description); + + $album->setEdited(time()); + $album->save(); + + return (int) !(!$title && !$description); + } + + function deleteAlbum(int $album_id): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios)->getPlaylist($album_id); + if(!$album) + $this->fail(0404, "Album not found"); + else if(!$album->canBeModifiedBy($this->getUser())) + $this->fail(600, "Insufficient rights to this album"); + + $album->delete(); + + return 1; + } + + function moveToAlbum(int $album_id, string $audio_ids): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios)->getPlaylist($album_id); + if(!$album) + $this->fail(0404, "Album not found"); + else if(!$album->canBeModifiedBy($this->getUser())) + $this->fail(600, "Insufficient rights to this album"); + + $audios = []; + $audio_ids = array_unique(explode(",", $audio_ids)); + if(sizeof($audio_ids) < 1 || sizeof($audio_ids) > 1000) + $this->fail(8, "audio_ids must contain at least 1 audio and at most 1000"); + + foreach($audio_ids as $audio_id) { + $audio = $this->audioFromAnyId($audio_id); + if(!$audio) + continue; + else if(!$audio->canBeViewedBy($this->getUser())) + continue; + + $audios[] = $audio; + } + + if(sizeof($audios) < 1) + return 0; + + $res = 1; + try { + foreach ($audios as $audio) + $res = min($res, (int) $album->add($audio)); + } catch(\OutOfBoundsException $ex) { + return 0; + } + + return $res; + } + + function removeFromAlbum(int $album_id, string $audio_ids): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios)->getPlaylist($album_id); + if(!$album) + $this->fail(0404, "Album not found"); + else if(!$album->canBeModifiedBy($this->getUser())) + $this->fail(600, "Insufficient rights to this album"); + + $audios = []; + $audio_ids = array_unique(explode(",", $audio_ids)); + if(sizeof($audio_ids) < 1 || sizeof($audio_ids) > 1000) + $this->fail(8, "audio_ids must contain at least 1 audio and at most 1000"); + + foreach($audio_ids as $audio_id) { + $audio = $this->audioFromAnyId($audio_id); + if(!$audio) + continue; + else if($audio->canBeViewedBy($this->getUser())) + continue; + + $audios[] = $audio; + } + + if(sizeof($audios) < 1) + return 0; + + foreach($audios as $audio) + $album->remove($audio); + + return 1; + } + + function copyToAlbum(int $album_id, string $audio_ids): int + { + return $this->moveToAlbum($album_id, $audio_ids); + } + + function bookmarkAlbum(int $id): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios)->getPlaylist($id); + if(!$album) + $this->fail(0404, "Not found"); + + if(!$album->canBeViewedBy($this->getUser())) + $this->fail(600, "Access error"); + + return (int) $album->bookmark($this->getUser()); + } + + function unBookmarkAlbum(int $id): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios)->getPlaylist($id); + if(!$album) + $this->fail(0404, "Not found"); + + if(!$album->canBeViewedBy($this->getUser())) + $this->fail(600, "Access error"); + + return (int) $album->unbookmark($this->getUser()); + } } diff --git a/VKAPI/Handlers/Groups.php b/VKAPI/Handlers/Groups.php index 3123a43f..85a251da 100644 --- a/VKAPI/Handlers/Groups.php +++ b/VKAPI/Handlers/Groups.php @@ -292,7 +292,8 @@ final class Groups extends VKAPIRequestHandler int $topics = NULL, int $adminlist = NULL, int $topicsAboveWall = NULL, - int $hideFromGlobalFeed = NULL) + int $hideFromGlobalFeed = NULL, + int $audio = NULL) { $this->requireUser(); $this->willExecuteWriteAction(); @@ -303,17 +304,22 @@ final class Groups extends VKAPIRequestHandler if(!$club || !$club->canBeModifiedBy($this->getUser())) $this->fail(15, "You can't modify this group."); if(!empty($screen_name) && !$club->setShortcode($screen_name)) $this->fail(103, "Invalid shortcode."); - !is_null($title) ? $club->setName($title) : NULL; - !is_null($description) ? $club->setAbout($description) : NULL; - !is_null($screen_name) ? $club->setShortcode($screen_name) : NULL; - !is_null($website) ? $club->setWebsite((!parse_url($website, PHP_URL_SCHEME) ? "https://" : "") . $website) : NULL; - !is_null($wall) ? $club->setWall($wall) : NULL; - !is_null($topics) ? $club->setEveryone_Can_Create_Topics($topics) : NULL; - !is_null($adminlist) ? $club->setAdministrators_List_Display($adminlist) : NULL; - !is_null($topicsAboveWall) ? $club->setDisplay_Topics_Above_Wall($topicsAboveWall) : NULL; - !is_null($hideFromGlobalFeed) ? $club->setHide_From_Global_Feed($hideFromGlobalFeed) : NULL; + !empty($title) ? $club->setName($title) : NULL; + !empty($description) ? $club->setAbout($description) : NULL; + !empty($screen_name) ? $club->setShortcode($screen_name) : NULL; + !empty($website) ? $club->setWebsite((!parse_url($website, PHP_URL_SCHEME) ? "https://" : "") . $website) : NULL; + !empty($wall) ? $club->setWall($wall) : NULL; + !empty($topics) ? $club->setEveryone_Can_Create_Topics($topics) : NULL; + !empty($adminlist) ? $club->setAdministrators_List_Display($adminlist) : NULL; + !empty($topicsAboveWall) ? $club->setDisplay_Topics_Above_Wall($topicsAboveWall) : NULL; + !empty($hideFromGlobalFeed) ? $club->setHide_From_Global_Feed($hideFromGlobalFeed) : NULL; + in_array($audio, [0, 1]) ? $club->setEveryone_can_upload_audios($audio) : NULL; - $club->save(); + try { + $club->save(); + } catch(\TypeError $e) { + $this->fail(8, "Nothing changed"); + } return 1; } @@ -370,7 +376,7 @@ final class Groups extends VKAPIRequestHandler $arr->items[$i]->can_see_all_posts = 1; break; case "can_see_audio": - $arr->items[$i]->can_see_audio = 0; + $arr->items[$i]->can_see_audio = 1; break; case "can_write_private_message": $arr->items[$i]->can_write_private_message = 0; @@ -469,7 +475,7 @@ final class Groups extends VKAPIRequestHandler "wall" => $club->canPost() == true ? 1 : 0, "photos" => 1, "video" => 0, - "audio" => 0, + "audio" => $club->isEveryoneCanUploadAudios() ? 1 : 0, "docs" => 0, "topics" => $club->isEveryoneCanCreateTopics() == true ? 1 : 0, "wiki" => 0, diff --git a/VKAPI/Handlers/Status.php b/VKAPI/Handlers/Status.php index 843f42bd..a1b104a2 100644 --- a/VKAPI/Handlers/Status.php +++ b/VKAPI/Handlers/Status.php @@ -8,13 +8,23 @@ final class Status extends VKAPIRequestHandler function get(int $user_id = 0, int $group_id = 0) { $this->requireUser(); - if($user_id == 0 && $group_id == 0) { - return $this->getUser()->getStatus(); - } else { - if($group_id > 0) - $this->fail(501, "Group statuses are not implemented"); - else - return (new UsersRepo)->get($user_id)->getStatus(); + + if($user_id == 0 && $group_id == 0) + $user_id = $this->getUser()->getId(); + + if($group_id > 0) + $this->fail(501, "Group statuses are not implemented"); + else { + $user = (new UsersRepo)->get($user_id); + $audioStatus = $user->getCurrentAudioStatus(); + if($audioStatus) { + return [ + "status" => $user->getStatus(), + "audio" => $audioStatus->toVkApiStruct(), + ]; + } + + return $user->getStatus(); } } diff --git a/VKAPI/Handlers/Users.php b/VKAPI/Handlers/Users.php index a4298787..68bf828f 100644 --- a/VKAPI/Handlers/Users.php +++ b/VKAPI/Handlers/Users.php @@ -95,6 +95,12 @@ final class Users extends VKAPIRequestHandler case "status": if($usr->getStatus() != NULL) $response[$i]->status = $usr->getStatus(); + + $audioStatus = $usr->getCurrentAudioStatus(); + + if($audioStatus) + $response[$i]->status_audio = $audioStatus->toVkApiStruct(); + break; case "screen_name": if($usr->getShortCode() != NULL) diff --git a/VKAPI/Handlers/Wall.php b/VKAPI/Handlers/Wall.php index 6b78a0b0..693ca3cb 100644 --- a/VKAPI/Handlers/Wall.php +++ b/VKAPI/Handlers/Wall.php @@ -15,6 +15,7 @@ use openvk\Web\Models\Entities\Video; use openvk\Web\Models\Repositories\Videos as VideosRepo; use openvk\Web\Models\Entities\Note; use openvk\Web\Models\Repositories\Notes as NotesRepo; +use openvk\Web\Models\Repositories\Audios as AudiosRepo; final class Wall extends VKAPIRequestHandler { @@ -58,6 +59,8 @@ final class Wall extends VKAPIRequestHandler $attachments[] = $attachment->getApiStructure(); } else if ($attachment instanceof \openvk\Web\Models\Entities\Note) { $attachments[] = $attachment->toVkApiStruct(); + } else if ($attachment instanceof \openvk\Web\Models\Entities\Audio) { + $attachments[] = $attachment->toVkApiStruct($this->getUser()); } else if ($attachment instanceof \openvk\Web\Models\Entities\Post) { $repostAttachments = []; @@ -233,6 +236,8 @@ final class Wall extends VKAPIRequestHandler $attachments[] = $attachment->getApiStructure(); } else if ($attachment instanceof \openvk\Web\Models\Entities\Note) { $attachments[] = $attachment->toVkApiStruct(); + } else if ($attachment instanceof \openvk\Web\Models\Entities\Audio) { + $attachments[] = $attachment->toVkApiStruct($this->getUser()); } else if ($attachment instanceof \openvk\Web\Models\Entities\Post) { $repostAttachments = []; @@ -450,6 +455,8 @@ final class Wall extends VKAPIRequestHandler $attachmentType = "video"; elseif(str_contains($attac, "note")) $attachmentType = "note"; + elseif(str_contains($attac, "audio")) + $attachmentType = "audio"; else $this->fail(205, "Unknown attachment type"); @@ -483,6 +490,12 @@ final class Wall extends VKAPIRequestHandler if(!$attacc->getOwner()->getPrivacyPermission('notes.read', $this->getUser())) $this->fail(11, "Access to note denied"); + $post->attach($attacc); + } elseif($attachmentType == "audio") { + $attacc = (new AudiosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); + if(!$attacc || $attacc->isDeleted()) + $this->fail(100, "Audio does not exist"); + $post->attach($attacc); } } @@ -562,6 +575,8 @@ final class Wall extends VKAPIRequestHandler $attachments[] = $this->getApiPhoto($attachment); } elseif($attachment instanceof \openvk\Web\Models\Entities\Note) { $attachments[] = $attachment->toVkApiStruct(); + } elseif($attachment instanceof \openvk\Web\Models\Entities\Audio) { + $attachments[] = $attachment->toVkApiStruct($this->getUser()); } } @@ -628,6 +643,8 @@ final class Wall extends VKAPIRequestHandler foreach($comment->getChildren() as $attachment) { if($attachment instanceof \openvk\Web\Models\Entities\Photo) { $attachments[] = $this->getApiPhoto($attachment); + } elseif($attachment instanceof \openvk\Web\Models\Entities\Audio) { + $attachments[] = $attachment->toVkApiStruct($this->getUser()); } } @@ -719,6 +736,8 @@ final class Wall extends VKAPIRequestHandler $attachmentType = "photo"; elseif(str_contains($attac, "video")) $attachmentType = "video"; + elseif(str_contains($attac, "audio")) + $attachmentType = "audio"; else $this->fail(205, "Unknown attachment type"); @@ -744,6 +763,12 @@ final class Wall extends VKAPIRequestHandler if(!$attacc->getOwner()->getPrivacyPermission('videos.read', $this->getUser())) $this->fail(11, "Access to video denied"); + $comment->attach($attacc); + } elseif($attachmentType == "audio") { + $attacc = (new AudiosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); + if(!$attacc || $attacc->isDeleted()) + $this->fail(100, "Audio does not exist"); + $comment->attach($attacc); } } diff --git a/Web/Models/Entities/Audio.php b/Web/Models/Entities/Audio.php new file mode 100644 index 00000000..11d8502b --- /dev/null +++ b/Web/Models/Entities/Audio.php @@ -0,0 +1,469 @@ +<?php declare(strict_types=1); +namespace openvk\Web\Models\Entities; +use Chandler\Database\DatabaseConnection; +use openvk\Web\Util\Shell\Exceptions\UnknownCommandException; +use openvk\Web\Util\Shell\Shell; + +/** + * @method setName(string) + * @method setPerformer(string) + * @method setLyrics(string) + * @method setExplicit(bool) + */ +class Audio extends Media +{ + protected $tableName = "audios"; + protected $fileExtension = "mpd"; + + # Taken from winamp :D + const genres = [ + 'Blues','Big Band','Classic Rock','Chorus','Country','Easy Listening','Dance','Acoustic','Disco','Humour','Funk','Speech','Grunge','Chanson','Hip-Hop','Opera','Jazz','Chamber Music','Metal','Sonata','New Age','Symphony','Oldies','Booty Bass','Other','Primus','Pop','Porn Groove','R&B','Satire','Rap','Slow Jam','Reggae','Club','Rock','Tango','Techno','Samba','Industrial','Folklore','Alternative','Ballad','Ska','Power Ballad','Death Metal','Rhythmic Soul','Pranks','Freestyle','Soundtrack','Duet','Euro-Techno','Punk Rock','Ambient','Drum Solo','Trip-Hop','A Cappella','Vocal','Euro-House','Jazz+Funk','Dance Hall','Fusion','Goa','Trance','Drum & Bass','Classical','Club-House','Instrumental','Hardcore','Acid','Terror','House','Indie','Game','BritPop','Sound Clip','Negerpunk','Gospel','Polsk Punk','Noise','Beat','AlternRock','Christian Gangsta Rap','Bass','Heavy Metal','Soul','Black Metal','Punk','Crossover','Space','Contemporary Christian','Meditative','Christian Rock','Instrumental Pop','Merengue','Instrumental Rock','Salsa','Ethnic','Thrash Metal','Gothic','Anime','Darkwave','JPop','Techno-Industrial','Synthpop','Electronic','Abstract','Pop-Folk','Art Rock','Eurodance','Baroque','Dream','Bhangra','Southern Rock','Big Beat','Comedy','Breakbeat','Cult','Chillout','Gangsta Rap','Downtempo','Top 40','Dub','Christian Rap','EBM','Pop / Funk','Eclectic','Jungle','Electro','Native American','Electroclash','Cabaret','Emo','New Wave','Experimental','Psychedelic','Garage','Rave','Global','Showtunes','IDM','Trailer','Illbient','Lo-Fi','Industro-Goth','Tribal','Jam Band','Acid Punk','Krautrock','Acid Jazz','Leftfield','Polka','Lounge','Retro','Math Rock','Musical','New Romantic','Rock & Roll','Nu-Breakz','Hard Rock','Post-Punk','Folk','Post-Rock','Folk-Rock','Psytrance','National Folk','Shoegaze','Swing','Space Rock','Fast Fusion','Trop Rock','Bebob','World Music','Latin','Neoclassical','Revival','Audiobook','Celtic','Audio Theatre','Bluegrass','Neue Deutsche Welle','Avantgarde','Podcast','Gothic Rock','Indie Rock','Progressive Rock','G-Funk','Psychedelic Rock','Dubstep','Symphonic Rock','Garage Rock','Slow Rock','Psybient','Psychobilly','Touhou' + ]; + + # Taken from: https://web.archive.org/web/20220322153107/https://dev.vk.com/reference/objects/audio-genres + const vkGenres = [ + "Rock" => 1, + "Pop" => 2, + "Rap" => 3, + "Hip-Hop" => 3, # VK API lists №3 as Rap & Hip-Hop, but these genres are distinct in OpenVK + "Easy Listening" => 4, + "House" => 5, + "Dance" => 5, + "Instrumental" => 6, + "Metal" => 7, + "Alternative" => 21, + "Dubstep" => 8, + "Jazz" => 1001, + "Blues" => 1001, + "Drum & Bass" => 10, + "Trance" => 11, + "Chanson" => 12, + "Ethnic" => 13, + "Acoustic" => 14, + "Vocal" => 14, + "Reggae" => 15, + "Classical" => 16, + "Indie Pop" => 17, + "Speech" => 19, + "Disco" => 22, + "Other" => 18, + ]; + + private function fileLength(string $filename): int + { + if(!Shell::commandAvailable("ffmpeg") || !Shell::commandAvailable("ffprobe")) + throw new \Exception(); + + $error = NULL; + $streams = Shell::ffprobe("-i", $filename, "-show_streams", "-select_streams a", "-loglevel error")->execute($error); + if($error !== 0) + throw new \DomainException("$filename is not recognized as media container"); + else if(empty($streams) || ctype_space($streams)) + throw new \DomainException("$filename does not contain any audio streams"); + + $vstreams = Shell::ffprobe("-i", $filename, "-show_streams", "-select_streams v", "-loglevel error")->execute($error); + + # check if audio has cover (attached_pic) + preg_match("%attached_pic=([0-1])%", $vstreams, $hasCover); + if(!empty($vstreams) && !ctype_space($vstreams) && ((int)($hasCover[1]) !== 1)) + throw new \DomainException("$filename is a video"); + + $durations = []; + preg_match_all('%duration=([0-9\.]++)%', $streams, $durations); + if(sizeof($durations[1]) === 0) + throw new \DomainException("$filename does not contain any meaningful audio streams"); + + $length = 0; + foreach($durations[1] as $duration) { + $duration = floatval($duration); + if($duration < 1.0 || $duration > 65536.0) + throw new \DomainException("$filename does not contain any meaningful audio streams"); + else + $length = max($length, $duration); + } + + return (int) round($length, 0, PHP_ROUND_HALF_EVEN); + } + + /** + * @throws \Exception + */ + protected function saveFile(string $filename, string $hash): bool + { + $duration = $this->fileLength($filename); + + $kid = openssl_random_pseudo_bytes(16); + $key = openssl_random_pseudo_bytes(16); + $tok = openssl_random_pseudo_bytes(28); + $ss = ceil($duration / 15); + + $this->stateChanges("kid", $kid); + $this->stateChanges("key", $key); + $this->stateChanges("token", $tok); + $this->stateChanges("segment_size", $ss); + $this->stateChanges("length", $duration); + + try { + $args = [ + str_replace("enabled", "available", OPENVK_ROOT), + str_replace("enabled", "available", $this->getBaseDir()), + $hash, + $filename, + + bin2hex($kid), + bin2hex($key), + bin2hex($tok), + $ss, + ]; + + if(Shell::isPowershell()) { + Shell::powershell("-executionpolicy bypass", "-File", __DIR__ . "/../shell/processAudio.ps1", ...$args) + ->start(); + } else { + Shell::bash(__DIR__ . "/../shell/processAudio.sh", ...$args) // Pls workkkkk + ->start(); // idk, not tested :") + } + + # Wait until processAudio will consume the file + $start = time(); + while(file_exists($filename)) + if(time() - $start > 5) + throw new \RuntimeException("Timed out waiting FFMPEG"); + + } catch(UnknownCommandException $ucex) { + exit(OPENVK_ROOT_CONF["openvk"]["debug"] ? "bash/pwsh is not installed" : VIDEOS_FRIENDLY_ERROR); + } + + return true; + } + + function getTitle(): string + { + return $this->getRecord()->name; + } + + function getPerformer(): string + { + return $this->getRecord()->performer; + } + + function getName(): string + { + return $this->getPerformer() . " — " . $this->getTitle(); + } + + function getGenre(): ?string + { + return $this->getRecord()->genre; + } + + function getLyrics(): ?string + { + return !is_null($this->getRecord()->lyrics) ? htmlspecialchars($this->getRecord()->lyrics, ENT_DISALLOWED | ENT_XHTML) : NULL; + } + + function getLength(): int + { + return $this->getRecord()->length; + } + + function getFormattedLength(): string + { + $len = $this->getLength(); + $mins = floor($len / 60); + $secs = $len - ($mins * 60); + + return ( + str_pad((string) $mins, 2, "0", STR_PAD_LEFT) + . ":" . + str_pad((string) $secs, 2, "0", STR_PAD_LEFT) + ); + } + + function getSegmentSize(): float + { + return $this->getRecord()->segment_size; + } + + function getListens(): int + { + return $this->getRecord()->listens; + } + + function getOriginalURL(bool $force = false): string + { + $disallowed = !OPENVK_ROOT_CONF["openvk"]["preferences"]["music"]["exposeOriginalURLs"] && !$force; + if(!$this->isAvailable() || $disallowed) + return ovk_scheme(true) + . $_SERVER["HTTP_HOST"] . ":" + . $_SERVER["HTTP_PORT"] + . "/assets/packages/static/openvk/audio/nomusic.mp3"; + + $key = bin2hex($this->getRecord()->token); + + return str_replace(".mpd", "_fragments", $this->getURL()) . "/original_$key.mp3"; + } + + function getURL(?bool $force = false): string + { + if ($this->isWithdrawn()) return ""; + + return parent::getURL(); + } + + function getKeys(): array + { + $keys[bin2hex($this->getRecord()->kid)] = bin2hex($this->getRecord()->key); + + return $keys; + } + + function isAnonymous(): bool + { + return false; + } + + function isExplicit(): bool + { + return (bool) $this->getRecord()->explicit; + } + + function isWithdrawn(): bool + { + return (bool) $this->getRecord()->withdrawn; + } + + function isUnlisted(): bool + { + return (bool) $this->getRecord()->unlisted; + } + + # NOTICE may flush model to DB if it was just processed + function isAvailable(): bool + { + if($this->getRecord()->processed) + return true; + + # throttle requests to isAvailable to prevent DoS attack if filesystem is actually an S3 storage + if(time() - $this->getRecord()->checked < 5) + return false; + + try { + $fragments = str_replace(".mpd", "_fragments", $this->getFileName()); + $original = "original_" . bin2hex($this->getRecord()->token) . ".mp3"; + if(file_exists("$fragments/$original")) { + # Original gets uploaded after fragments + $this->stateChanges("processed", 0x01); + + return true; + } + } finally { + $this->stateChanges("checked", time()); + $this->save(); + } + + return false; + } + + function isInLibraryOf($entity): bool + { + return sizeof(DatabaseConnection::i()->getContext()->table("audio_relations")->where([ + "entity" => $entity->getId() * ($entity instanceof Club ? -1 : 1), + "audio" => $this->getId(), + ])) != 0; + } + + function add($entity): bool + { + if($this->isInLibraryOf($entity)) + return false; + + $entityId = $entity->getId() * ($entity instanceof Club ? -1 : 1); + $audioRels = DatabaseConnection::i()->getContext()->table("audio_relations"); + if(sizeof($audioRels->where("entity", $entityId)) > 65536) + throw new \OverflowException("Can't have more than 65536 audios in a playlist"); + + $audioRels->insert([ + "entity" => $entityId, + "audio" => $this->getId(), + ]); + + return true; + } + + function remove($entity): bool + { + if(!$this->isInLibraryOf($entity)) + return false; + + DatabaseConnection::i()->getContext()->table("audio_relations")->where([ + "entity" => $entity->getId() * ($entity instanceof Club ? -1 : 1), + "audio" => $this->getId(), + ])->delete(); + + return true; + } + + function listen($entity, Playlist $playlist = NULL): bool + { + $listensTable = DatabaseConnection::i()->getContext()->table("audio_listens"); + $lastListen = $listensTable->where([ + "entity" => $entity->getRealId(), + "audio" => $this->getId(), + ])->order("index DESC")->fetch(); + + if(!$lastListen || (time() - $lastListen->time >= $this->getLength())) { + $listensTable->insert([ + "entity" => $entity->getRealId(), + "audio" => $this->getId(), + "time" => time(), + "playlist" => $playlist ? $playlist->getId() : NULL, + ]); + + if($entity instanceof User) { + $this->stateChanges("listens", ($this->getListens() + 1)); + $this->save(); + + if($playlist) { + $playlist->incrementListens(); + $playlist->save(); + } + } + + $entity->setLast_played_track($this->getId()); + $entity->save(); + + return true; + } + + $lastListen->update([ + "time" => time(), + ]); + + return false; + } + + /** + * Returns compatible with VK API 4.x, 5.x structure. + * + * Always sets album(_id) to NULL at this time. + * If genre is not present in VK genre list, fallbacks to "Other". + * The url and manifest properties will be set to false if the audio can't be played (processing, removed). + * + * Aside from standard VK properties, this method will also return some OVK extended props: + * 1. added - Is in the library of $user? + * 2. editable - Can be edited by $user? + * 3. withdrawn - Removed due to copyright request? + * 4. ready - Can be played at this time? + * 5. genre_str - Full name of genre, NULL if it's undefined + * 6. manifest - URL to MPEG-DASH manifest + * 7. keys - ClearKey DRM keys + * 8. explicit - Marked as NSFW? + * 9. searchable - Can be found via search? + * 10. unique_id - Unique ID of audio + * + * @notice that in case if exposeOriginalURLs is set to false in config, "url" will always contain link to nomusic.mp3, + * unless $forceURLExposure is set to true. + * + * @notice may trigger db flush if the audio is not processed yet, use with caution on unsaved models. + * + * @param ?User $user user, relative to whom "added", "editable" will be set + * @param bool $forceURLExposure force set "url" regardless of config + */ + function toVkApiStruct(?User $user = NULL, bool $forceURLExposure = false): object + { + $obj = (object) []; + $obj->unique_id = base64_encode((string) $this->getId()); + $obj->id = $obj->aid = $this->getVirtualId(); + $obj->artist = $this->getPerformer(); + $obj->title = $this->getTitle(); + $obj->duration = $this->getLength(); + $obj->album_id = $obj->album = NULL; # i forgor to implement + $obj->url = false; + $obj->manifest = false; + $obj->keys = false; + $obj->genre_id = $obj->genre = self::vkGenres[$this->getGenre() ?? ""] ?? 18; # return Other if no match + $obj->genre_str = $this->getGenre(); + $obj->owner_id = $this->getOwner()->getId(); + if($this->getOwner() instanceof Club) + $obj->owner_id *= -1; + + $obj->lyrics = NULL; + if(!is_null($this->getLyrics())) + $obj->lyrics = $this->getId(); + + $obj->added = $user && $this->isInLibraryOf($user); + $obj->editable = $user && $this->canBeModifiedBy($user); + $obj->searchable = !$this->isUnlisted(); + $obj->explicit = $this->isExplicit(); + $obj->withdrawn = $this->isWithdrawn(); + $obj->ready = $this->isAvailable() && !$obj->withdrawn; + if($obj->ready) { + $obj->url = $this->getOriginalURL($forceURLExposure); + $obj->manifest = $this->getURL(); + $obj->keys = $this->getKeys(); + } + + return $obj; + } + + function setOwner(int $oid): void + { + # WARNING: API implementation won't be able to handle groups like that, don't remove + if($oid <= 0) + throw new \OutOfRangeException("Only users can be owners of audio!"); + + $this->stateChanges("owner", $oid); + } + + function setGenre(string $genre): void + { + if(!in_array($genre, Audio::genres)) { + $this->stateChanges("genre", NULL); + return; + } + + $this->stateChanges("genre", $genre); + } + + function setCopyrightStatus(bool $withdrawn = true): void { + $this->stateChanges("withdrawn", $withdrawn); + } + + function setSearchability(bool $searchable = true): void { + $this->stateChanges("unlisted", !$searchable); + } + + function setToken(string $tok): void { + throw new \LogicException("Changing keys is not supported."); + } + + function setKid(string $kid): void { + throw new \LogicException("Changing keys is not supported."); + } + + function setKey(string $key): void { + throw new \LogicException("Changing keys is not supported."); + } + + function setLength(int $len): void { + throw new \LogicException("Changing length is not supported."); + } + + function setSegment_Size(int $len): void { + throw new \LogicException("Changing length is not supported."); + } + + function delete(bool $softly = true): void + { + $ctx = DatabaseConnection::i()->getContext(); + $ctx->table("audio_relations")->where("audio", $this->getId()) + ->delete(); + $ctx->table("audio_listens")->where("audio", $this->getId()) + ->delete(); + $ctx->table("playlist_relations")->where("media", $this->getId()) + ->delete(); + + parent::delete($softly); + } +} diff --git a/Web/Models/Entities/Club.php b/Web/Models/Entities/Club.php index fbdc503b..b8b8838a 100644 --- a/Web/Models/Entities/Club.php +++ b/Web/Models/Entities/Club.php @@ -371,36 +371,60 @@ class Club extends RowModel { return $this->getRecord()->alert; } + + function getRealId(): int + { + return $this->getId() * -1; + } + + function isEveryoneCanUploadAudios(): bool + { + return (bool) $this->getRecord()->everyone_can_upload_audios; + } + + function canUploadAudio(?User $user): bool + { + if(!$user) + return NULL; + + return $this->isEveryoneCanUploadAudios() || $this->canBeModifiedBy($user); + } + + function getAudiosCollectionSize() + { + return (new \openvk\Web\Models\Repositories\Audios)->getClubCollectionSize($this); + } function toVkApiStruct(?User $user = NULL): object { - $res = []; + $res = (object)[]; $res->id = $this->getId(); $res->name = $this->getName(); $res->screen_name = $this->getShortCode(); $res->is_closed = 0; $res->deactivated = NULL; - $res->is_admin = $this->canBeModifiedBy($user); + $res->is_admin = $user && $this->canBeModifiedBy($user); - if($this->canBeModifiedBy($user)) { + if($user && $this->canBeModifiedBy($user)) { $res->admin_level = 3; } - $res->is_member = $this->getSubscriptionStatus($user) ? 1 : 0; + $res->is_member = $user && $this->getSubscriptionStatus($user) ? 1 : 0; $res->type = "group"; $res->photo_50 = $this->getAvatarUrl("miniscule"); $res->photo_100 = $this->getAvatarUrl("tiny"); $res->photo_200 = $this->getAvatarUrl("normal"); - $res->can_create_topic = $this->canBeModifiedBy($user) ? 1 : ($this->isEveryoneCanCreateTopics() ? 1 : 0); + $res->can_create_topic = $user && $this->canBeModifiedBy($user) ? 1 : ($this->isEveryoneCanCreateTopics() ? 1 : 0); - $res->can_post = $this->canBeModifiedBy($user) ? 1 : ($this->canPost() ? 1 : 0); + $res->can_post = $user && $this->canBeModifiedBy($user) ? 1 : ($this->canPost() ? 1 : 0); - return (object) $res; + return $res; } use Traits\TBackDrops; use Traits\TSubscribable; + use Traits\TAudioStatuses; } diff --git a/Web/Models/Entities/MediaCollection.php b/Web/Models/Entities/MediaCollection.php index 05f3835c..1f061988 100644 --- a/Web/Models/Entities/MediaCollection.php +++ b/Web/Models/Entities/MediaCollection.php @@ -17,7 +17,17 @@ abstract class MediaCollection extends RowModel protected $specialNames = []; - private $relations; + protected $relations; + + /** + * Maximum amount of items Collection can have + */ + const MAX_ITEMS = INF; + + /** + * Maximum amount of Collections with same "owner" allowed + */ + const MAX_COUNT = INF; function __construct(?ActiveRow $ar = NULL) { @@ -70,18 +80,29 @@ abstract class MediaCollection extends RowModel } abstract function getCoverURL(): ?string; - - function fetch(int $page = 1, ?int $perPage = NULL): \Traversable + + function fetchClassic(int $offset = 0, ?int $limit = NULL): \Traversable { - $related = $this->getRecord()->related("$this->relTableName.collection")->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE)->order("media ASC"); + $related = $this->getRecord()->related("$this->relTableName.collection") + ->limit($limit ?? OPENVK_DEFAULT_PER_PAGE, $offset) + ->order("media ASC"); + foreach($related as $rel) { $media = $rel->ref($this->entityTableName, "media"); if(!$media) continue; - + yield new $this->entityClassName($media); } } + + function fetch(int $page = 1, ?int $perPage = NULL): \Traversable + { + $page = max(1, $page); + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + + return $this->fetchClassic($perPage * ($page - 1), $perPage); + } function size(): int { @@ -110,7 +131,7 @@ abstract class MediaCollection extends RowModel { return $this->getRecord()->special_type !== 0; } - + function add(RowModel $entity): bool { $this->entitySuitable($entity); @@ -118,6 +139,10 @@ abstract class MediaCollection extends RowModel if(!$this->allowDuplicates) if($this->has($entity)) return false; + + if(self::MAX_ITEMS != INF) + if(sizeof($this->relations->where("collection", $this->getId())) > self::MAX_ITEMS) + throw new \OutOfBoundsException("Collection is full"); $this->relations->insert([ "collection" => $this->getId(), @@ -127,14 +152,14 @@ abstract class MediaCollection extends RowModel return true; } - function remove(RowModel $entity): void + function remove(RowModel $entity): bool { $this->entitySuitable($entity); - $this->relations->where([ + return $this->relations->where([ "collection" => $this->getId(), "media" => $entity->getId(), - ])->delete(); + ])->delete() > 0; } function has(RowModel $entity): bool @@ -148,6 +173,33 @@ abstract class MediaCollection extends RowModel return !is_null($rel); } - + + function save(?bool $log = false): void + { + $thisTable = DatabaseConnection::i()->getContext()->table($this->tableName); + if(self::MAX_COUNT != INF) + if(isset($this->changes["owner"])) + if(sizeof($thisTable->where("owner", $this->changes["owner"])) > self::MAX_COUNT) + throw new \OutOfBoundsException("Maximum amount of collections"); + + if(is_null($this->getRecord())) + if(!isset($this->changes["created"])) + $this->stateChanges("created", time()); + else + $this->stateChanges("edited", time()); + + parent::save($log); + } + + function delete(bool $softly = true): void + { + if(!$softly) { + $this->relations->where("collection", $this->getId()) + ->delete(); + } + + parent::delete($softly); + } + use Traits\TOwnable; } diff --git a/Web/Models/Entities/Playlist.php b/Web/Models/Entities/Playlist.php new file mode 100644 index 00000000..c027a038 --- /dev/null +++ b/Web/Models/Entities/Playlist.php @@ -0,0 +1,256 @@ +<?php declare(strict_types=1); +namespace openvk\Web\Models\Entities; +use Chandler\Database\DatabaseConnection; +use Nette\Database\Table\ActiveRow; +use openvk\Web\Models\Repositories\Audios; +use openvk\Web\Models\Repositories\Photos; +use openvk\Web\Models\RowModel; +use openvk\Web\Models\Entities\Photo; + +/** + * @method setName(string $name) + * @method setDescription(?string $desc) + */ +class Playlist extends MediaCollection +{ + protected $tableName = "playlists"; + protected $relTableName = "playlist_relations"; + protected $entityTableName = "audios"; + protected $entityClassName = 'openvk\Web\Models\Entities\Audio'; + protected $allowDuplicates = false; + + private $importTable; + + const MAX_COUNT = 1000; + const MAX_ITEMS = 10000; + + function __construct(?ActiveRow $ar = NULL) + { + parent::__construct($ar); + + $this->importTable = DatabaseConnection::i()->getContext()->table("playlist_imports"); + } + + function getCoverURL(string $size = "normal"): ?string + { + $photo = (new Photos)->get((int) $this->getRecord()->cover_photo_id); + return is_null($photo) ? "/assets/packages/static/openvk/img/song.jpg" : $photo->getURLBySizeId($size); + } + + function getLength(): int + { + return $this->getRecord()->length; + } + + function getAudios(int $offset = 0, ?int $limit = NULL, ?int $shuffleSeed = NULL): \Traversable + { + if(!$shuffleSeed) { + foreach ($this->fetchClassic($offset, $limit) as $e) + yield $e; # No, I can't return, it will break with [] + + return; + } + + $ids = []; + foreach($this->relations->select("media AS i")->where("collection", $this->getId()) as $rel) + $ids[] = $rel->i; + + $ids = knuth_shuffle($ids, $shuffleSeed); + $ids = array_slice($ids, $offset, $limit ?? OPENVK_DEFAULT_PER_PAGE); + foreach($ids as $id) + yield (new Audios)->get($id); + } + + function add(RowModel $audio): bool + { + if($res = parent::add($audio)) { + $this->stateChanges("length", $this->getRecord()->length + $audio->getLength()); + $this->save(); + } + + return $res; + } + + function remove(RowModel $audio): bool + { + if($res = parent::remove($audio)) { + $this->stateChanges("length", $this->getRecord()->length - $audio->getLength()); + $this->save(); + } + + return $res; + } + + function isBookmarkedBy(RowModel $entity): bool + { + $id = $entity->getId(); + if($entity instanceof Club) + $id *= -1; + + return !is_null($this->importTable->where([ + "entity" => $id, + "playlist" => $this->getId(), + ])->fetch()); + } + + function bookmark(RowModel $entity): bool + { + if($this->isBookmarkedBy($entity)) + return false; + + $id = $entity->getId(); + if($entity instanceof Club) + $id *= -1; + + if($this->importTable->where("entity", $id)->count() > self::MAX_COUNT) + throw new \OutOfBoundsException("Maximum amount of playlists"); + + $this->importTable->insert([ + "entity" => $id, + "playlist" => $this->getId(), + ]); + + return true; + } + + function unbookmark(RowModel $entity): bool + { + $id = $entity->getId(); + if($entity instanceof Club) + $id *= -1; + + $count = $this->importTable->where([ + "entity" => $id, + "playlist" => $this->getId(), + ])->delete(); + + return $count > 0; + } + + function getDescription(): ?string + { + return $this->getRecord()->description; + } + + function getDescriptionHTML(): ?string + { + return htmlspecialchars($this->getRecord()->description, ENT_DISALLOWED | ENT_XHTML); + } + + function getListens() + { + return $this->getRecord()->listens; + } + + function toVkApiStruct(?User $user = NULL): object + { + $oid = $this->getOwner()->getId(); + if($this->getOwner() instanceof Club) + $oid *= -1; + + return (object) [ + "id" => $this->getId(), + "owner_id" => $oid, + "title" => $this->getName(), + "description" => $this->getDescription(), + "size" => $this->size(), + "length" => $this->getLength(), + "created" => $this->getCreationTime()->timestamp(), + "modified" => $this->getEditTime() ? $this->getEditTime()->timestamp() : NULL, + "accessible" => $this->canBeViewedBy($user), + "editable" => $this->canBeModifiedBy($user), + "bookmarked" => $this->isBookmarkedBy($user), + "listens" => $this->getListens(), + "cover_url" => $this->getCoverURL(), + ]; + } + + function setLength(): void + { + throw new \LogicException("Can't set length of playlist manually"); + } + + function resetLength(): bool + { + $this->stateChanges("length", 0); + + return true; + } + + function delete(bool $softly = true): void + { + $ctx = DatabaseConnection::i()->getContext(); + $ctx->table("playlist_imports")->where("playlist", $this->getId()) + ->delete(); + + parent::delete($softly); + } + + function hasAudio(Audio $audio): bool + { + $ctx = DatabaseConnection::i()->getContext(); + return !is_null($ctx->table("playlist_relations")->where([ + "collection" => $this->getId(), + "media" => $audio->getId() + ])->fetch()); + } + + function getCoverPhotoId(): ?int + { + return $this->getRecord()->cover_photo_id; + } + + function canBeModifiedBy(User $user): bool + { + if(!$user) + return false; + + if($this->getOwner() instanceof User) + return $user->getId() == $this->getOwner()->getId(); + else + return $this->getOwner()->canBeModifiedBy($user); + } + + function getLengthInMinutes(): int + { + return (int)round($this->getLength() / 60, PHP_ROUND_HALF_DOWN); + } + + function fastMakeCover(int $owner, array $file) + { + $cover = new Photo; + $cover->setOwner($owner); + $cover->setDescription("Playlist cover image"); + $cover->setFile($file); + $cover->setCreated(time()); + $cover->save(); + + $this->setCover_photo_id($cover->getId()); + + return $cover; + } + + function getURL(): string + { + return "/playlist" . $this->getOwner()->getRealId() . "_" . $this->getId(); + } + + function incrementListens() + { + $this->stateChanges("listens", ($this->getListens() + 1)); + } + + function getMetaDescription(): string + { + $length = $this->getLengthInMinutes(); + + $props = []; + $props[] = tr("audios_count", $this->size()); + $props[] = "<span id='listensCount'>" . tr("listens_count", $this->getListens()) . "</span>"; + if($length > 0) $props[] = tr("minutes_count", $length); + $props[] = tr("created_playlist") . " " . $this->getPublicationTime(); + # if($this->getEditTime()) $props[] = tr("updated_playlist") . " " . $this->getEditTime(); + + return implode(" • ", $props); + } +} diff --git a/Web/Models/Entities/Report.php b/Web/Models/Entities/Report.php index d449a2e8..5487056f 100644 --- a/Web/Models/Entities/Report.php +++ b/Web/Models/Entities/Report.php @@ -5,7 +5,7 @@ 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 openvk\Web\Models\Repositories\{Applications, Comments, Notes, Reports, Audios, Users, Posts, Photos, Videos, Clubs}; use Chandler\Database\DatabaseConnection as DB; use Nette\InvalidStateException as ISE; use Nette\Database\Table\Selection; @@ -74,6 +74,7 @@ class Report extends RowModel else if ($this->getContentType() == "note") return (new Notes)->get($this->getContentId()); else if ($this->getContentType() == "app") return (new Applications)->get($this->getContentId()); else if ($this->getContentType() == "user") return (new Users)->get($this->getContentId()); + else if ($this->getContentType() == "audio") return (new Audios)->get($this->getContentId()); else return null; } diff --git a/Web/Models/Entities/Traits/TAudioStatuses.php b/Web/Models/Entities/Traits/TAudioStatuses.php new file mode 100644 index 00000000..f957a104 --- /dev/null +++ b/Web/Models/Entities/Traits/TAudioStatuses.php @@ -0,0 +1,38 @@ +<?php declare(strict_types=1); +namespace openvk\Web\Models\Entities\Traits; +use openvk\Web\Models\Repositories\Audios; +use Chandler\Database\DatabaseConnection; + +trait TAudioStatuses +{ + function isBroadcastEnabled(): bool + { + if($this->getRealId() < 0) return true; + return (bool) $this->getRecord()->audio_broadcast_enabled; + } + + function getCurrentAudioStatus() + { + if(!$this->isBroadcastEnabled()) return NULL; + + $audioId = $this->getRecord()->last_played_track; + + if(!$audioId) return NULL; + $audio = (new Audios)->get($audioId); + + if(!$audio || $audio->isDeleted()) + return NULL; + + $listensTable = DatabaseConnection::i()->getContext()->table("audio_listens"); + $lastListen = $listensTable->where([ + "entity" => $this->getRealId(), + "audio" => $audio->getId(), + "time >" => (time() - $audio->getLength()) - 10, + ])->fetch(); + + if($lastListen) + return $audio; + + return NULL; + } +} diff --git a/Web/Models/Entities/Traits/TOwnable.php b/Web/Models/Entities/Traits/TOwnable.php index 9dc9ce2a..08e5fde3 100644 --- a/Web/Models/Entities/Traits/TOwnable.php +++ b/Web/Models/Entities/Traits/TOwnable.php @@ -4,6 +4,12 @@ use openvk\Web\Models\Entities\User; trait TOwnable { + function canBeViewedBy(?User $user): bool + { + // TODO implement normal check in master + return true; + } + function canBeModifiedBy(User $user): bool { if(method_exists($this, "isCreatedBySystem")) diff --git a/Web/Models/Entities/User.php b/Web/Models/Entities/User.php index dc227039..291dfb6a 100644 --- a/Web/Models/Entities/User.php +++ b/Web/Models/Entities/User.php @@ -4,7 +4,7 @@ use morphos\Gender; use openvk\Web\Themes\{Themepack, Themepacks}; use openvk\Web\Util\DateTime; use openvk\Web\Models\RowModel; -use openvk\Web\Models\Entities\{Photo, Message, Correspondence, Gift}; +use openvk\Web\Models\Entities\{Photo, Message, Correspondence, Gift, Audio}; use openvk\Web\Models\Repositories\{Applications, Bans, Comments, Notes, Posts, Users, Clubs, Albums, Gifts, Notifications, Videos, Photos}; use openvk\Web\Models\Exceptions\InvalidUserNameException; use Nette\Database\Table\ActiveRow; @@ -190,7 +190,7 @@ class User extends RowModel function getMorphedName(string $case = "genitive", bool $fullName = true): string { $name = $fullName ? ($this->getLastName() . " " . $this->getFirstName()) : $this->getFirstName(); - if(!preg_match("%^[А-яё\-]+$%", $name)) + if(!preg_match("%[А-яё\-]+$%", $name)) return $name; # name is probably not russian $inflected = inflectName($name, $case, $this->isFemale() ? Gender::FEMALE : Gender::MALE); @@ -455,6 +455,7 @@ class User extends RowModel "length" => 1, "mappings" => [ "photos", + "audios", "videos", "messages", "notes", @@ -462,7 +463,7 @@ class User extends RowModel "news", "links", "poster", - "apps" + "apps", ], ])->get($id); } @@ -482,6 +483,7 @@ class User extends RowModel "friends.add", "wall.write", "messages.write", + "audios.read", ], ])->get($id); } @@ -1010,6 +1012,7 @@ class User extends RowModel "friends.add", "wall.write", "messages.write", + "audios.read", ], ])->set($id, $status)->toInteger()); } @@ -1020,6 +1023,7 @@ class User extends RowModel "length" => 1, "mappings" => [ "photos", + "audios", "videos", "messages", "notes", @@ -1027,7 +1031,7 @@ class User extends RowModel "news", "links", "poster", - "apps" + "apps", ], ])->set($id, (int) $status)->toInteger(); @@ -1223,6 +1227,11 @@ class User extends RowModel return $response; } + function getRealId() + { + return $this->getId(); + } + function toVkApiStruct(): object { $res = (object) []; @@ -1239,7 +1248,45 @@ class User extends RowModel return $res; } + + function getAudiosCollectionSize() + { + return (new \openvk\Web\Models\Repositories\Audios)->getUserCollectionSize($this); + } + + function getBroadcastList(string $filter = "friends", bool $shuffle = false) + { + $dbContext = DatabaseConnection::i()->getContext(); + $entityIds = []; + $query = $dbContext->table("subscriptions")->where("follower", $this->getRealId()); + + if($filter != "all") + $query = $query->where("model = ?", "openvk\\Web\\Models\\Entities\\" . ($filter == "groups" ? "Club" : "User")); + + foreach($query as $_rel) { + $entityIds[] = $_rel->model == "openvk\\Web\\Models\\Entities\\Club" ? $_rel->target * -1 : $_rel->target; + } + + if($shuffle) { + $shuffleSeed = openssl_random_pseudo_bytes(6); + $shuffleSeed = hexdec(bin2hex($shuffleSeed)); + $entityIds = knuth_shuffle($entityIds, $shuffleSeed); + } + + $returnArr = []; + + foreach($entityIds as $id) { + $entit = $id > 0 ? (new Users)->get($id) : (new Clubs)->get(abs($id)); + + if($id > 0 && $entit->isDeleted()) return; + $returnArr[] = $entit; + } + + return $returnArr; + } + use Traits\TBackDrops; use Traits\TSubscribable; + use Traits\TAudioStatuses; } diff --git a/Web/Models/Entities/Video.php b/Web/Models/Entities/Video.php index cef48e27..db520ee1 100644 --- a/Web/Models/Entities/Video.php +++ b/Web/Models/Entities/Video.php @@ -1,7 +1,7 @@ <?php declare(strict_types=1); namespace openvk\Web\Models\Entities; use openvk\Web\Util\Shell\Shell; -use openvk\Web\Util\Shell\Shell\Exceptions\{ShellUnavailableException, UnknownCommandException}; +use openvk\Web\Util\Shell\Exceptions\{ShellUnavailableException, UnknownCommandException}; use openvk\Web\Models\VideoDrivers\VideoDriver; use Nette\InvalidStateException as ISE; diff --git a/Web/Models/Repositories/Audios.php b/Web/Models/Repositories/Audios.php new file mode 100644 index 00000000..ad5e822e --- /dev/null +++ b/Web/Models/Repositories/Audios.php @@ -0,0 +1,296 @@ +<?php declare(strict_types=1); +namespace openvk\Web\Models\Repositories; +use Chandler\Database\DatabaseConnection; +use openvk\Web\Models\Entities\Audio; +use openvk\Web\Models\Entities\Club; +use openvk\Web\Models\Entities\Playlist; +use openvk\Web\Models\Entities\User; +use openvk\Web\Models\Repositories\Util\EntityStream; + +class Audios +{ + private $context; + private $audios; + private $rels; + private $playlists; + private $playlistImports; + private $playlistRels; + + const ORDER_NEW = 0; + const ORDER_POPULAR = 1; + + const VK_ORDER_NEW = 0; + const VK_ORDER_LENGTH = 1; + const VK_ORDER_POPULAR = 2; + + function __construct() + { + $this->context = DatabaseConnection::i()->getContext(); + $this->audios = $this->context->table("audios"); + $this->rels = $this->context->table("audio_relations"); + + $this->playlists = $this->context->table("playlists"); + $this->playlistImports = $this->context->table("playlist_imports"); + $this->playlistRels = $this->context->table("playlist_relations"); + } + + function get(int $id): ?Audio + { + $audio = $this->audios->get($id); + if(!$audio) + return NULL; + + return new Audio($audio); + } + + function getPlaylist(int $id): ?Playlist + { + $playlist = $this->playlists->get($id); + if(!$playlist) + return NULL; + + return new Playlist($playlist); + } + + function getByOwnerAndVID(int $owner, int $vId): ?Audio + { + $audio = $this->audios->where([ + "owner" => $owner, + "virtual_id" => $vId, + ])->fetch(); + if(!$audio) return NULL; + + return new Audio($audio); + } + + function getPlaylistByOwnerAndVID(int $owner, int $vId): ?Playlist + { + $playlist = $this->playlists->where([ + "owner" => $owner, + "id" => $vId, + ])->fetch(); + if(!$playlist) return NULL; + + return new Playlist($playlist); + } + + function getByEntityID(int $entity, int $offset = 0, ?int $limit = NULL, ?int& $deleted = nullptr): \Traversable + { + $limit ??= OPENVK_DEFAULT_PER_PAGE; + $iter = $this->rels->where("entity", $entity)->limit($limit, $offset); + foreach($iter as $rel) { + $audio = $this->get($rel->audio); + if(!$audio || $audio->isDeleted()) { + $deleted++; + continue; + } + + yield $audio; + } + } + + function getPlaylistsByEntityId(int $entity, int $offset = 0, ?int $limit = NULL, ?int& $deleted = nullptr): \Traversable + { + $limit ??= OPENVK_DEFAULT_PER_PAGE; + $iter = $this->playlistImports->where("entity", $entity)->limit($limit, $offset); + foreach($iter as $rel) { + $playlist = $this->getPlaylist($rel->playlist); + if(!$playlist || $playlist->isDeleted()) { + $deleted++; + continue; + } + + yield $playlist; + } + } + + function getByUser(User $user, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable + { + return $this->getByEntityID($user->getId(), ($perPage * ($page - 1)), $perPage, $deleted); + } + + function getRandomThreeAudiosByEntityId(int $id): Array + { + $iter = $this->rels->where("entity", $id); + $ids = []; + + foreach($iter as $it) + $ids[] = $it->audio; + + $shuffleSeed = openssl_random_pseudo_bytes(6); + $shuffleSeed = hexdec(bin2hex($shuffleSeed)); + + $ids = knuth_shuffle($ids, $shuffleSeed); + $ids = array_slice($ids, 0, 3); + $audios = []; + + foreach($ids as $id) { + $audio = $this->get((int)$id); + + if(!$audio || $audio->isDeleted()) + continue; + + $audios[] = $audio; + } + + return $audios; + } + + function getByClub(Club $club, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable + { + return $this->getByEntityID($club->getId() * -1, ($perPage * ($page - 1)), $perPage, $deleted); + } + + function getPlaylistsByUser(User $user, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable + { + return $this->getPlaylistsByEntityId($user->getId(), ($perPage * ($page - 1)), $perPage, $deleted); + } + + function getPlaylistsByClub(Club $club, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable + { + return $this->getPlaylistsByEntityId($club->getId() * -1, ($perPage * ($page - 1)), $perPage, $deleted); + } + + function getCollectionSizeByEntityId(int $id): int + { + return sizeof($this->rels->where("entity", $id)); + } + + function getUserCollectionSize(User $user): int + { + return sizeof($this->rels->where("entity", $user->getId())); + } + + function getClubCollectionSize(Club $club): int + { + return sizeof($this->rels->where("entity", $club->getId() * -1)); + } + + function getUserPlaylistsCount(User $user): int + { + return sizeof($this->playlistImports->where("entity", $user->getId())); + } + + function getClubPlaylistsCount(Club $club): int + { + return sizeof($this->playlistImports->where("entity", $club->getId() * -1)); + } + + function getByUploader(User $user): EntityStream + { + $search = $this->audios->where([ + "owner" => $user->getId(), + "deleted" => 0, + ]); + + return new EntityStream("Audio", $search); + } + + function getGlobal(int $order, ?string $genreId = NULL): EntityStream + { + $search = $this->audios->where([ + "deleted" => 0, + "unlisted" => 0, + "withdrawn" => 0, + ])->order($order == Audios::ORDER_NEW ? "created DESC" : "listens DESC"); + + if(!is_null($genreId)) + $search = $search->where("genre", $genreId); + + return new EntityStream("Audio", $search); + } + + function search(string $query, int $sortMode = 0, bool $performerOnly = false, bool $withLyrics = false): EntityStream + { + $columns = $performerOnly ? "performer" : "performer, name"; + $order = (["created", "length", "listens"][$sortMode] ?? "") . " DESC"; + + $search = $this->audios->where([ + "unlisted" => 0, + "deleted" => 0, + ])->where("MATCH ($columns) AGAINST (? WITH QUERY EXPANSION)", $query)->order($order); + + if($withLyrics) + $search = $search->where("lyrics IS NOT NULL"); + + return new EntityStream("Audio", $search); + } + + function searchPlaylists(string $query): EntityStream + { + $search = $this->playlists->where([ + "deleted" => 0, + ])->where("MATCH (`name`, `description`) AGAINST (? IN BOOLEAN MODE)", $query); + + return new EntityStream("Playlist", $search); + } + + function getNew(): EntityStream + { + return new EntityStream("Audio", $this->audios->where("created >= " . (time() - 259200))->where(["withdrawn" => 0, "deleted" => 0, "unlisted" => 0])->order("created DESC")->limit(25)); + } + + function getPopular(): EntityStream + { + return new EntityStream("Audio", $this->audios->where("listens > 0")->where(["withdrawn" => 0, "deleted" => 0, "unlisted" => 0])->order("listens DESC")->limit(25)); + } + + function isAdded(int $user_id, int $audio_id): bool + { + return !is_null($this->rels->where([ + "entity" => $user_id, + "audio" => $audio_id + ])->fetch()); + } + + function find(string $query, array $pars = [], string $sort = "id DESC", int $page = 1, ?int $perPage = NULL): \Traversable + { + $query = "%$query%"; + $result = $this->audios->where([ + "unlisted" => 0, + "deleted" => 0, + ]); + + $notNullParams = []; + + foreach($pars as $paramName => $paramValue) + if($paramName != "before" && $paramName != "after" && $paramName != "only_performers") + $paramValue != NULL ? $notNullParams+=["$paramName" => "%$paramValue%"] : NULL; + else + $paramValue != NULL ? $notNullParams+=["$paramName" => "$paramValue"] : NULL; + + $nnparamsCount = sizeof($notNullParams); + + if($notNullParams["only_performers"] == "1") { + $result->where("performer LIKE ?", $query); + } else { + $result->where("name LIKE ? OR performer LIKE ?", $query, $query); + } + + if($nnparamsCount > 0) { + foreach($notNullParams as $paramName => $paramValue) { + switch($paramName) { + case "before": + $result->where("created < ?", $paramValue); + break; + case "after": + $result->where("created > ?", $paramValue); + break; + case "with_lyrics": + $result->where("lyrics IS NOT NULL"); + break; + } + } + } + + return new Util\EntityStream("Audio", $result->order($sort)); + } + + function findPlaylists(string $query, int $page = 1, ?int $perPage = NULL): \Traversable + { + $query = "%$query%"; + $result = $this->playlists->where("name LIKE ?", $query); + + return new Util\EntityStream("Playlist", $result); + } +} diff --git a/Web/Models/shell/processAudio.ps1 b/Web/Models/shell/processAudio.ps1 new file mode 100644 index 00000000..206e11e1 --- /dev/null +++ b/Web/Models/shell/processAudio.ps1 @@ -0,0 +1,39 @@ +$ovkRoot = $args[0] +$storageDir = $args[1] +$fileHash = $args[2] +$hashPart = $fileHash.substring(0, 2) +$filename = $args[3] +$audioFile = [System.IO.Path]::GetTempFileName() +$temp = [System.IO.Path]::GetTempFileName() + +$keyID = $args[4] +$key = $args[5] +$token = $args[6] +$seg = $args[7] + +$shell = Get-WmiObject Win32_process -filter "ProcessId = $PID" +$shell.SetPriority(16384) # because there's no "nice" program in Windows we just set a lower priority for entire tree + +Remove-Item $temp +Remove-Item $audioFile +New-Item -ItemType "directory" $temp +New-Item -ItemType "directory" ("$temp/$fileHash" + '_fragments') +New-Item -ItemType "directory" ("$storageDir/$hashPart/$fileHash" + '_fragments') +Set-Location -Path $temp + +Move-Item $filename $audioFile +ffmpeg -i $audioFile -f dash -encryption_scheme cenc-aes-ctr -encryption_key $key ` + -encryption_kid $keyID -map 0:a -c:a aac -ar 44100 -seg_duration $seg ` + -use_timeline 1 -use_template 1 -init_seg_name ($fileHash + '_fragments/0_0.$ext$') ` + -media_seg_name ($fileHash + '_fragments/chunk$Number%06d$_$RepresentationID$.$ext$') -adaptation_sets 'id=0,streams=a' ` + "$fileHash.mpd" + +ffmpeg -i $audioFile -vn -ar 44100 "original_$token.mp3" +Move-Item "original_$token.mp3" ($fileHash + '_fragments') + +Get-ChildItem -Path ($fileHash + '_fragments/*') | Move-Item -Destination ("$storageDir/$hashPart/$fileHash" + '_fragments') +Move-Item -Path ("$fileHash.mpd") -Destination "$storageDir/$hashPart" + +cd .. +Remove-Item -Recurse $temp +Remove-Item $audioFile diff --git a/Web/Models/shell/processAudio.sh b/Web/Models/shell/processAudio.sh new file mode 100644 index 00000000..ab5a5c55 --- /dev/null +++ b/Web/Models/shell/processAudio.sh @@ -0,0 +1,35 @@ +ovkRoot=$1 +storageDir=$2 +fileHash=$3 +hashPart=$(echo $fileHash | cut -c1-2) +filename=$4 +audioFile=$(mktemp) +temp=$(mktemp -d) + +keyID=$5 +key=$6 +token=$7 +seg=$8 + +trap 'rm -f "$temp" "$audioFile"' EXIT + +mkdir -p "$temp/$fileHash"_fragments +mkdir -p "$storageDir/$hashPart/$fileHash"_fragments +cd "$temp" + +mv "$filename" "$audioFile" +ffmpeg -i "$audioFile" -f dash -encryption_scheme cenc-aes-ctr -encryption_key "$key" \ + -encryption_kid "$keyID" -map 0 -c:a aac -ar 44100 -seg_duration "$seg" \ + -use_timeline 1 -use_template 1 -init_seg_name "$fileHash"_fragments/0_0."\$ext\$" \ + -media_seg_name "$fileHash"_fragments/chunk"\$Number"%06d\$_"\$RepresentationID\$"."\$ext\$" -adaptation_sets 'id=0,streams=a' \ + "$fileHash.mpd" + +ffmpeg -i "$audioFile" -vn -ar 44100 "original_$token.mp3" +mv "original_$token.mp3" "$fileHash"_fragments + +mv "$fileHash"_fragments "$storageDir/$hashPart" +mv "$fileHash.mpd" "$storageDir/$hashPart" + +cd .. +rm -rf "$temp" +rm -f "$audioFile" diff --git a/Web/Presenters/AdminPresenter.php b/Web/Presenters/AdminPresenter.php index 14fbbc74..658d2f4b 100644 --- a/Web/Presenters/AdminPresenter.php +++ b/Web/Presenters/AdminPresenter.php @@ -3,7 +3,19 @@ namespace openvk\Web\Presenters; use Chandler\Database\Log; use Chandler\Database\Logs; use openvk\Web\Models\Entities\{Voucher, Gift, GiftCategory, User, BannedLink}; -use openvk\Web\Models\Repositories\{Bans, ChandlerGroups, ChandlerUsers, Photos, Posts, Users, Clubs, Videos, Vouchers, Gifts, BannedLinks}; +use openvk\Web\Models\Repositories\{Audios, + ChandlerGroups, + ChandlerUsers, + Users, + Clubs, + Util\EntityStream, + Vouchers, + Gifts, + BannedLinks, + Bans, + Photos, + Posts, + Videos}; use Chandler\Database\DatabaseConnection; final class AdminPresenter extends OpenVKPresenter @@ -14,9 +26,10 @@ final class AdminPresenter extends OpenVKPresenter private $gifts; private $bannedLinks; private $chandlerGroups; + private $audios; private $logs; - function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gifts, BannedLinks $bannedLinks, ChandlerGroups $chandlerGroups) + function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gifts, BannedLinks $bannedLinks, ChandlerGroups $chandlerGroups, Audios $audios) { $this->users = $users; $this->clubs = $clubs; @@ -24,8 +37,9 @@ final class AdminPresenter extends OpenVKPresenter $this->gifts = $gifts; $this->bannedLinks = $bannedLinks; $this->chandlerGroups = $chandlerGroups; + $this->audios = $audios; $this->logs = DatabaseConnection::i()->getContext()->table("ChandlerLogs"); - + parent::__construct(); } @@ -43,6 +57,15 @@ final class AdminPresenter extends OpenVKPresenter $count = $repo->find($query)->size(); return $repo->find($query)->page($page, 20); } + + private function searchPlaylists(&$count) + { + $query = $this->queryParam("q") ?? ""; + $page = (int) ($this->queryParam("p") ?? 1); + + $count = $this->audios->findPlaylists($query)->size(); + return $this->audios->findPlaylists($query)->page($page, 20); + } function onStartup(): void { @@ -578,6 +601,54 @@ final class AdminPresenter extends OpenVKPresenter $this->redirect("/admin/users/id" . $user->getId()); } + function renderMusic(): void + { + $this->template->mode = in_array($this->queryParam("act"), ["audios", "playlists"]) ? $this->queryParam("act") : "audios"; + if ($this->template->mode === "audios") + $this->template->audios = $this->searchResults($this->audios, $this->template->count); + else + $this->template->playlists = $this->searchPlaylists($this->template->count); + } + + function renderEditMusic(int $audio_id): void + { + $audio = $this->audios->get($audio_id); + $this->template->audio = $audio; + + try { + $this->template->owner = $audio->getOwner()->getId(); + } catch(\Throwable $e) { + $this->template->owner = 1; + } + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $audio->setName($this->postParam("name")); + $audio->setPerformer($this->postParam("performer")); + $audio->setLyrics($this->postParam("text")); + $audio->setGenre($this->postParam("genre")); + $audio->setOwner((int) $this->postParam("owner")); + $audio->setExplicit(!empty($this->postParam("explicit"))); + $audio->setDeleted(!empty($this->postParam("deleted"))); + $audio->setWithdrawn(!empty($this->postParam("withdrawn"))); + $audio->save(); + } + } + + function renderEditPlaylist(int $playlist_id): void + { + $playlist = $this->audios->getPlaylist($playlist_id); + $this->template->playlist = $playlist; + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $playlist->setName($this->postParam("name")); + $playlist->setDescription($this->postParam("description")); + $playlist->setCover_Photo_Id((int) $this->postParam("photo")); + $playlist->setOwner((int) $this->postParam("owner")); + $playlist->setDeleted(!empty($this->postParam("deleted"))); + $playlist->save(); + } + } + function renderLogs(): void { $filter = []; diff --git a/Web/Presenters/AudioPresenter.php b/Web/Presenters/AudioPresenter.php new file mode 100644 index 00000000..3bae710c --- /dev/null +++ b/Web/Presenters/AudioPresenter.php @@ -0,0 +1,696 @@ +<?php declare(strict_types=1); +namespace openvk\Web\Presenters; +use Chandler\Database\DatabaseConnection; +use openvk\Web\Models\Entities\Audio; +use openvk\Web\Models\Entities\Club; +use openvk\Web\Models\Entities\Photo; +use openvk\Web\Models\Entities\Playlist; +use openvk\Web\Models\Repositories\Audios; +use openvk\Web\Models\Repositories\Clubs; +use openvk\Web\Models\Repositories\Users; + +final class AudioPresenter extends OpenVKPresenter +{ + private $audios; + protected $presenterName = "audios"; + + const MAX_AUDIO_SIZE = 25000000; + + function __construct(Audios $audios) + { + $this->audios = $audios; + } + + function renderPopular(): void + { + $this->renderList(NULL, "popular"); + } + + function renderNew(): void + { + $this->renderList(NULL, "new"); + } + + function renderList(?int $owner = NULL, ?string $mode = "list"): void + { + $this->template->_template = "Audio/List.xml"; + $page = (int)($this->queryParam("p") ?? 1); + $audios = []; + + if ($mode === "list") { + $entity = NULL; + if ($owner < 0) { + $entity = (new Clubs)->get($owner * -1); + if (!$entity || $entity->isBanned()) + $this->redirect("/audios" . $this->user->id); + + $audios = $this->audios->getByClub($entity, $page, 10); + $audiosCount = $this->audios->getClubCollectionSize($entity); + } else { + $entity = (new Users)->get($owner); + if (!$entity || $entity->isDeleted() || $entity->isBanned()) + $this->redirect("/audios" . $this->user->id); + + if(!$entity->getPrivacyPermission("audios.read", $this->user->identity)) + $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); + + $audios = $this->audios->getByUser($entity, $page, 10); + $audiosCount = $this->audios->getUserCollectionSize($entity); + } + + if (!$entity) + $this->notFound(); + + $this->template->owner = $entity; + $this->template->ownerId = $owner; + $this->template->club = $owner < 0 ? $entity : NULL; + $this->template->isMy = ($owner > 0 && ($entity->getId() === $this->user->id)); + $this->template->isMyClub = ($owner < 0 && $entity->canBeModifiedBy($this->user->identity)); + } else if ($mode === "new") { + $audios = $this->audios->getNew(); + $audiosCount = $audios->size(); + } else if ($mode === "playlists") { + if($owner < 0) { + $entity = (new Clubs)->get(abs($owner)); + if (!$entity || $entity->isBanned()) + $this->redirect("/playlists" . $this->user->id); + + $playlists = $this->audios->getPlaylistsByClub($entity, $page, 10); + $playlistsCount = $this->audios->getClubPlaylistsCount($entity); + } else { + $entity = (new Users)->get($owner); + if (!$entity || $entity->isDeleted() || $entity->isBanned()) + $this->redirect("/playlists" . $this->user->id); + + if(!$entity->getPrivacyPermission("audios.read", $this->user->identity)) + $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); + + $playlists = $this->audios->getPlaylistsByUser($entity, $page, 9); + $playlistsCount = $this->audios->getUserPlaylistsCount($entity); + } + + $this->template->playlists = iterator_to_array($playlists); + $this->template->playlistsCount = $playlistsCount; + $this->template->owner = $entity; + $this->template->ownerId = $owner; + $this->template->club = $owner < 0 ? $entity : NULL; + $this->template->isMy = ($owner > 0 && ($entity->getId() === $this->user->id)); + $this->template->isMyClub = ($owner < 0 && $entity->canBeModifiedBy($this->user->identity)); + } else { + $audios = $this->audios->getPopular(); + $audiosCount = $audios->size(); + } + + // $this->renderApp("owner=$owner"); + if ($audios !== []) { + $this->template->audios = iterator_to_array($audios); + $this->template->audiosCount = $audiosCount; + } + + $this->template->mode = $mode; + $this->template->page = $page; + + if(in_array($mode, ["list", "new", "popular"]) && $this->user->identity) + $this->template->friendsAudios = $this->user->identity->getBroadcastList("all", true); + } + + function renderEmbed(int $owner, int $id): void + { + $audio = $this->audios->getByOwnerAndVID($owner, $id); + if(!$audio) { + header("HTTP/1.1 404 Not Found"); + exit("<b>" . tr("audio_embed_not_found") . ".</b>"); + } else if($audio->isDeleted()) { + header("HTTP/1.1 410 Not Found"); + exit("<b>" . tr("audio_embed_deleted") . ".</b>"); + } else if($audio->isWithdrawn()) { + header("HTTP/1.1 451 Unavailable for legal reasons"); + exit("<b>" . tr("audio_embed_withdrawn") . ".</b>"); + } else if(!$audio->canBeViewedBy(NULL)) { + header("HTTP/1.1 403 Forbidden"); + exit("<b>" . tr("audio_embed_forbidden") . ".</b>"); + } else if(!$audio->isAvailable()) { + header("HTTP/1.1 425 Too Early"); + exit("<b>" . tr("audio_embed_processing") . ".</b>"); + } + + $this->template->audio = $audio; + } + + function renderUpload(): void + { + $this->assertUserLoggedIn(); + + $group = NULL; + $isAjax = $this->postParam("ajax", false) == 1; + if(!is_null($this->queryParam("gid"))) { + $gid = (int) $this->queryParam("gid"); + $group = (new Clubs)->get($gid); + if(!$group) + $this->flashFail("err", tr("forbidden"), tr("not_enough_permissions_comment"), null, $isAjax); + + if(!$group->canUploadAudio($this->user->identity)) + $this->flashFail("err", tr("forbidden"), tr("not_enough_permissions_comment"), null, $isAjax); + } + + $this->template->group = $group; + + if($_SERVER["REQUEST_METHOD"] !== "POST") + return; + + $upload = $_FILES["blob"]; + if(isset($upload) && file_exists($upload["tmp_name"])) { + if($upload["size"] > self::MAX_AUDIO_SIZE) + $this->flashFail("err", tr("error"), tr("media_file_corrupted_or_too_large"), null, $isAjax); + } else { + $err = !isset($upload) ? 65536 : $upload["error"]; + $err = str_pad(dechex($err), 9, "0", STR_PAD_LEFT); + $readableError = tr("error_generic"); + + switch($upload["error"]) { + default: + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + $readableError = tr("file_too_big"); + break; + case UPLOAD_ERR_PARTIAL: + $readableError = tr("file_loaded_partially"); + break; + case UPLOAD_ERR_NO_FILE: + $readableError = tr("file_not_uploaded"); + break; + case UPLOAD_ERR_NO_TMP_DIR: + $readableError = "Missing a temporary folder."; + break; + case UPLOAD_ERR_CANT_WRITE: + case UPLOAD_ERR_EXTENSION: + $readableError = "Failed to write file to disk. "; + break; + } + + $this->flashFail("err", tr("error"), $readableError . " " . tr("error_code", $err), null, $isAjax); + } + + $performer = $this->postParam("performer"); + $name = $this->postParam("name"); + $lyrics = $this->postParam("lyrics"); + $genre = empty($this->postParam("genre")) ? "Other" : $this->postParam("genre"); + $nsfw = ($this->postParam("explicit") ?? "off") === "on"; + if(empty($performer) || empty($name) || iconv_strlen($performer . $name) > 128) # FQN of audio must not be more than 128 chars + $this->flashFail("err", tr("error"), tr("error_insufficient_info"), null, $isAjax); + + $audio = new Audio; + $audio->setOwner($this->user->id); + $audio->setName($name); + $audio->setPerformer($performer); + $audio->setLyrics(empty($lyrics) ? NULL : $lyrics); + $audio->setGenre($genre); + $audio->setExplicit($nsfw); + + try { + $audio->setFile($upload); + } catch(\DomainException $ex) { + $e = $ex->getMessage(); + $this->flashFail("err", tr("error"), tr("media_file_corrupted_or_too_large") . " $e.", null, $isAjax); + } catch(\RuntimeException $ex) { + $this->flashFail("err", tr("error"), tr("ffmpeg_timeout"), null, $isAjax); + } catch(\BadMethodCallException $ex) { + $this->flashFail("err", tr("error"), "Загрузка аудио под Linux на данный момент не реализована. Следите за обновлениями: <a href='https://github.com/openvk/openvk/pull/512/commits'>https://github.com/openvk/openvk/pull/512/commits</a>", null, $isAjax); + } catch(\Exception $ex) { + $this->flashFail("err", tr("error"), tr("ffmpeg_not_installed"), null, $isAjax); + } + + $audio->save(); + $audio->add($group ?? $this->user->identity); + + if(!$isAjax) + $this->redirect(is_null($group) ? "/audios" . $this->user->id : "/audios-" . $group->getId()); + else { + $redirectLink = "/audios"; + + if(!is_null($group)) + $redirectLink .= $group->getRealId(); + else + $redirectLink .= $this->user->id; + + $pagesCount = (int)ceil((new Audios)->getCollectionSizeByEntityId(isset($group) ? $group->getRealId() : $this->user->id) / 10); + $redirectLink .= "?p=".$pagesCount; + + $this->returnJson([ + "success" => true, + "redirect_link" => $redirectLink, + ]); + } + } + + function renderListen(int $id): void + { + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $this->assertNoCSRF(); + + if(is_null($this->user)) + $this->returnJson(["success" => false]); + + $audio = $this->audios->get($id); + + if ($audio && !$audio->isDeleted() && !$audio->isWithdrawn()) { + if(!empty($this->postParam("playlist"))) { + $playlist = (new Audios)->getPlaylist((int)$this->postParam("playlist")); + + if(!$playlist || $playlist->isDeleted() || !$playlist->canBeViewedBy($this->user->identity) || !$playlist->hasAudio($audio)) + $playlist = NULL; + } + + $listen = $audio->listen($this->user->identity, $playlist); + + $returnArr = ["success" => $listen]; + + if($playlist) + $returnArr["new_playlists_listens"] = $playlist->getListens(); + + $this->returnJson($returnArr); + } + + $this->returnJson(["success" => false]); + } else { + $this->redirect("/"); + } + } + + function renderSearch(): void + { + $this->redirect("/search?type=audios"); + } + + function renderNewPlaylist(): void + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(true); + + $owner = $this->user->id; + + if ($this->requestParam("gid")) { + $club = (new Clubs)->get((int) abs((int)$this->requestParam("gid"))); + if (!$club || $club->isBanned() || !$club->canBeModifiedBy($this->user->identity)) + $this->redirect("/audios" . $this->user->id); + + $owner = ($club->getId() * -1); + + $this->template->club = $club; + } + + $this->template->owner = $owner; + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $title = $this->postParam("title"); + $description = $this->postParam("description"); + $audios = !empty($this->postParam("audios")) ? array_slice(explode(",", $this->postParam("audios")), 0, 100) : []; + + if(empty($title) || iconv_strlen($title) < 1) + $this->flashFail("err", tr("error"), tr("set_playlist_name")); + + $playlist = new Playlist; + $playlist->setOwner($owner); + $playlist->setName(substr($title, 0, 125)); + $playlist->setDescription(substr($description, 0, 2045)); + + if($_FILES["cover"]["error"] === UPLOAD_ERR_OK) { + if(!str_starts_with($_FILES["cover"]["type"], "image")) + $this->flashFail("err", tr("error"), tr("not_a_photo")); + + try { + $playlist->fastMakeCover($this->user->id, $_FILES["cover"]); + } catch(\Throwable $e) { + $this->flashFail("err", tr("error"), tr("invalid_cover_photo")); + } + } + + $playlist->save(); + + foreach($audios as $audio) { + $audio = $this->audios->get((int)$audio); + + if(!$audio || $audio->isDeleted() || !$audio->canBeViewedBy($this->user->identity)) + continue; + + $playlist->add($audio); + } + + $playlist->bookmark(isset($club) ? $club : $this->user->identity); + $this->redirect("/playlist" . $owner . "_" . $playlist->getId()); + } else { + if(isset($club)) { + $this->template->audios = iterator_to_array($this->audios->getByClub($club, 1, 10)); + $count = (new Audios)->getClubCollectionSize($club); + } else { + $this->template->audios = iterator_to_array($this->audios->getByUser($this->user->identity, 1, 10)); + $count = (new Audios)->getUserCollectionSize($this->user->identity); + } + + $this->template->pagesCount = ceil($count / 10); + } + } + + function renderPlaylistAction(int $id) { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(true); + $this->assertNoCSRF(); + + if ($_SERVER["REQUEST_METHOD"] !== "POST") { + header("HTTP/1.1 405 Method Not Allowed"); + $this->redirect("/"); + } + + $playlist = $this->audios->getPlaylist($id); + + if(!$playlist || $playlist->isDeleted()) + $this->flashFail("err", "error", tr("invalid_playlist"), null, true); + + switch ($this->queryParam("act")) { + case "bookmark": + if(!$playlist->isBookmarkedBy($this->user->identity)) + $playlist->bookmark($this->user->identity); + else + $this->flashFail("err", "error", tr("playlist_already_bookmarked"), null, true); + + break; + case "unbookmark": + if($playlist->isBookmarkedBy($this->user->identity)) + $playlist->unbookmark($this->user->identity); + else + $this->flashFail("err", "error", tr("playlist_not_bookmarked"), null, true); + + break; + case "delete": + if($playlist->canBeModifiedBy($this->user->identity)) { + $tmOwner = $playlist->getOwner(); + $playlist->delete(); + } else + $this->flashFail("err", "error", tr("access_denied"), null, true); + + $this->returnJson(["success" => true, "id" => $tmOwner->getRealId()]); + break; + default: + break; + } + + $this->returnJson(["success" => true]); + } + + function renderEditPlaylist(int $owner_id, int $virtual_id) + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + + $playlist = $this->audios->getPlaylistByOwnerAndVID($owner_id, $virtual_id); + $page = (int)($this->queryParam("p") ?? 1); + if (!$playlist || $playlist->isDeleted() || !$playlist->canBeModifiedBy($this->user->identity)) + $this->notFound(); + + $this->template->playlist = $playlist; + $this->template->page = $page; + + $audios = iterator_to_array($playlist->fetch(1, $playlist->size())); + $this->template->audios = array_slice($audios, 0, 10); + $audiosIds = []; + + foreach($audios as $aud) + $audiosIds[] = $aud->getId(); + + $this->template->audiosIds = implode(",", array_unique($audiosIds)) . ","; + $this->template->ownerId = $owner_id; + $this->template->owner = $playlist->getOwner(); + $this->template->pagesCount = $pagesCount = ceil($playlist->size() / 10); + + if($_SERVER["REQUEST_METHOD"] !== "POST") + return; + + $title = $this->postParam("title"); + $description = $this->postParam("description"); + $new_audios = !empty($this->postParam("audios")) ? explode(",", rtrim($this->postParam("audios"), ",")) : []; + + if(empty($title) || iconv_strlen($title) < 1) + $this->flashFail("err", tr("error"), tr("set_playlist_name")); + + $playlist->setName(ovk_proc_strtr($title, 125)); + $playlist->setDescription(ovk_proc_strtr($description, 2045)); + $playlist->setEdited(time()); + $playlist->resetLength(); + + if($_FILES["new_cover"]["error"] === UPLOAD_ERR_OK) { + if(!str_starts_with($_FILES["new_cover"]["type"], "image")) + $this->flashFail("err", tr("error"), tr("not_a_photo")); + + try { + $playlist->fastMakeCover($this->user->id, $_FILES["new_cover"]); + } catch(\Throwable $e) { + $this->flashFail("err", tr("error"), tr("invalid_cover_photo")); + } + } + + $playlist->save(); + + DatabaseConnection::i()->getContext()->table("playlist_relations")->where([ + "collection" => $playlist->getId() + ])->delete(); + + foreach ($new_audios as $new_audio) { + $audio = (new Audios)->get((int)$new_audio); + + if(!$audio || $audio->isDeleted()) + continue; + + $playlist->add($audio); + } + + $this->redirect("/playlist".$playlist->getPrettyId()); + } + + function renderPlaylist(int $owner_id, int $virtual_id): void + { + $playlist = $this->audios->getPlaylistByOwnerAndVID($owner_id, $virtual_id); + $page = (int)($this->queryParam("p") ?? 1); + if (!$playlist || $playlist->isDeleted()) + $this->notFound(); + + $this->template->playlist = $playlist; + $this->template->page = $page; + $this->template->audios = iterator_to_array($playlist->fetch($page, 10)); + $this->template->ownerId = $owner_id; + $this->template->owner = $playlist->getOwner(); + $this->template->isBookmarked = $playlist->isBookmarkedBy($this->user->identity); + $this->template->isMy = $playlist->getOwner()->getId() === $this->user->id; + $this->template->canEdit = $playlist->canBeModifiedBy($this->user->identity); + } + + function renderAction(int $audio_id): void + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(true); + $this->assertNoCSRF(); + + if ($_SERVER["REQUEST_METHOD"] !== "POST") { + header("HTTP/1.1 405 Method Not Allowed"); + $this->redirect("/"); + } + + $audio = $this->audios->get($audio_id); + + if(!$audio || $audio->isDeleted()) + $this->flashFail("err", "error", tr("invalid_audio"), null, true); + + switch ($this->queryParam("act")) { + case "add": + if($audio->isWithdrawn()) + $this->flashFail("err", "error", tr("invalid_audio"), null, true); + + if(!$audio->isInLibraryOf($this->user->identity)) + $audio->add($this->user->identity); + else + $this->flashFail("err", "error", tr("do_have_audio"), null, true); + + break; + + case "remove": + if($audio->isInLibraryOf($this->user->identity)) + $audio->remove($this->user->identity); + else + $this->flashFail("err", "error", tr("do_not_have_audio"), null, true); + + break; + case "remove_club": + $club = (new Clubs)->get((int)$this->postParam("club")); + + if(!$club || !$club->canBeModifiedBy($this->user->identity)) + $this->flashFail("err", "error", tr("access_denied"), null, true); + + if($audio->isInLibraryOf($club)) + $audio->remove($club); + else + $this->flashFail("err", "error", tr("group_hasnt_audio"), null, true); + + break; + case "add_to_club": + $club = (new Clubs)->get((int)$this->postParam("club")); + + if(!$club || !$club->canBeModifiedBy($this->user->identity)) + $this->flashFail("err", "error", tr("access_denied"), null, true); + + if(!$audio->isInLibraryOf($club)) + $audio->add($club); + else + $this->flashFail("err", "error", tr("group_has_audio"), null, true); + + break; + case "delete": + if($audio->canBeModifiedBy($this->user->identity)) + $audio->delete(); + else + $this->flashFail("err", "error", tr("access_denied"), null, true); + + break; + case "edit": + $audio = $this->audios->get($audio_id); + if (!$audio || $audio->isDeleted() || $audio->isWithdrawn()) + $this->flashFail("err", "error", tr("invalid_audio"), null, true); + + if ($audio->getOwner()->getId() !== $this->user->id) + $this->flashFail("err", "error", tr("access_denied"), null, true); + + $performer = $this->postParam("performer"); + $name = $this->postParam("name"); + $lyrics = $this->postParam("lyrics"); + $genre = empty($this->postParam("genre")) ? "undefined" : $this->postParam("genre"); + $nsfw = (int)($this->postParam("explicit") ?? 0) === 1; + $unlisted = (int)($this->postParam("unlisted") ?? 0) === 1; + if(empty($performer) || empty($name) || iconv_strlen($performer . $name) > 128) # FQN of audio must not be more than 128 chars + $this->flashFail("err", tr("error"), tr("error_insufficient_info"), null, true); + + $audio->setName($name); + $audio->setPerformer($performer); + $audio->setLyrics(empty($lyrics) ? NULL : $lyrics); + $audio->setGenre($genre); + $audio->setExplicit($nsfw); + $audio->setSearchability($unlisted); + $audio->setEdited(time()); + $audio->save(); + + $this->returnJson(["success" => true, "new_info" => [ + "name" => ovk_proc_strtr($audio->getTitle(), 40), + "performer" => ovk_proc_strtr($audio->getPerformer(), 40), + "lyrics" => nl2br($audio->getLyrics() ?? ""), + "lyrics_unformatted" => $audio->getLyrics() ?? "", + "explicit" => $audio->isExplicit(), + "genre" => $audio->getGenre(), + "unlisted" => $audio->isUnlisted(), + ]]); + break; + + default: + break; + } + + $this->returnJson(["success" => true]); + } + + function renderPlaylists(int $owner) + { + $this->renderList($owner, "playlists"); + } + + function renderApiGetContext() + { + if ($_SERVER["REQUEST_METHOD"] !== "POST") { + header("HTTP/1.1 405 Method Not Allowed"); + $this->redirect("/"); + } + + $ctx_type = $this->postParam("context"); + $ctx_id = (int)($this->postParam("context_entity")); + $page = (int)($this->postParam("page") ?? 1); + $perPage = 10; + + switch($ctx_type) { + default: + case "entity_audios": + if($ctx_id >= 0) { + $entity = $ctx_id != 0 ? (new Users)->get($ctx_id) : $this->user->identity; + + if(!$entity || !$entity->getPrivacyPermission("audios.read", $this->user->identity)) + $this->flashFail("err", "Error", "Can't get queue", 80, true); + + $audios = $this->audios->getByUser($entity, $page, $perPage); + $audiosCount = $this->audios->getUserCollectionSize($entity); + } else { + $entity = (new Clubs)->get(abs($ctx_id)); + + if(!$entity || $entity->isBanned()) + $this->flashFail("err", "Error", "Can't get queue", 80, true); + + $audios = $this->audios->getByClub($entity, $page, $perPage); + $audiosCount = $this->audios->getClubCollectionSize($entity); + } + break; + case "new_audios": + $audios = $this->audios->getNew(); + $audiosCount = $audios->size(); + break; + case "popular_audios": + $audios = $this->audios->getPopular(); + $audiosCount = $audios->size(); + break; + case "playlist_context": + $playlist = $this->audios->getPlaylist($ctx_id); + + if (!$playlist || $playlist->isDeleted()) + $this->flashFail("err", "Error", "Can't get queue", 80, true); + + $audios = $playlist->fetch($page, 10); + $audiosCount = $playlist->size(); + break; + case "search_context": + $stream = $this->audios->search($this->postParam("query"), 2, $this->postParam("type") === "by_performer"); + $audios = $stream->page($page, 10); + $audiosCount = $stream->size(); + break; + } + + $pagesCount = ceil($audiosCount / $perPage); + + # костылёк для получения плееров в пикере аудиозаписей + if((int)($this->postParam("returnPlayers")) === 1) { + $this->template->audios = $audios; + $this->template->page = $page; + $this->template->pagesCount = $pagesCount; + $this->template->count = $audiosCount; + + return 0; + } + + $audiosArr = []; + + foreach($audios as $audio) { + $audiosArr[] = [ + "id" => $audio->getId(), + "name" => $audio->getTitle(), + "performer" => $audio->getPerformer(), + "keys" => $audio->getKeys(), + "url" => $audio->getUrl(), + "length" => $audio->getLength(), + "available" => $audio->isAvailable(), + "withdrawn" => $audio->isWithdrawn(), + ]; + } + + $resultArr = [ + "success" => true, + "page" => $page, + "perPage" => $perPage, + "pagesCount" => $pagesCount, + "count" => $audiosCount, + "items" => $audiosArr, + ]; + + $this->returnJson($resultArr); + } +} diff --git a/Web/Presenters/BlobPresenter.php b/Web/Presenters/BlobPresenter.php index 7bb3e2be..5987281d 100644 --- a/Web/Presenters/BlobPresenter.php +++ b/Web/Presenters/BlobPresenter.php @@ -18,6 +18,8 @@ final class BlobPresenter extends OpenVKPresenter function renderFile(/*string*/ $dir, string $name, string $format) { + header("Access-Control-Allow-Origin: *"); + $dir = $this->getDirName($dir); $base = realpath(OPENVK_ROOT . "/storage/$dir"); $path = realpath(OPENVK_ROOT . "/storage/$dir/$name.$format"); @@ -37,5 +39,5 @@ final class BlobPresenter extends OpenVKPresenter readfile($path); exit; - } + } } diff --git a/Web/Presenters/CommentPresenter.php b/Web/Presenters/CommentPresenter.php index e005af86..86ccb126 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, Photos}; +use openvk\Web\Models\Repositories\{Comments, Clubs, Videos, Photos, Audios}; final class CommentPresenter extends OpenVKPresenter { @@ -103,8 +103,27 @@ final class CommentPresenter extends OpenVKPresenter } } } + + $audios = []; + + if(!empty($this->postParam("audios"))) { + $un = rtrim($this->postParam("audios"), ","); + $arr = explode(",", $un); + + if(sizeof($arr) < 11) { + foreach($arr as $dat) { + $ids = explode("_", $dat); + $audio = (new Audios)->getByOwnerAndVID((int)$ids[0], (int)$ids[1]); + + if(!$audio || $audio->isDeleted()) + continue; + + $audios[] = $audio; + } + } + } - if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1) + if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1 && sizeof($audios) < 1) $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_empty")); try { @@ -126,6 +145,9 @@ final class CommentPresenter extends OpenVKPresenter if(sizeof($videos) > 0) foreach($videos as $vid) $comment->attach($vid); + + foreach($audios as $audio) + $comment->attach($audio); if($entity->getOwner()->getId() !== $this->user->identity->getId()) if(($owner = $entity->getOwner()) instanceof User) diff --git a/Web/Presenters/GroupPresenter.php b/Web/Presenters/GroupPresenter.php index d3a46fd5..eb8f446c 100644 --- a/Web/Presenters/GroupPresenter.php +++ b/Web/Presenters/GroupPresenter.php @@ -3,7 +3,7 @@ namespace openvk\Web\Presenters; use openvk\Web\Models\Entities\{Club, Photo, Post}; use Nette\InvalidStateException; use openvk\Web\Models\Entities\Notifications\ClubModeratorNotification; -use openvk\Web\Models\Repositories\{Clubs, Users, Albums, Managers, Topics}; +use openvk\Web\Models\Repositories\{Clubs, Users, Albums, Managers, Topics, Audios}; use Chandler\Security\Authenticator; final class GroupPresenter extends OpenVKPresenter @@ -31,6 +31,8 @@ final class GroupPresenter extends OpenVKPresenter $this->template->albumsCount = (new Albums)->getClubAlbumsCount($club); $this->template->topics = (new Topics)->getLastTopics($club, 3); $this->template->topicsCount = (new Topics)->getClubTopicsCount($club); + $this->template->audios = (new Audios)->getRandomThreeAudiosByEntityId($club->getRealId()); + $this->template->audiosCount = (new Audios)->getClubCollectionSize($club); } $this->template->club = $club; @@ -218,6 +220,7 @@ final class GroupPresenter extends OpenVKPresenter $club->setAdministrators_List_Display(empty($this->postParam("administrators_list_display")) ? 0 : $this->postParam("administrators_list_display")); $club->setEveryone_Can_Create_Topics(empty($this->postParam("everyone_can_create_topics")) ? 0 : 1); $club->setDisplay_Topics_Above_Wall(empty($this->postParam("display_topics_above_wall")) ? 0 : 1); + $club->setEveryone_can_upload_audios(empty($this->postParam("upload_audios")) ? 0 : 1); $club->setHide_From_Global_Feed(empty($this->postParam("hide_from_global_feed")) ? 0 : 1); $website = $this->postParam("website") ?? ""; diff --git a/Web/Presenters/ReportPresenter.php b/Web/Presenters/ReportPresenter.php index a87154c8..0c4cbd83 100644 --- a/Web/Presenters/ReportPresenter.php +++ b/Web/Presenters/ReportPresenter.php @@ -23,7 +23,7 @@ final class ReportPresenter extends OpenVKPresenter if ($_SERVER["REQUEST_METHOD"] === "POST") $this->assertNoCSRF(); - $act = in_array($this->queryParam("act"), ["post", "photo", "video", "group", "comment", "note", "app", "user"]) ? $this->queryParam("act") : NULL; + $act = in_array($this->queryParam("act"), ["post", "photo", "video", "group", "comment", "note", "app", "user", "audio"]) ? $this->queryParam("act") : NULL; if (!$this->queryParam("orig")) { $this->template->reports = $this->reports->getReports(0, (int)($this->queryParam("p") ?? 1), $act, $_SERVER["REQUEST_METHOD"] !== "POST"); @@ -88,7 +88,7 @@ final class ReportPresenter extends OpenVKPresenter if(!$id) exit(json_encode([ "error" => tr("error_segmentation") ])); - if(in_array($this->queryParam("type"), ["post", "photo", "video", "group", "comment", "note", "app", "user"])) { + if(in_array($this->queryParam("type"), ["post", "photo", "video", "group", "comment", "note", "app", "user", "audio"])) { if (count(iterator_to_array($this->reports->getDuplicates($this->queryParam("type"), $id, NULL, $this->user->id))) <= 0) { $report = new Report; $report->setUser_id($this->user->id); diff --git a/Web/Presenters/SearchPresenter.php b/Web/Presenters/SearchPresenter.php index fadf9954..d80c06c3 100644 --- a/Web/Presenters/SearchPresenter.php +++ b/Web/Presenters/SearchPresenter.php @@ -1,7 +1,7 @@ <?php declare(strict_types=1); namespace openvk\Web\Presenters; use openvk\Web\Models\Entities\{User, Club}; -use openvk\Web\Models\Repositories\{Users, Clubs, Posts, Comments, Videos, Applications, Notes}; +use openvk\Web\Models\Repositories\{Users, Clubs, Posts, Comments, Videos, Applications, Notes, Audios}; use Chandler\Database\DatabaseConnection; final class SearchPresenter extends OpenVKPresenter @@ -13,6 +13,7 @@ final class SearchPresenter extends OpenVKPresenter private $videos; private $apps; private $notes; + private $audios; function __construct(Users $users, Clubs $clubs) { @@ -23,22 +24,21 @@ final class SearchPresenter extends OpenVKPresenter $this->videos = new Videos; $this->apps = new Applications; $this->notes = new Notes; + $this->audios = new Audios; parent::__construct(); } function renderIndex(): void { + $this->assertUserLoggedIn(); + $query = $this->queryParam("query") ?? ""; $type = $this->queryParam("type") ?? "users"; $sorter = $this->queryParam("sort") ?? "id"; $invert = $this->queryParam("invert") == 1 ? "ASC" : "DESC"; $page = (int) ($this->queryParam("p") ?? 1); - $this->willExecuteWriteAction(); - if($query != "") - $this->assertUserLoggedIn(); - # https://youtu.be/pSAWM5YuXx8 $repos = [ @@ -47,7 +47,7 @@ final class SearchPresenter extends OpenVKPresenter "posts" => "posts", "comments" => "comments", "videos" => "videos", - "audios" => "posts", + "audios" => "audios", "apps" => "apps", "notes" => "notes" ]; @@ -62,7 +62,17 @@ final class SearchPresenter extends OpenVKPresenter break; case "rating": $sort = "rating " . $invert; - break; + break; + case "length": + if($type != "audios") break; + + $sort = "length " . $invert; + break; + case "listens": + if($type != "audios") break; + + $sort = "listens " . $invert; + break; } $parameters = [ @@ -86,18 +96,21 @@ final class SearchPresenter extends OpenVKPresenter "hometown" => $this->queryParam("hometown") != "" ? $this->queryParam("hometown") : NULL, "before" => $this->queryParam("datebefore") != "" ? strtotime($this->queryParam("datebefore")) : NULL, "after" => $this->queryParam("dateafter") != "" ? strtotime($this->queryParam("dateafter")) : NULL, - "gender" => $this->queryParam("gender") != "" && $this->queryParam("gender") != 2 ? $this->queryParam("gender") : NULL + "gender" => $this->queryParam("gender") != "" && $this->queryParam("gender") != 2 ? $this->queryParam("gender") : NULL, + "only_performers" => $this->queryParam("only_performers") == "on" ? "1" : NULL, + "with_lyrics" => $this->queryParam("with_lyrics") == "on" ? true : NULL, ]; $repo = $repos[$type] or $this->throwError(400, "Bad Request", "Invalid search entity $type."); $results = $this->{$repo}->find($query, $parameters, $sort); - $iterator = $results->page($page); + $iterator = $results->page($page, 14); $count = $results->size(); $this->template->iterator = iterator_to_array($iterator); $this->template->count = $count; $this->template->type = $type; $this->template->page = $page; + $this->template->perPage = 14; } } diff --git a/Web/Presenters/UserPresenter.php b/Web/Presenters/UserPresenter.php index e645f888..56d5685b 100644 --- a/Web/Presenters/UserPresenter.php +++ b/Web/Presenters/UserPresenter.php @@ -5,7 +5,7 @@ use openvk\Web\Util\Sms; use openvk\Web\Themes\Themepacks; use openvk\Web\Models\Entities\{Photo, Post, EmailChangeVerification}; use openvk\Web\Models\Entities\Notifications\{CoinsTransferNotification, RatingUpNotification}; -use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Videos, Notes, Vouchers, EmailChangeVerifications}; +use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Videos, Notes, Vouchers, EmailChangeVerifications, Audios}; use openvk\Web\Models\Exceptions\InvalidUserNameException; use openvk\Web\Util\Validator; use Chandler\Security\Authenticator; @@ -45,7 +45,10 @@ final class UserPresenter extends OpenVKPresenter $this->template->videosCount = (new Videos)->getUserVideosCount($user); $this->template->notes = (new Notes)->getUserNotes($user, 1, 4); $this->template->notesCount = (new Notes)->getUserNotesCount($user); - + $this->template->audios = (new Audios)->getRandomThreeAudiosByEntityId($user->getId()); + $this->template->audiosCount = (new Audios)->getUserCollectionSize($user); + $this->template->audioStatus = $user->getCurrentAudioStatus(); + $this->template->user = $user; } } @@ -169,6 +172,7 @@ final class UserPresenter extends OpenVKPresenter if ($this->postParam("gender") <= 1 && $this->postParam("gender") >= 0) $user->setSex($this->postParam("gender")); + $user->setAudio_broadcast_enabled($this->checkbox("broadcast_music")); if(!empty($this->postParam("phone")) && $this->postParam("phone") !== $user->getPhone()) { if(!OPENVK_ROOT_CONF["openvk"]["credentials"]["smsc"]["enable"]) @@ -241,6 +245,7 @@ final class UserPresenter extends OpenVKPresenter } $user->setStatus(empty($this->postParam("status")) ? NULL : $this->postParam("status")); + $user->setAudio_broadcast_enabled($this->postParam("broadcast") == 1); $user->save(); $this->returnJson([ @@ -430,6 +435,7 @@ final class UserPresenter extends OpenVKPresenter "friends.add", "wall.write", "messages.write", + "audios.read", ]; foreach($settings as $setting) { $input = $this->postParam(str_replace(".", "_", $setting)); @@ -474,6 +480,7 @@ final class UserPresenter extends OpenVKPresenter } else if($_GET['act'] === "lMenu") { $settings = [ "menu_bildoj" => "photos", + "menu_muziko" => "audios", "menu_filmetoj" => "videos", "menu_mesagoj" => "messages", "menu_notatoj" => "notes", diff --git a/Web/Presenters/WallPresenter.php b/Web/Presenters/WallPresenter.php index d89b722a..a01c8262 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, Photos}; +use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums, Notes, Videos, Comments, Photos, Audios}; use Chandler\Database\DatabaseConnection; use Nette\InvalidStateException as ISE; use Bhaktaraz\RSSGenerator\Item; @@ -311,8 +311,27 @@ final class WallPresenter extends OpenVKPresenter } } } + + $audios = []; + + if(!empty($this->postParam("audios"))) { + $un = rtrim($this->postParam("audios"), ","); + $arr = explode(",", $un); + + if(sizeof($arr) < 11) { + foreach($arr as $dat) { + $ids = explode("_", $dat); + $audio = (new Audios)->getByOwnerAndVID((int)$ids[0], (int)$ids[1]); + + if(!$audio || $audio->isDeleted()) + continue; + + $audios[] = $audio; + } + } + } - if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1 && !$poll && !$note) + if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1 && sizeof($audios) < 1 && !$poll && !$note) $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big")); try { @@ -341,6 +360,9 @@ final class WallPresenter extends OpenVKPresenter if(!is_null($note)) $post->attach($note); + + foreach($audios as $audio) + $post->attach($audio); if($wall > 0 && $wall !== $this->user->identity->getId()) (new WallPostNotification($wallOwner, $post, $this->user->identity))->emit(); diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml index f8a975e0..31e0bc5c 100644 --- a/Web/Presenters/templates/@layout.xml +++ b/Web/Presenters/templates/@layout.xml @@ -16,6 +16,8 @@ {script "js/node_modules/umbrellajs/umbrella.min.js"} {script "js/l10n.js"} {script "js/openvk.cls.js"} + {script "js/node_modules/dashjs/dist/dash.all.min.js"} + {script "js/al_music.js"} {css "js/node_modules/tippy.js/dist/backdrop.css"} {css "js/node_modules/tippy.js/dist/border.css"} @@ -122,6 +124,7 @@ <option value="comments">{_s_by_comments}</option> <option value="videos">{_s_by_videos}</option> <option value="apps">{_s_by_apps}</option> + <option value="audios">{_s_by_audios}</option> </select> </form> <div class="searchTips" id="srcht" hidden> @@ -140,13 +143,13 @@ <option value="comments" {if str_contains($_SERVER['REQUEST_URI'], "type=comments")}selected{/if}>{_s_by_comments}</option> <option value="videos" {if str_contains($_SERVER['REQUEST_URI'], "type=videos")}selected{/if}>{_s_by_videos}</option> <option value="apps" {if str_contains($_SERVER['REQUEST_URI'], "type=apps")}selected{/if}>{_s_by_apps}</option> + <option value="audios" {if str_contains($_SERVER['REQUEST_URI'], "type=audios")}selected{/if}>{_s_by_audios}</option> </select> <button class="searchBtn"><span style="color:white;font-weight: 600;font-size:12px;">{_header_search}</span></button> </form> <script> let els = document.querySelectorAll("div.dec") - for(const element of els) - { + for(const element of els) { element.style.display = "none" } </script> @@ -182,6 +185,7 @@ </a> <a n:if="$thisUser->getLeftMenuItemStatus('photos')" href="/albums{$thisUser->getId()}" class="link">{_my_photos}</a> <a n:if="$thisUser->getLeftMenuItemStatus('videos')" href="/videos{$thisUser->getId()}" class="link">{_my_videos}</a> + <a n:if="$thisUser->getLeftMenuItemStatus('audios')" href="/audios{$thisUser->getId()}" class="link">{_my_audios}</a> <a n:if="$thisUser->getLeftMenuItemStatus('messages')" href="/im" class="link">{_my_messages} <object type="internal/link" n:if="$thisUser->getUnreadMessagesCount() > 0"> (<b>{$thisUser->getUnreadMessagesCount()}</b>) @@ -426,6 +430,12 @@ //]]> </script> + <script> + window.openvk = { + "audio_genres": {\openvk\Web\Models\Entities\Audio::genres} + } + </script> + {ifset bodyScripts} {include bodyScripts} {/ifset} diff --git a/Web/Presenters/templates/About/Tour.xml b/Web/Presenters/templates/About/Tour.xml index cc484c66..0c02b1ca 100755 --- a/Web/Presenters/templates/About/Tour.xml +++ b/Web/Presenters/templates/About/Tour.xml @@ -178,11 +178,29 @@ <h2>{_tour_section_6_title_1|noescape}</h2> <ul class="listing"> - <li><span>{_tour_section_6_text_1|noescape}</span></li> + <li><span>{_tour_section_6_text_1|noescape}</span></li> + <li><span>{_tour_section_6_text_2|noescape}</span></li> + <li><span>{_tour_section_6_text_3|noescape}</span></li> + <img src="assets/packages/static/openvk/img/tour/audios.png" width="440"> </ul> + + <ul class="listing"> + <li><span>{_tour_section_6_text_4|noescape}</span></li> + <img src="assets/packages/static/openvk/img/tour/audios_search.png" width="440"> + <li><span>{_tour_section_6_text_5|noescape}</span></li> + <img src="assets/packages/static/openvk/img/tour/audios_upload.png" width="440"> + </ul> - - + <p class="big">{_tour_section_6_bottom_text_1|noescape}</p> + + <h2>{_tour_section_6_title_2|noescape}</h2> + + <ul class="listing"> + <li><span>{_tour_section_6_text_6|noescape}</span></li> + <li><span>{_tour_section_6_text_7|noescape}</span></li> + <img src="assets/packages/static/openvk/img/tour/audios_playlists.png" width="440"> + </ul> + <br> </div> diff --git a/Web/Presenters/templates/Admin/@layout.xml b/Web/Presenters/templates/Admin/@layout.xml index f254dd9a..055bd0a0 100644 --- a/Web/Presenters/templates/Admin/@layout.xml +++ b/Web/Presenters/templates/Admin/@layout.xml @@ -97,6 +97,9 @@ <li> <a href="/admin/bannedLinks">{_admin_banned_links}</a> </li> + <li> + <a href="/admin/music">{_admin_music}</a> + </li> </ul> <div class="aui-nav-heading"> <strong>Chandler</strong> diff --git a/Web/Presenters/templates/Admin/EditMusic.xml b/Web/Presenters/templates/Admin/EditMusic.xml new file mode 100644 index 00000000..c2584760 --- /dev/null +++ b/Web/Presenters/templates/Admin/EditMusic.xml @@ -0,0 +1,81 @@ +{extends "@layout.xml"} + +{block title} + {_edit} {$audio->getName()} +{/block} + +{block heading} + {$audio->getName()} +{/block} + +{block content} + <div class="aui-tabs horizontal-tabs"> + <form class="aui" method="POST"> + <div class="field-group"> + <label for="id">ID</label> + <input class="text medium-field" type="number" id="id" disabled value="{$audio->getId()}" /> + </div> + <div class="field-group"> + <label>{_created}</label> + {$audio->getPublicationTime()} + </div> + <div class="field-group"> + <label>{_edited}</label> + {$audio->getEditTime() ?? "never"} + </div> + <div class="field-group"> + <label for="name">{_name}</label> + <input class="text medium-field" type="text" id="name" name="name" value="{$audio->getTitle()}" /> + </div> + <div class="field-group"> + <label for="performer">{_performer}</label> + <input class="text medium-field" type="text" id="performer" name="performer" value="{$audio->getPerformer()}" /> + </div> + <div class="field-group"> + <label for="ext">{_lyrics}</label> + <textarea class="text medium-field" type="text" id="text" name="text" style="resize: vertical;">{$audio->getLyrics()}</textarea> + </div> + <div class="field-group"> + <label>{_admin_audio_length}</label> + {$audio->getFormattedLength()} + </div> + <div class="field-group"> + <label for="ext">{_genre}</label> + <select class="select medium-field" name="genre"> + <option n:foreach='\openvk\Web\Models\Entities\Audio::genres as $genre' + n:attr="selected: $genre == $audio->getGenre()" value="{$genre}"> + {$genre} + </option> + </select> + </div> + <div class="field-group"> + <label>{_admin_original_file}</label> + <audio controls src="{$audio->getOriginalURL(true)}"> + </div> + <hr /> + <div class="field-group"> + <label for="owner">{_owner}</label> + <input class="text medium-field" type="number" id="owner_id" name="owner" value="{$owner}" /> + </div> + <div class="field-group"> + <label for="explicit">Explicit</label> + <input class="toggle-large" type="checkbox" id="explicit" name="explicit" value="1" {if $audio->isExplicit()} checked {/if} /> + </div> + <div class="field-group"> + <label for="deleted">{_deleted}</label> + <input class="toggle-large" type="checkbox" id="deleted" name="deleted" value="1" {if $audio->isDeleted()} checked {/if} /> + </div> + <div class="field-group"> + <label for="withdrawn">{_withdrawn}</label> + <input class="toggle-large" type="checkbox" id="withdrawn" name="withdrawn" value="1" {if $audio->isWithdrawn()} checked {/if} /> + </div> + <hr /> + <div class="buttons-container"> + <div class="buttons"> + <input type="hidden" name="hash" value="{$csrfToken}" /> + <input class="button submit" type="submit" value="{_save}"> + </div> + </div> + </form> + </div> +{/block} diff --git a/Web/Presenters/templates/Admin/EditPlaylist.xml b/Web/Presenters/templates/Admin/EditPlaylist.xml new file mode 100644 index 00000000..b0bd823f --- /dev/null +++ b/Web/Presenters/templates/Admin/EditPlaylist.xml @@ -0,0 +1,54 @@ +{extends "@layout.xml"} + +{block title} + {_edit} {$playlist->getName()} +{/block} + +{block heading} + {$playlist->getName()} +{/block} + +{block content} + <div class="aui-tabs horizontal-tabs"> + <form class="aui" method="POST"> + <div class="field-group"> + <label for="id">ID</label> + <input class="text medium-field" type="number" id="id" disabled value="{$playlist->getId()}" /> + </div> + <div class="field-group"> + <label for="name">{_name}</label> + <input class="text medium-field" type="text" id="name" name="name" value="{$playlist->getName()}" /> + </div> + <div class="field-group"> + <label for="ext">{_description}</label> + <textarea class="text medium-field" type="text" id="description" name="description" style="resize: vertical;">{$playlist->getDescription()}</textarea> + </div> + <div class="field-group"> + <label for="ext">{_admin_cover_id}</label> + <span id="avatar" class="aui-avatar aui-avatar-project aui-avatar-xlarge"> + <span class="aui-avatar-inner"> + <img src="{$playlist->getCoverUrl()}" style="object-fit: cover;"></img> + </span> + </span> + <br /> + <input class="text medium-field" type="number" id="photo" name="photo" value="{$playlist->getCoverPhotoId()}" /> + </div> + <hr /> + <div class="field-group"> + <label for="owner">{_owner}</label> + <input class="text medium-field" type="number" id="owner_id" name="owner" value="{$playlist->getOwner()->getId()}" /> + </div> + <div class="field-group"> + <label for="deleted">{_deleted}</label> + <input class="toggle-large" type="checkbox" id="deleted" name="deleted" value="1" {if $playlist->isDeleted()} checked {/if} /> + </div> + <hr /> + <div class="buttons-container"> + <div class="buttons"> + <input type="hidden" name="hash" value="{$csrfToken}" /> + <input class="button submit" type="submit" value="{_save}"> + </div> + </div> + </form> + </div> +{/block} diff --git a/Web/Presenters/templates/Admin/Music.xml b/Web/Presenters/templates/Admin/Music.xml new file mode 100644 index 00000000..448ee54d --- /dev/null +++ b/Web/Presenters/templates/Admin/Music.xml @@ -0,0 +1,135 @@ +{extends "@layout.xml"} +{var $search = $mode === "audios"} + +{block title} + {_audios} +{/block} + +{block heading} + {_audios} +{/block} + +{block searchTitle} + {include title} +{/block} + +{block content} + <nav class="aui-navgroup aui-navgroup-horizontal"> + <div class="aui-navgroup-inner"> + <div class="aui-navgroup-primary"> + <ul class="aui-nav" resolved=""> + <li n:attr="class => $mode === 'audios' ? 'aui-nav-selected' : ''"> + <a href="?act=audios">{_audios}</a> + </li> + <li n:attr="class => $mode === 'playlists' ? 'aui-nav-selected' : ''"> + <a href="?act=playlists">{_playlists}</a> + </li> + </ul> + </div> + </div> + </nav> + + <table class="aui aui-table-list"> + {if $mode === "audios"} + {var $audios = iterator_to_array($audios)} + {var $amount = sizeof($audios)} + <thead> + <tr> + <th>ID</th> + <th>{_admin_author}</th> + <th>{_peformer}</th> + <th>{_admin_title}</th> + <th>{_genre}</th> + <th>Explicit</th> + <th>{_withdrawn}</th> + <th>{_deleted}</th> + <th>{_created}</th> + <th>{_actions}</th> + </tr> + </thead> + <tbody> + <tr n:foreach="$audios as $audio"> + <td>{$audio->getId()}</td> + <td> + {var $owner = $audio->getOwner()} + <span class="aui-avatar aui-avatar-xsmall"> + <span class="aui-avatar-inner"> + <img src="{$owner->getAvatarUrl('miniscule')}" alt="{$owner->getCanonicalName()}" style="object-fit: cover;" role="presentation" /> + </span> + </span> + + <a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a> + + <span n:if="$owner->isBanned()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed">{_admin_banned}</span> + </td> + <td>{$audio->getPerformer()}</td> + <td>{$audio->getTitle()}</td> + <td>{$audio->getGenre()}</td> + <td>{$audio->isExplicit() ? tr("yes") : tr("no")}</td> + <td n:attr="style => $audio->isWithdrawn() ? 'color: red;' : ''"> + {$audio->isWithdrawn() ? tr("yes") : tr("no")} + </td> + <td n:attr="style => $audio->isDeleted() ? 'color: red;' : ''"> + {$audio->isDeleted() ? tr("yes") : tr("no")} + </td> + <td>{$audio->getPublicationTime()}</td> + <td> + <a class="aui-button aui-button-primary" href="/admin/music/{$audio->getId()}/edit"> + <span class="aui-icon aui-icon-small aui-iconfont-new-edit">Редактировать</span> + </a> + </td> + </tr> + </tbody> + {else} + {var $playlists = iterator_to_array($playlists)} + {var $amount = sizeof($playlists)} + <thead> + <tr> + <th>ID</th> + <th>{_admin_author}</th> + <th>{_name}</th> + <th>{_created_playlist}</th> + <th>{_actions}</th> + </tr> + </thead> + <tbody> + <tr n:foreach="$playlists as $playlist"> + <td>{$playlist->getId()}</td> + <td> + {var $owner = $playlist->getOwner()} + <span class="aui-avatar aui-avatar-xsmall"> + <span class="aui-avatar-inner"> + <img src="{$owner->getAvatarUrl('miniscule')}" alt="{$owner->getCanonicalName()}" style="object-fit: cover;" role="presentation" /> + </span> + </span> + + <a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a> + + <span n:if="$owner->isBanned()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed">{_admin_banned}</span> + </td> + <td> + <span class="aui-avatar aui-avatar-xsmall"> + <span class="aui-avatar-inner"> + <img src="{$playlist->getCoverURL()}" alt="{$owner->getCanonicalName()}" style="object-fit: cover;" role="presentation" /> + </span> + </span> + {ovk_proc_strtr($playlist->getName(), 30)} + </td> + <td>{$playlist->getCreationTime()}</td> + <td> + <a class="aui-button aui-button-primary" href="/admin/playlist/{$playlist->getId()}/edit"> + <span class="aui-icon aui-icon-small aui-iconfont-new-edit">{_edit}</span> + </a> + </td> + </tr> + </tbody> + {/if} + </table> + <br/> + <div align="right"> + {var $isLast = ((10 * (($_GET['p'] ?? 1) - 1)) + $amount) < $count} + + <a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="/admin/music?act={($_GET['act'] ?? 'audios')}&p={($_GET['p'] ?? 1) - 1}">«</a> + <a n:if="$isLast" class="aui-button" href="/admin/music?act={($_GET['act'] ?? 'audios')}&p={($_GET['p'] ?? 1) + 1}">»</a> + </div> +{/block} diff --git a/Web/Presenters/templates/Audio/ApiGetContext.xml b/Web/Presenters/templates/Audio/ApiGetContext.xml new file mode 100644 index 00000000..77b99990 --- /dev/null +++ b/Web/Presenters/templates/Audio/ApiGetContext.xml @@ -0,0 +1,7 @@ +<input type="hidden" name="count" value="{$count}"> +<input type="hidden" name="pagesCount" value="{$pagesCount}"> +<input type="hidden" name="page" value="{$page}"> + +{foreach $audios as $audio} + {include "player.xml", audio => $audio, hideButtons => true} +{/foreach} diff --git a/Web/Presenters/templates/Audio/EditPlaylist.xml b/Web/Presenters/templates/Audio/EditPlaylist.xml new file mode 100644 index 00000000..50dfcc2a --- /dev/null +++ b/Web/Presenters/templates/Audio/EditPlaylist.xml @@ -0,0 +1,95 @@ +{extends "../@layout.xml"} + +{block title}{_edit_playlist}{/block} + +{block header} + <a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a> + » + <a href="/audios{$ownerId}">{_audios}</a> + » + <a href="/playlist{$playlist->getPrettyId()}">{_playlist}</a> + » + {_edit_playlist} +{/block} + +{block content} + <div class="playlistBlock" style="display: flex;margin-top: 0px;"> + <div class="playlistCover"> + <a> + <img src="{$playlist->getCoverURL('normal')}" alt="{_playlist_cover}"> + </a> + + <div class="profile_links" style="width: 139px;"> + <a class="profile_link" style="width: 98%;" id="_deletePlaylist" data-id="{$playlist->getId()}">{_delete_playlist}</a> + </div> + </div> + + <div style="padding-left: 13px;width:75%"> + <div class="playlistInfo"> + <input value="{$playlist->getName()}" type="text" name="title" maxlength="125"> + </div> + + <div class="moreInfo"> + <textarea name="description" maxlength="2045" style="margin-top: 11px;">{$playlist->getDescription()}</textarea> + </div> + </div> + </div> + + <div style="margin-top: 19px;"> + <input id="playlist_query" type="text" style="height: 26px;" placeholder="{_header_search}"> + <div class="playlistAudiosContainer editContainer"> + <div id="newPlaylistAudios" n:foreach="$audios as $audio"> + <div class="playerContainer"> + {include "player.xml", audio => $audio, hideButtons => true} + </div> + <div class="attachAudio addToPlaylist" data-id="{$audio->getId()}"> + <span>{_remove_from_playlist}</span> + </div> + </div> + </div> + + <div class="showMoreAudiosPlaylist" data-page="2" data-playlist="{$playlist->getId()}" n:if="$pagesCount > 1"> + {_show_more_audios} + </div> + </div> + + <form method="post" id="editPlaylistForm" data-id="{$playlist->getId()}" enctype="multipart/form-data"> + <input type="hidden" name="title" maxlength="128" /> + <input type="hidden" name="hash" value="{$csrfToken}" /> + <textarea style="display:none;" name="description" maxlength="2048" /> + <input type="hidden" name="audios"> + <input type="file" style="display:none;" name="new_cover" accept=".jpg,.png"> + + <div style="float:right;margin-top: 8px;"> + <button class="button" type="submit">{_save}</button> + </div> + </form> + + <script> + document.querySelector("input[name='audios']").value = {$audiosIds} + + u("#editPlaylistForm").on("submit", (e) => { + document.querySelector("#editPlaylistForm input[name='title']").value = document.querySelector(".playlistInfo input[name='title']").value + document.querySelector("#editPlaylistForm textarea[name='description']").value = document.querySelector(".playlistBlock textarea[name='description']").value + }) + + u("#editPlaylistForm input[name='new_cover']").on("change", (e) => { + if(!e.currentTarget.files[0].type.startsWith("image/")) { + fastError(tr("not_a_photo")) + return + } + + let image = URL.createObjectURL(e.currentTarget.files[0]) + + document.querySelector(".playlistCover img").src = image + }) + + u(".playlistCover img").on("click", (e) => { + document.querySelector("input[name='new_cover']").click() + }) + + document.querySelector("#editPlaylistForm input[name='new_cover']").value = "" + </script> + + {script "js/al_playlists.js"} +{/block} diff --git a/Web/Presenters/templates/Audio/Embed.xml b/Web/Presenters/templates/Audio/Embed.xml new file mode 100644 index 00000000..b7540358 --- /dev/null +++ b/Web/Presenters/templates/Audio/Embed.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> + <head> + <meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" /> + <link rel="icon"> + <title>{$audio->getName()} + + {css "css/main.css"} + {css "css/audios.css"} + {script "js/node_modules/dashjs/dist/dash.all.min.js"} + {script "js/node_modules/jquery/dist/jquery.min.js"} + {script "js/node_modules/umbrellajs/umbrella.min.js"} + + + {include "player.xml", audio => $audio} + + {script "js/al_music.js"} + + diff --git a/Web/Presenters/templates/Audio/List.xml b/Web/Presenters/templates/Audio/List.xml new file mode 100644 index 00000000..ec24f3b3 --- /dev/null +++ b/Web/Presenters/templates/Audio/List.xml @@ -0,0 +1,126 @@ +{extends "../@layout.xml"} + +{block title} + {if $mode == 'list'} + {if $ownerId > 0} + {_audios} {$owner->getMorphedName("genitive", false)} + {else} + {_audios_group} + {/if} + {elseif $mode == 'new'} + {_audio_new} + {elseif $mode == 'popular'} + {_audio_popular} + {else} + {if $ownerId > 0} + {_playlists} {$owner->getMorphedName("genitive", false)} + {else} + {_playlists_group} + {/if} + {/if} +{/block} + +{block header} + + +
+ {_audios} + » + {_audio_new} +
+ +
+ {_audios} + » + {_audio_popular} +
+ +
+ {_audios} + » + {if $isMy}{_my_playlists}{else}{_playlists}{/if} +
+{/block} + +{block content} + {* ref: https://archive.li/P32em *} + + {include "bigplayer.xml"} + + + + +
+ +
+
+
+
+ {include "../components/error.xml", description => $ownerId > 0 ? ($ownerId == $thisUser->getId() ? tr("no_audios_thisuser") : tr("no_audios_user")) : tr("no_audios_club")} +
+
+
+ {include "player.xml", audio => $audio, club => $club} +
+
+ +
+ {include "../components/paginator.xml", conf => (object) [ + "page" => $page, + "count" => $audiosCount, + "amount" => sizeof($audios), + "perPage" => $perPage ?? OPENVK_DEFAULT_PER_PAGE, + "atBottom" => true, + ]} +
+
+
+ +
+
+
+ {include "../components/error.xml", description => $ownerId > 0 ? ($ownerId == $thisUser->getId() ? tr("no_playlists_thisuser") : tr("no_playlists_user")) : tr("no_playlists_club")} +
+ + + +
+ {include "../components/paginator.xml", conf => (object) [ + "page" => $page, + "count" => $playlistsCount, + "amount" => sizeof($playlists), + "perPage" => $perPage ?? OPENVK_DEFAULT_PER_PAGE, + "atBottom" => true, + ]} +
+
+
+ {include "tabs.xml"} +
+{/block} diff --git a/Web/Presenters/templates/Audio/NewPlaylist.xml b/Web/Presenters/templates/Audio/NewPlaylist.xml new file mode 100644 index 00000000..42efae7f --- /dev/null +++ b/Web/Presenters/templates/Audio/NewPlaylist.xml @@ -0,0 +1,109 @@ +{extends "../@layout.xml"} + +{block title} + {_new_playlist} +{/block} + +{block header} + {if !$_GET["gid"]} + {$thisUser->getCanonicalName()} + » + {_audios} + {else} + {$club->getCanonicalName()} + » + {_audios} + {/if} + » + {_new_playlist} +{/block} + +{block content} + + +
+
+ + {_playlist_cover} + +
+ +
+
+ +
+
+ + + + + + + + + + + + + + + + + +
+
+
+ + +{/block} diff --git a/Web/Presenters/templates/Audio/bigplayer.xml b/Web/Presenters/templates/Audio/bigplayer.xml new file mode 100644 index 00000000..7e8ffbe9 --- /dev/null +++ b/Web/Presenters/templates/Audio/bigplayer.xml @@ -0,0 +1,56 @@ +
+
diff --git a/Web/Presenters/templates/Audio/player.xml b/Web/Presenters/templates/Audio/player.xml new file mode 100644 index 00000000..fb5862b4 --- /dev/null +++ b/Web/Presenters/templates/Audio/player.xml @@ -0,0 +1,69 @@ +{php $id = $audio->getId() . rand(0, 1000)} +{php $isWithdrawn = $audio->isWithdrawn()} +{php $editable = isset($thisUser) && $audio->canBeModifiedBy($thisUser)} +
+
diff --git a/Web/Presenters/templates/Audio/tabs.xml b/Web/Presenters/templates/Audio/tabs.xml new file mode 100644 index 00000000..ed74ca86 --- /dev/null +++ b/Web/Presenters/templates/Audio/tabs.xml @@ -0,0 +1,40 @@ + diff --git a/Web/Presenters/templates/Group/Edit.xml b/Web/Presenters/templates/Group/Edit.xml index 22f0b490..8f85e7e7 100644 --- a/Web/Presenters/templates/Group/Edit.xml +++ b/Web/Presenters/templates/Group/Edit.xml @@ -102,6 +102,15 @@ + + + {_audios}: + + + + + + diff --git a/Web/Presenters/templates/Group/View.xml b/Web/Presenters/templates/Group/View.xml index 0242bbb8..de00d9b0 100644 --- a/Web/Presenters/templates/Group/View.xml +++ b/Web/Presenters/templates/Group/View.xml @@ -90,6 +90,25 @@
+ +
+
+ {_audios} +
+
+
+ {tr("audios_count", $audiosCount)} + +
+
+
+ {include "../Audio/player.xml", audio => $audio} +
+
+
+
{presenter "openvk!Wall->wallEmbedded", -$club->getId()}
diff --git a/Web/Presenters/templates/Photos/UploadPhoto.xml b/Web/Presenters/templates/Photos/UploadPhoto.xml index ab81d3aa..4f5f8e70 100644 --- a/Web/Presenters/templates/Photos/UploadPhoto.xml +++ b/Web/Presenters/templates/Photos/UploadPhoto.xml @@ -35,7 +35,7 @@
{_admin_limits} -
    +
    • {_supported_formats}
    • {_max_load_photos}
    diff --git a/Web/Presenters/templates/Report/Tabs.xml b/Web/Presenters/templates/Report/Tabs.xml index e878ccb3..6003f202 100644 --- a/Web/Presenters/templates/Report/Tabs.xml +++ b/Web/Presenters/templates/Report/Tabs.xml @@ -46,6 +46,9 @@ +
    + {_audios} +
    {if $graffiti} diff --git a/Web/di.yml b/Web/di.yml index 57d46f14..81e06117 100644 --- a/Web/di.yml +++ b/Web/di.yml @@ -7,6 +7,7 @@ services: - openvk\Web\Presenters\CommentPresenter - openvk\Web\Presenters\PhotosPresenter - openvk\Web\Presenters\VideosPresenter + - openvk\Web\Presenters\AudioPresenter - openvk\Web\Presenters\BlobPresenter - openvk\Web\Presenters\GroupPresenter - openvk\Web\Presenters\SearchPresenter @@ -33,6 +34,7 @@ services: - openvk\Web\Models\Repositories\Albums - openvk\Web\Models\Repositories\Clubs - openvk\Web\Models\Repositories\Videos + - openvk\Web\Models\Repositories\Audios - openvk\Web\Models\Repositories\Notes - openvk\Web\Models\Repositories\Tickets - openvk\Web\Models\Repositories\Messages diff --git a/Web/routes.yml b/Web/routes.yml index dea8ddd5..d12ccbc0 100644 --- a/Web/routes.yml +++ b/Web/routes.yml @@ -187,6 +187,34 @@ routes: handler: "Videos->edit" - url: "/video{num}_{num}/remove" handler: "Videos->remove" + - url: "/player/upload" + handler: "Audio->upload" + - url: "/audios{num}" + handler: "Audio->list" + - url: "/audios/popular" + handler: "Audio->popular" + - url: "/audios/new" + handler: "Audio->new" + - url: "/audio{num}_{num}/embed.xhtml" + handler: "Audio->embed" + - url: "/audio{num}/listen" + handler: "Audio->listen" + - url: "/audios/search" + handler: "Audio->search" + - url: "/audios/newPlaylist" + handler: "Audio->newPlaylist" + - url: "/audios/context" + handler: "Audio->apiGetContext" + - url: "/playlist{num}_{num}" + handler: "Audio->playlist" + - url: "/playlist{num}_{num}/edit" + handler: "Audio->editPlaylist" + - url: "/playlist{num}/action" + handler: "Audio->playlistAction" + - url: "/playlists{num}" + handler: "Audio->playlists" + - url: "/audio{num}/action" + handler: "Audio->action" - url: "/{?!club}{num}" handler: "Group->view" placeholders: @@ -221,24 +249,6 @@ routes: handler: "Topics->edit" - url: "/topic{num}_{num}/delete" handler: "Topics->delete" - - url: "/audios{num}" - handler: "Audios->app" - - url: "/audios{num}.json" - handler: "Audios->apiListSongs" - - url: "/audios/popular.json" - handler: "Audios->apiListPopSongs" - - url: "/audios/playlist{num}.json" - handler: "Audios->apiListPlaylists" - - url: "/audios/search.json" - handler: "Audios->apiSearch" - - url: "/audios/add.json" - handler: "Audios->apiAdd" - - url: "/audios/playlist.json" - handler: "Audios->apiAddPlaylist" - - url: "/audios/upload.json" - handler: "Audios->apiUpload" - - url: "/audios/beacon" - handler: "Audios->apiBeacon" - url: "/im" handler: "Messenger->index" - url: "/im/sel{num}" @@ -341,6 +351,12 @@ routes: handler: "Admin->bannedLink" - url: "/admin/bannedLink/id{num}/unban" handler: "Admin->unbanLink" + - url: "/admin/music" + handler: "Admin->music" + - url: "/admin/music/{num}/edit" + handler: "Admin->editMusic" + - url: "/admin/playlist/{num}/edit" + handler: "Admin->editPlaylist" - url: "/admin/user{num}/bans" handler: "Admin->bansHistory" - url: "/upload/photo/{text}" diff --git a/Web/static/css/audios.css b/Web/static/css/audios.css new file mode 100644 index 00000000..5decbb89 --- /dev/null +++ b/Web/static/css/audios.css @@ -0,0 +1,661 @@ +.noOverflow { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.overflowedName { + position: absolute; + z-index: 99; +} + +.musicIcon { + background-image: url('/assets/packages/static/openvk/img/audios_controls.png'); + background-repeat: no-repeat; + cursor: pointer; +} + +.musicIcon:hover { + filter: brightness(99%); +} + +.musicIcon.pressed { + filter: brightness(150%); +} + +.bigPlayer { + background-color: rgb(240, 241, 242); + margin-left: -10px; + margin-top: -10px; + width: 102.8%; + height: 46px; + border-bottom: 1px solid #d8d8d8; + box-shadow: 1px 0px 8px 0px rgba(34, 60, 80, 0.2); + position: relative; +} + +.bigPlayer.floating { + position: fixed; + z-index: 199; + width: 627px; + margin-top: -76px; +} + +.bigPlayer .paddingLayer { + padding: 0px 0px 0px 14px; +} + +.bigPlayer .paddingLayer .playButtons { + padding: 12px 0px; +} + +.bigPlayer .paddingLayer .playButtons .playButton { + width: 22px; + height: 22px; + float: left; + background-position-x: -72px; +} + +.bigPlayer .paddingLayer .playButtons .playButton.pause { + background-position-x: -168px; +} + +.bigPlayer .paddingLayer .playButtons .nextButton { + width: 16px; + height: 16px; + background-position-y: -47px; +} + +.bigPlayer .paddingLayer .playButtons .backButton { + width: 16px; + height: 16px; + background-position-y: -47px; + background-position-x: -16px; + margin-left: 6px; +} + +.bigPlayer .paddingLayer .additionalButtons { + float: left; + margin-top: -6px; + width: 11%; +} + +.bigPlayer .paddingLayer .additionalButtons .repeatButton { + width: 14px; + height: 16px; + background-position-y: -49px; + background-position-x: -31px; + margin-left: 7px; + float: left; +} + +.broadcastButton { + width: 16px; + height: 12px; + background-position-y: -50px; + background-position-x: -64px; + margin-left: 6px; + float: left; +} + +.broadcastButton.atProfile { + width: 13px; + height: 12px; + background-position-y: -50px; + background-position-x: -64px; + margin-left: 0px !important; + margin-right: 5px; + float: left; +} + +.bigPlayer .paddingLayer .additionalButtons .shuffleButton { + width: 14px; + height: 16px; + background-position: -50px -50px; + margin-left: 7px; + float: left; +} + +.bigPlayer .paddingLayer .additionalButtons .deviceButton { + width: 12px; + height: 16px; + background-position: -202px -50px; + margin-left: 7px; + float: left; +} + +.bigPlayer .paddingLayer .playButtons .arrowsButtons { + float: left; + display: flex; + padding-left: 4px; + padding-top: 1.2px; +} + +.bigPlayer .paddingLayer .trackPanel { + float: left; + margin-top: -13px; + margin-left: 13px; + width: 63%; + position: relative; +} + +.bigPlayer .paddingLayer .bigPlayerTip { + display: none; + z-index: 999; + background: #cecece; + padding: 3px; + top: -3px; + position: absolute; + transition: all .1s ease-out; + user-select: none; +} + +.bigPlayer .paddingLayer .volumePanel { + width: 73px; + float: left; +} + +.bigPlayer .paddingLayer .slider, .audioEmbed .track .slider { + width: 18px; + height: 7px; + background: #606060; + position: absolute; + bottom: 0; + top: 0px; + pointer-events: none; +} + +.bigPlayer .paddingLayer .trackInfo .timer { + float: right; + margin-right: 8px; + font-size: 10px; +} + +.bigPlayer .paddingLayer .trackInfo .trackName { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 81%; + display: inline-block; +} + +.bigPlayer .paddingLayer .trackInfo .timer span { + font-size: 10px; +} + +.bigPlayer .paddingLayer .trackInfo b:hover { + text-decoration: underline; + cursor: pointer; +} + +.audioEmbed .track > .selectableTrack, .bigPlayer .selectableTrack { + margin-top: 3px; + width: calc(100% - 8px); + border-top: #606060 1px solid; + height: 6px; + position: relative; + user-select: none; +} + +#audioEmbed { + cursor: pointer; + user-select: none; + background: #eee; + height: 40px; + width: 486px; + position: absolute; + top: 50%; + left: 50%; + margin-right: -50%; + transform: translate(-50%, -50%); + overflow: hidden; + border: 1px solid #8B8B8B; +} + +.audioEntry.nowPlaying { + background: #606060; + border: 1px solid #4f4f4f; + box-sizing: border-box; +} + +.audioEntry.nowPlaying .playIcon { + background-position-y: -16px !important; +} + +.audioEntry.nowPlaying:hover { + background: #4e4e4e !important; +} + +.audioEntry.nowPlaying .performer a { + color: #f4f4f4 !important; +} + +.audioEntry .performer a { + color: #4C4C4C; +} + +.audioEntry.nowPlaying .title { + color: #fff; +} + +.audioEntry.nowPlaying .status { + color: white; +} + +.audioEntry.nowPlaying .volume .nobold { + color: white !important; +} + +.audioEntry.nowPlaying .buttons .musicIcon, .audioEntry.nowPlaying .explicitMark { + filter: brightness(187%) opacity(72%); +} + +.audioEntry { + height: 100%; + position: relative; + width: 100%; +} + +.audioEntry .playerButton { + position: relative; + padding: 10px 9px 9px 9px; + width: 16px; + height: 16px; +} + +.audioEntry .subTracks { + display: flex; + padding-bottom: 5px; + padding-left: 8px; + padding-right: 12px; +} + +.audioEntry .playerButton .playIcon { + background-image: url('/assets/packages/static/openvk/img/play_buttons.gif'); + cursor: pointer; + width: 16px; + height: 16px; + background-repeat: no-repeat; +} + +.audioEntry .playerButton .playIcon.paused { + background-position-y: -16px; +} + +.audioEntry .status { + overflow: hidden; + display: grid; + grid-template-columns: 1fr; + width: 85%; + height: 23px; +} + +.audioEntry .status strong { + color: #4C4C4C; +} + +.audioEmbed .track { + display: none; + padding: 4px 0; +} + +.audioEmbed .track, .audioEmbed.playing .track { + display: unset; +} + +.audioEntry:hover { + background: #EEF0F2; + border-radius: 2px; +} + +.audioEntry:hover .buttons { + display: block; +} + +.audioEntry:hover .volume .hideOnHover { + display: none; +} + +.audioEntry .buttons { + display: none; + width: 62px; + height: 20px; + position: absolute; + right: 3%; + top: 2px; + /* чтоб избежать заедания во время ховера кнопки добавления */ + clip-path: inset(0 0 0 0); +} + +.audioEntry .buttons .edit-icon { + width: 11px; + height: 11px; + float: right; + margin-right: 4px; + margin-top: 3px; + background-position: -137px -51px; +} + +.audioEntry .buttons .add-icon { + width: 11px; + height: 11px; + float: right; + background-position: -80px -52px; + margin-top: 3px; + margin-left: 2px; +} + +.audioEntry .buttons .add-icon-group { + width: 14px; + height: 11px; + float: right; + background-position: -94px -52px; + margin-top: 3px; + transition: margin-right 0.1s ease-out, opacity 0.1s ease-out; +} + +.audioEntry .buttons .report-icon { + width: 12px; + height: 11px; + float: right; + background-position: -67px -51px; + margin-top: 3px; + margin-right: 3px; +} + +.add-icon-noaction { + background-image: url('/assets/packages/static/openvk/img/audios_controls.png'); + width: 11px; + height: 11px; + float: right; + background-position: -94px -52px; + margin-top: 2px; + margin-right: 2px; +} + +.audioEntry .buttons .remove-icon { + margin-top: 3px; + width: 11px; + height: 11px; + margin-left: 2px; + float: right; + background-position: -108px -52px; +} + +.audioEntry .buttons .remove-icon-group { + margin-top: 3px; + width: 13px; + height: 11px; + float: right; + background-position: -122px -52px; + margin-left: 3px; + margin-right: 3px; +} + +.audioEmbed .lyrics { + display: none; + padding: 6px 33px 10px 33px; +} + +.audioEmbed .lyrics.showed { + display: block !important; +} + +.audioEntry .withLyrics { + user-select: none; + color: #507597; +} + +.audioEntry .withLyrics:hover { + text-decoration: underline; +} + +.audioEmbed.withdrawn .status > *, .audioEmbed.processed .status > *, .audioEmbed.withdrawn .playerButton > *, .audioEmbed.processed .playerButton > * { + pointer-events: none; +} + +.audioEmbed.withdrawn { + filter: opacity(0.8); +} + +.playlistCover img { + max-width: 135px; + max-height: 135px; +} + +.playlistBlock { + margin-top: 14px; +} + +.playlistContainer { + display: grid; + grid-template-columns: repeat(3, 146px); + gap: 18px 10px; +} + +.playlistContainer .playlistCover { + width: 111px; + height: 111px; + display: flex; + background: #c4c4c4; +} + +.playlistContainer .playlistCover img { + max-width: 111px; + max-height: 111px; + margin: auto; +} + +.ovk-diag-body .searchBox { + background: #e6e6e6; + padding-top: 10px; + height: 35px; + padding-left: 10px; + padding-right: 10px; + display: flex; +} + +.ovk-diag-body .searchBox input { + height: 24px; + margin-right: -1px; + width: 77%; +} + +.ovk-diag-body .searchBox select { + width: 29%; + padding-left: 8px; + height: 24px; +} + +.ovk-diag-body .audiosInsert { + height: 82%; + padding: 9px 5px 9px 9px; + overflow-y: auto; +} + +.attachAudio { + float: left; + width: 28%; + height: 26px; + padding-top: 11px; + text-align: center; +} + +.attachAudio span { + user-select: none; +} + +.attachAudio:hover { + background: rgb(236, 236, 236); + cursor: pointer; +} + +.playlistCover img { + cursor: pointer; +} + +.explicitMark { + margin-top: 2px; + margin-left: 3px; + width: 11px; + height: 11px; + background: url('/assets/packages/static/openvk/img/explicit.svg'); + background-repeat: no-repeat; +} + +.audioStatus span { + color: #2B587A; +} + +.audioStatus span:hover { + text-decoration: underline; + cursor: pointer; +} + +.audioStatus { + padding-top: 2px; + padding-bottom: 3px; +} + +/*
    🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣*/ +.audiosDiv center span { + color: #707070; + margin: 120px 0px !important; + display: block; +} + +.audiosDiv center { + margin-left: -10px; +} + +.playlistInfo { + display: flex; + flex-direction: column; +} + +.playlistInfo .playlistName { + font-weight: 600; +} + +.searchList.floating { + position: fixed; + z-index: 199; + width: 156px; + margin-top: -65px !important; +} + +.audiosSearchBox input[type='search'] { + height: 25px; + width: 77%; + padding-left: 21px; + padding-top: 4px; + background: rgb(255, 255, 255) url("/assets/packages/static/openvk/img/search_icon.png") 5px 6px no-repeat; +} + +.audiosSearchBox { + padding-bottom: 10px; + padding-top: 7px; + display: flex; +} + +.audiosSearchBox select { + width: 30%; + padding-left: 7px; + margin-left: -2px; +} + +.audioStatus { + color: #2B587A; + margin-top: -3px; +} + +.audioStatus::before { + background-image: url('/assets/packages/static/openvk/img/audios_controls.png'); + background-repeat: no-repeat; + width: 11px; + height: 11px; + background-position: -66px -51px; + margin-top: 1px; + display: inline-block; + vertical-align: bottom; + content: ""; + padding-right: 2px; +} + +.friendsAudiosList { + margin-left: -7px; + margin-top: 8px; +} + +.friendsAudiosList .elem { + display: flex; + padding: 1px 1px; + width: 148px; +} + +.friendsAudiosList .elem img { + width: 30px; + border-radius: 2px; + object-fit: cover; + height: 31px; + min-width: 30px; +} + +.friendsAudiosList .elem .additionalInfo { + margin-left: 7px; + padding-top: 1px; + width: 100%; + display: flex; + flex-direction: column; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.friendsAudiosList .elem .additionalInfo .name { + color: #2B587A; +} + +.friendsAudiosList #used .elem .additionalInfo .name { + color: #F4F4F4; +} + +.friendsAudiosList .elem .additionalInfo .desc { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + color: #878A8F; + font-size: 11px; +} + +.friendsAudiosList #used .elem .additionalInfo .desc { + color: #F4F4F4; +} + +.friendsAudiosList .elem:hover { + background: #E8EBF0; + cursor: pointer; +} + +.friendsAudiosList #used .elem:hover { + background: #787878; + cursor: pointer; +} + +.editContainer { + display:table; + clear:both; + width:100%; + margin-top: 10px; +} + +.editContainer .playerContainer { + width: 78%; + float: left; + max-width: 78%; + min-width: 68%; +} + +.addToPlaylist { + width: 22%; +} diff --git a/Web/static/css/main.css b/Web/static/css/main.css index 7b63d6f6..7a087705 100644 --- a/Web/static/css/main.css +++ b/Web/static/css/main.css @@ -1243,64 +1243,6 @@ table.User { border-bottom: 1px solid #c3cad2; } -.music-app { - display: grid; -} - -.music-app--player { - display: grid; - grid-template-columns: 32px 32px 32px 1fr; - padding: 8px; - border-bottom: 1px solid #c1c1c1; - border-bottom-style: solid; - border-bottom-style: dashed; -} - -.music-app--player .play, -.music-app--player .perv, -.music-app--player .next { - -webkit-appearance: none; - -moz-appearance: none; - background-color: #507597; - color: #fff; - height: 20px; - margin: 5px; - border: none; - border-radius: 5px; - cursor: pointer; - padding: 0; - font-size: 10px; -} - -.music-app--player .info { - margin-left: 10px; - width: 550px; -} - -.music-app--player .info .song-name * { - color: black; -} - -.music-app--player .info .song-name time { - float: right; -} - -.music-app--player .info .track { - margin-top: 8px; - height: 5px; - width: 70%; - background-color: #fff; - border-top: 1px solid #507597; - float: left; -} - -.music-app--player .info .track .inner-track { - background-color: #507597; - height: inherit; - width: 15px; - opacity: .7; -} - .settings_delete { margin: -12px; padding: 12px; @@ -1477,7 +1419,7 @@ body.scrolled .toTop:hover { display: none; } -.post-has-videos { +.post-has-videos, .post-has-audios { margin-top: 11px; margin-left: 3px; color: #3c3c3c; @@ -1494,7 +1436,7 @@ body.scrolled .toTop:hover { margin-left: 2px; } -.post-has-video { +.post-has-video, .post-has-audio { padding-bottom: 4px; cursor: pointer; } @@ -1503,6 +1445,10 @@ body.scrolled .toTop:hover { text-decoration: underline; } +.post-has-audio:hover span { + text-decoration: underline; +} + .post-has-video::before { content: " "; width: 14px; @@ -1516,6 +1462,19 @@ body.scrolled .toTop:hover { margin-bottom: -1px; } +.post-has-audio::before { + content: " "; + width: 14px; + height: 15px; + display: inline-block; + vertical-align: bottom; + background-image: url("/assets/packages/static/openvk/img/audio.png"); + background-repeat: no-repeat; + margin: 3px; + margin-left: 2px; + margin-bottom: -1px; +} + .post-opts { margin-top: 10px; } @@ -2123,6 +2082,45 @@ table td[width="120"] { margin: 0 auto; } +#upload_container { + background: white; + padding: 30px 80px 20px; + margin: 10px 25px 30px; + border: 1px solid #d6d6d6; +} + +#upload_container h4 { + border-bottom: solid 1px #daE1E8; + text-align: left; + padding: 0 0 4px 0; + margin: 0; +} + +#audio_upload { + width: 350px; + margin: 20px auto; + margin-bottom: 10px; + padding: 15px 0; + border: 2px solid #ccc; + background-color: #EFEFEF; + text-align: center; +} + +ul { + list-style: url(/assets/packages/static/openvk/img/bullet.gif) outside; + margin: 10px 0; + padding-left: 30px; + color: black; +} + +li { + padding: 1px 0; +} + +#upload_container ul { + padding-left: 15px; +} + #votesBalance { margin-top: 10px; padding: 7px; @@ -2473,8 +2471,7 @@ a.poll-retract-vote { display: none; } -.searchOptions -{ +.searchOptions { overflow: hidden; width:25.5%; border-top:1px solid #E5E7E6; @@ -2485,8 +2482,7 @@ a.poll-retract-vote { margin-right: -7px; } -.searchBtn -{ +.searchBtn { border: solid 1px #575757; background-color: #696969; color:white; @@ -2498,52 +2494,47 @@ a.poll-retract-vote { margin-top: 1px; } -.searchBtn:active -{ +.searchBtn:active { border: solid 1px #666666; background-color: #696969; color:white; box-shadow: 0px -2px 0px 0px rgba(255, 255, 255, 0.18) inset; } -.searchList -{ +.searchList { list-style: none; user-select: none; padding-left:0px; } -.searchList #used -{ +.searchList #used { margin-left:0px; - color: white; + color: white !important; padding:2px; padding-top:5px; padding-bottom:5px; - border: solid 0.125rem #696969; - background:linear-gradient(#888888,#858585); + border: solid 0.125rem #4F4F4F; + background: #606060; margin-bottom:2px; padding-left:9px; width:87%; } -.searchList #used a -{ +.searchList #used a { color: white; } -.sr:focus -{ +.sr:focus { outline:none; } -.searchHide -{ +.searchHide { padding-right: 5px; } -.searchList li +.searchList li, .searchList a { + display: block; margin-left:0px; color: #2B587A !important; cursor:pointer; @@ -2554,26 +2545,27 @@ a.poll-retract-vote { padding-left:9px; } -.searchList li a -{ +.searchList li a { min-width:100%; } -.searchList li:hover -{ - margin-left:0px; - color: #2B587A !important; - background:#ebebeb; - padding:2px; - padding-top:5px; - padding-bottom:5px; - margin-bottom:2px; - padding-left:9px; - width:91%; +.searchList a { + min-width: 88%; } -.whatFind -{ +.searchList a:hover { + margin-left: 0px; + color: #2B587A !important; + background: #ebebeb; + padding: 2px; + padding-top: 5px; + padding-bottom: 5px; + margin-bottom: 2px; + padding-left: 9px; + width: 89.9%; +} + +.whatFind { color:rgb(128, 128, 128); background:none; border:none; @@ -2585,8 +2577,7 @@ a.poll-retract-vote { margin-top: 0.5px; } -.searchOptionName -{ +.searchOptionName { cursor:pointer; background-color: #EAEAEA; padding-left:5px; @@ -2598,8 +2589,7 @@ a.poll-retract-vote { border-bottom: 2px solid #E4E4E4; } -.searchOption -{ +.searchOption { user-select: none; } @@ -2876,7 +2866,7 @@ body.article .floating_sidebar, body.article .page_content { .lagged { filter: opacity(0.5); - cursor: progress; + cursor: not-allowed; user-select: none; } @@ -2890,7 +2880,7 @@ body.article .floating_sidebar, body.article .page_content { pointer-events: none; } -.lagged * { +.lagged *, .lagged { pointer-events: none; } @@ -2975,3 +2965,40 @@ body.article .floating_sidebar, body.article .page_content { background: #E9F0F1 !important; } +.searchOptions.newer { + padding-left: 6px; + border-top: unset !important; + height: unset !important; + border-left: 1px solid #d8d8d8; + width: 26% !important; +} + +hr { + background-color: #d8d8d8; + border: none; + height: 1px; +} + +.searchList hr { + width: 153px; + margin-left: 0px; + margin-top: 6px; +} + +.showMore, .showMoreAudiosPlaylist { + width: 100%; + text-align: center; + background: #d5d5d5; + height: 22px; + padding-top: 9px; + cursor: pointer; +} + +#upload_container.uploading { + background: white url('/assets/packages/static/openvk/img/progressbar.gif') !important; + background-position-x: 0% !important; + background-position-y: 0% !important; + background-repeat: repeat !important; + background-repeat: no-repeat !important; + background-position: 50% !important; +} diff --git a/Web/static/img/audio.png b/Web/static/img/audio.png new file mode 100644 index 0000000000000000000000000000000000000000..62d9dff203a8f34d908941223522ab2ed648faa9 GIT binary patch literal 560 zcmV-00?+-4P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGf6951U69E94oEQKA0mVr~K~y+Tol?DO z0s#z8)vBQ4sVFE^hr`KN5F9#`zJ#tmKo?QF6h{TQ!QH`EXzAeQAR;&w#Kj*R{6X2ykgK;Q|yTrTNwIM8G=q4W7nolXa7qf914 zCQ%p|kH@rHt-wR(;d;FmyId|qv)Mf46qvwbu%KG4QX~?g!C)Ya31BQbI2;brb^Xyu zz3-UM=V>;Z(P%UR>zmC+x7lnu7bVK!uIC#ImP2~2*k44ELAEMNn(E1 zH0^o2-4YCt5+08Sltf2t)$8@*U6P8$VvzLz?CGJ>KRkg(5C&6em zn&YjOLZOfv4u@6vZ!rq9{J#bUI0pkN&~S^7Z?DYPDMAbUMWV8I{MgPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!2kdb!2!6DYwZ944i8C0K~#8N?VWv$ z7DpY&_c^Wyr9Iw6W3^C}Rx~6W{vqL7 zXj*$Hw%C%0@vR!8#uRHBBSu32ArNbAwW)-=Qo_q|$K&@m&&)DA&&=-3&hGQ@+|4KX z-u`xW=DYjM?fjm3cAuBZqD6~3qA2MF&pta$1!ei~-Me>8_;+=6 zh5o-p!~FwML`h)lRYtnLd4bA^wAdgV7srypiy4Z*T9p;)*MxN~OZ;<)vD! zMi(wzh(<<6Vt*uo(Eprf^nW0V=)blcCt)-?I?6i8N8~|X;4Acpv;3;7t}4u*ygCr+Fgb#!z@bRznSygY3ryv%l#HVMGT--br}>-tb7t6Wwi zk3Zlz6KM4r040Dj2{V8|E5c?1x@;^YxcA_2nnOjceLCr+G5_+NSDmEIqd70}0J z&z?OS`!8FzEcCxZBl|zlfV%@_y#A6sTiw5GTD^bD_7CK^Fla-gJ6SiS^id-T{a>#@ zW1%E4SY*eO7kTnk7Pq=T4-2Ew$KUGyJS>bdJX@Kz2@@vlrf+pA;vY#Keb~#(5I(ff9&s{{@;4B>|dh75?K6NDgeug+X45Kg9fz1WNJ8$<6#R$V)wbB!N== zksO#m25M>j6OzE#fZi;$1zHXHo*=|@h`heV)@lU*ON>STK#N03V0kSpl*Erm@MVz% z|8yGZA82tX2^8@Rp(1+*|9Zxff1t%F{j&psK;ZI*E)3GWQzPQ)zw1?Sv}5;l`}+D) z{l(R7*SiF0$Nbl=TbJrDE?l|Z3PU^QkH@X`zvrHNqFJ+MvH0beUyh!9@<|q(%2l%Z z=kB}jj;^`p8Wz9u$}7>+Pe09KQ@Ki3|IkObueMjy+puAS&#{qD-7&zE9fNM0^-RF`R$vxo-|8y* zWEBIQu!&s}1D&vmT@eGFu!%jO6I+{zEcqQIJSY~J%5CvECV3DM@_O1fD?~Ez!0Gs$ zUJisr5bz-HiTz2wBy3_AdAem*X)pd~X?M zkt+#=1Q75bBmf7>P5^{+5(o((;6X?L4m2VG5Kc)TB!GYic~9tH^Ce*uyD;t(0AZa3 zLIOxaZwVII#IDd=f(16QEA*CNflcfIoy|Ruqr` zoQ^N^vjchAX!+Rz519Yp;9x2Kn=?K;Pyoz-^5n^-_^-A6Y(-wEe<-nrTdl26?z;nl zKwxY@N#JBHEEL%@_}4R*`~xixB?0=*xT5(UUlPH;p0VT~XmKbB^g`Pp`VvWB$b-DT z!PaU7|4WQT|3HfqzH;32!j|t{lkk<}fo4;opRa0V9(dpZ9_6FIzn@2e!1$yklEA7} ztE!JY@(8=-6$k{1rDbjrQX=4CAP^`J@0kDwCy(60vpcoF1K|_U+gV`-zB2;fSq0j^ ziuUj1tMu+J9@W^iX%mmqw~Zz6I4MEz$K(5Ot;7k|7Ru8$h93j_zk2p_?4tw!|4CTT z?=CEb-cM!V0Q4j137i|}Sji<)>qBb&hLf5EXu!}?9W-(|VpaxN8BjaWR4$Uh350K{ zm1H9yi?nYS630WwU<>y5Di-gMk3k*i;4jd8sP4p0OAx@Soq$eyl-KpCsa%k^oe65k zL&7x?B-%o8Tln(G3fZTEW8k4P2_L?U!T+DW+4UFDJm@#jpCRIA=u^<&p*wJX(0M{w z)rUm=reJ(IfCnshNQ|jm!nEB~E)u{38eBT@?RCjWg1lu>r+x$yd>0bi1ah2-NC369 zRA?Z}OIxhliGimKKx4TeZ#xs3`Yw;zr>%_YIW^boh1OvW1zwM6NuV$ zL2IB?;QIo3n}jMh37~Xr2e9qBSZ>*}r9x8cnb6g9m`UjNVcS-wkv=q?{}A*woQp58 z5($3(eifoROQ5(uR97FU^_xar65uC%9j5XWO!ZxTY!U!%m$e7l2~mCe0sJB8gAnuK z$T9IzzHAy|Al`W6jX#BcNo82lkATLQrt&_tUi!_L5-t?%j!fsx&wO znEvNl9~=h{>u~bnOPS9@uR(j+nw}p+uS0i2uJxNnA_)|j00b@_UnF0G=v{4;3<#PD z(CaUO?FnlGy_ToPBP1i2Q$3kPI~s@s0|V8^AAh_G?u0%&d-m+==;-MGFes-`13}h+ zGi_JP<$ZQ`c1BPY`#OqE$JKobnv?+4cFpjisrw-$%8Rwtdib_O5Y=V%A!B_Y*KZn^ z#VtaMXL?u8L-g9~i_jmSA3$0Bi7#ETeJ!ijt7}IWX*)VPqM@N7CW=Q!MkXv>y7UyU z5@S2iRn+ZbpOs35`e+w48+uK*&nC|R#Co%Q=r#V?TA0cii0%lu)^8e#Bp?Q(imBWJ zQ@gm_HUVJ!tvs}$2l{3$j6MPV6Z!yKt5LTv9LKG6Eb?O&>%8ngwbDB1s!X`=t?^XASb~Lm4 zD>TXqfI|YPYYVd4>bC11TcdpF|8*PA!{bohlM2cPpkoN>A|TxbVD+KnknTxn0Fvvc zvTxtMs*QN{7*n}S-Aq6p1Tv=b6ioH&%F%i8K~k&}Hi-*|y07b@?XtE)1np}FMANOm zL9|UE$C=>q(|ZGSiGXfv(SC$vC>lwAJ5QqoQY>;+kvj4 zmdpE;5Ov~xqTP!a@ZU!GIfz~4a!3F#LW#D(wry6uM*7eXc}|7i4Sk#n6c$16fjXh* zAlLfQ{{8!pn6zNQg3M!7>&SyYEfHv64iu0GO!aKroA@t8S6gkMd$B|}L~qLQCM6N% z2G7WBxq9{L%JA@TG;P{6x)`{VgyI#@aZJs>z$yXV0M`vjwVtkBcXxNZ&#haxR!M+n z1Mxo5j{5OtEMLcd=&}FSApx+q5MBIYZ6Vilpr~`n*T?18Wv_)?>qqnF&!5ZI6hwJi zB1lUB0#hO|UK8+01g7oAcCshMDWW&(=)P2v+N+YMSad5!lndlog{H1ru>bhof<+*n z^&foj!D@SZdqjg0(BLQi19}^#<};~~PzRt`HtYXGTWYQF#I(WB9^W5=i+z02%Z2M->6 zAC|N5tW58!jL91KYPmq&4qy6)plesITv<7N`ZOhg=TN5Zb1}3Xx&`fSVI}s&ZMl39 zk4gL39LVK^^MR)K^!8txjsAKTl7h*9tBM FYXF1T4W9r2 literal 0 HcmV?d00001 diff --git a/Web/static/img/explicit.svg b/Web/static/img/explicit.svg new file mode 100644 index 00000000..df6f6c15 --- /dev/null +++ b/Web/static/img/explicit.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/Web/static/img/favicons/favicon24_paused.png b/Web/static/img/favicons/favicon24_paused.png new file mode 100644 index 0000000000000000000000000000000000000000..c8fdef269222dd0fff08c6177bd5f2d3a3a26a05 GIT binary patch literal 932 zcmV;V16%xwP)004&&004{<008|>004nL003F*009yY002DZ000@zy2&Ck0003Y zX+uL$Nkc;*P;zf(X>4Tx04UF6U|=$Eba8TJ5@2A+%_}Jia(7aQh>TKTKhMC%z{~&! ziOIzUjsXEaAa-7UUMd3y_;!tf5kz0s1)0S_8sJJUC@KNce}LF1CAB!2fq~Hg$j&Y= zDFBKy&H=JTlCr_<4Ip+=NRTs-eFVtnONX$pfY?b8_7f1h2CFd6v zfq~@@Ld>L>fnk9T0|Wn5gqWHk1A~A*1H-;K0Kmpi{cn=x1ONa432;bRa{vGi!~g&e z!~vBn4jTXf0qsddK~zXftyViu12Gh|&1PLiBv$~gKxhyR*FZF>QjwmNe7Yzchz8LB zN8k=1NEPA_GxjDJgCZGrHsiHD+4ub3^I9vX)2W!x=Z95QT?!!%Kn-q&2W9O3f(7_s zj5+UiyZ3T3nH(;c%Uev=L2d!`uY~xhI-O&sYklRMt3lBg(Fp5&t+l>Z&bxv49$wqK z9PRnwY+pI&wYL~p2no&77IXbPBY^`fTr2M&L~q~t7p$K3ykpT8wkw7cLc*2fW;Sbj zLY$4qO)p}TCW9%a$qU8C!W$#WnW99q%!oxFc9liPagrfrOi`lIrscwsDirxE?>SSH zXp91z=gR>*EK1H4B^r%0D|8eiuqISIXNnRnq=+?uBh;XYz;nBPXNnSSl`6c=+(k`D z<|k8>Xffq6nz)OaLCzE>}2IUNfdB(bg$~UB!@C z1A5DuqC|@+#~lTi!{N}sy}kc>7X1~Hj-(X*-u5a*EHuMvhfERAp^zekv|uPaV%uUo zp!X3S*t)0EyXKUPvkZ!9<;r{^#z=tvRRE|a?@%r*|)ale0@Hr4t z>i9*1KZPGgoZ!k(xG)o?^_L6#97zaJ7t9JiTPD3;|LzkHSXjYrQUNId0000004&&004{<008|>004nL003F*009yY002DZ000@zy2&Ck0003Y zX+uL$Nkc;*P;zf(X>4Tx04UF6U|=$Eba8TJ5@2A+%_}Jia(7aQh>TKTKhMC%z{~&! ziOIzUjsXEaAa-7UUMd3y_;!tf5kz0s1)0S_8sJJUC@KNce}LF1CAB!2fq~Hg$j&Y= zDFBKy&H=JTlCr_<4Ip+=NRTs-eFVtnONX$pfY?b8_7f1h2CFd6v zfq~@@Ld>L>fnk9T0|Wn5gqWHk1A~A*1H-;K0Kmpi{cn=x1ONa432;bRa{vGi!~g&e z!~vBn4jTXf0)k0IK~zXftyaBm8bJ`AJ-E9H1(0}wB#k4PE>2^miJSt&8$?ko3Pg*9 zWDbR$nmanduaS`5mI0tn0pT ze9!-+l=1*d3wV%J+Vg$?v!k>+(prNxy`N%x+K@eUl+wPI=txYY1@;64*tqX#B~54D z#rs0@8=ID}jSzv+0D~5sp1z@cA#a`A3xyCd5$rnrkYHm^Y$%t@)Hyq&mnSEbafDT8 z85hOKoD>^u!eQ(6dQ_=Y5Vp+jIvD4uFj4G*j7-LD*e9;*Qms~_^Y`a; zSU%ib$06bzfCxEAJkWexckw?FLO^e8J=-&pF{dbTDY9dqpnlP0GQkP!(8bS-jWuz| zV{hV9lyq#c$!s>miR#d3G@|S9dS{I`_F@uq4>%#WZf#>-7K;UOdGGi8G@s9{`iwZ_ z$we~*oCjvLfQ7>`9*?QjYEcjbR=o`{fobzaGrQ;NXUB9prFOeb!{Ly^FtqAyBMv#< zNVuri>-y^Ik14ZhiC`j}u0sC7ZHPG0>{dG95OEzcA%aUGyJD(eN{Fj7Y{IsKX8QM&M(aoB;p0cYWTak^P?;oz_ZJXAZlXxO7F->FUtT_UUGD=pk8t<>_=^Ne=8ql^ z@XnApGZIhTeYtR(djb=f<)aqhv!$EQ7ry=l-ND0C8%{=U00000NkvXXu0mjfv3k|u literal 0 HcmV?d00001 diff --git a/Web/static/img/play_buttons.gif b/Web/static/img/play_buttons.gif new file mode 100644 index 0000000000000000000000000000000000000000..b5a8078d03e13d2741b942f32fcb211317aa93d0 GIT binary patch literal 103 zcmZ?wbhEHb6kt$bC}3bv{K>+|z`(?y!vF*zvBrdig#Z8lgSbF(3z5E=%ii)o=5|zy z(mhxBd8g+>k4T5iWvfm`IB6ezy(X#s?SqQNi*0rVf3fRS)DYHTs@<|oTUCRB!5RSI CaUwJT literal 0 HcmV?d00001 diff --git a/Web/static/img/progressbar.gif b/Web/static/img/progressbar.gif new file mode 100644 index 0000000000000000000000000000000000000000..09d6437c6fa4670ea38a1bd904994acf7266c9d4 GIT binary patch literal 1018 zcmZ?wbhEHbjAP(u_{hjGYu2ozM~^;x_U!-v|BC;){aizWogD*Qjr0td85tND6o0a? zaxpM6=m6OaAT10`wLSeSPrv0~JZHu83{Y=afQ7EUlwJO9#Hpqo=xWQKLx|U#5nFxs z%cg6(&CRb1|NZM*-u}!y<2w5bwH0nVGJc4+vmY?@jLfX;oVjvmB+Nxj3_30h%nafT zoIn@JxAZSM_?DwlXkFWkX}LenoGqQi)}_oE_h|a8oSl=R8KJ&lX3#MQ>QMpfF=^>v zeDJMR%N&tNhPiwbstQ$4Un{vUA}4urhWd;o<=68z+H6?+$?Ln<z>% literal 0 HcmV?d00001 diff --git a/Web/static/img/song.jpg b/Web/static/img/song.jpg new file mode 100644 index 0000000000000000000000000000000000000000..21e5a3a39ea15a318629c5d31996cc08d1791164 GIT binary patch literal 2481 zcmds%dpJ~S9LL`?XD)8FVPtX}QfNfUWlN7sVML)3DO#75+qgu?B{HOvWU1KXl8M|l z)@2ex7EPCx%VH+lL?sa!VVUIY89h(^vwx_+_SxTge&6$+^ZwrVd_T|oz6aqVFTg5G zss$B55CmM&3m}8Q!z|3(0{|;4Kmh;<0W2f~aA*rc(-4yV(xyUc0A6~JK|8eos$dnG z)@TaC6-~jIrTZ~AZ~tIT|D(Z*x|*9cw*aIcm;em=SqKs+Mi5w7pg1fR#^P~!{8AEx zh42Jn0v<0UDkLl-2(%?8Dk3I$5KQuUC-07DW0BM!mh z5Yht3sCV3wJpsZ}Ko}T{!xK;&qG$nY6>1-bQPVioFr76;j?=-Xvw)Ik`^za8RtU$ZBcQ8kzMfs%jf3I=Xs4Zr!GDVroV;-(_LB&(40o z!-0d2?njP#czSvJ91ji&JrNcj5qCQN%-M4Z=Tk3TzLJ*y%he3#jlBGu1%*Y$6_r)2 z+tqh!YJYETX}!;W@bFRBpWQvZ9PZP;!J(J0UJt(+c{|FR`Y=5+`}f>DU%&+c_%jx& z|H>tfa$y8(KoD?2m=kEi;#i!L7G7cpjo=n2xpoUfh`2K)x4cPMS=)9}>Tpn}$m(@E zgX?($v?a3TfyI6o*%z>{Tzx#eos{RK={)7EuGtnE#>hP9?aoO!?L&hVd`_ zG}lD8E1R$X^!(sKMiaM~YOH)lHD^yqua3{|T!r4ui0n=mHM)y@wE9?N{J@3M-2IE9 z2EVyRU>G*TbeH1x8inVZa%X;OYhzueM%JAhx)2ghq9hmT@>~G-`3285b{sOTXr_>_^u$n_xv?wUPf`$&>N$vjk8nQ%Mp&g5>@+GbU8~V-clUMX z&p0BW881)z(~eZ9bWc+i`EvXG8(($>@P`K;6)T>!wop|$KU1p^wm&WDMZ0uZg2{+8 zse2PKQ&?kMKD~6CqHDBDL53DtU7LQl69FFY6CI0IsKm)5)rA|-G}92o<#mG0>b9>-PZg0UtD}{>|*c?q(+0cMt9D( zmMuiGQHPv)8Xa}WLjXs2(f;Mq2l2$ckQrFE$Zh)LjSnN$#s{dsX-qV#$~ni1vh|a` z=P>SG(9&x7bQQ)a_J@tKb9S@$h2<_iA((pErCctds&?#a5Ac(<3f#h$xlmb$+iUQ! zEHOmG)1l|}hnXnH;c@=^nykElwuD=&rDl}-=gIbAWkU#PKDF||6Hc~9MXFg+Of@TW zx00``ru5cz{GkS`;)^osp*Gd&uFTpCoR+BauVi*k8=Sm)&MFbb-?oOm@eN(aWW+Cj ztITAPTu;wC5pY>@;sH6lvXf8C|7i7S8>}{xsqm^H?zMCAZ-O~A}q4-{`(ORFu z^=o`2>JoTn0VmDoqHZnd^W^$eN<^!i-SpC(j#aX=NeWllG%hD!c!Rmg8H*1j9s5+` z2s15wKS|kJo{KfLJ#eW5=x2cXK|hMsi57lnl7v6KZiESc8iuEa>*HpF=ahf-j;ILx z*gC7>dzdN{mH9A1baPV*By<`9UM|?Cx{h|ufydECmhUL_ zc^untC2m{CUD-jS74px?ldqGmx#Pyi?GGde;cbTKmIWEMqYjjJM7x-8Ia$m8*ZdbI IB_mJ&0k$w~`v3p{ literal 0 HcmV?d00001 diff --git a/Web/static/img/tour/audios.png b/Web/static/img/tour/audios.png new file mode 100644 index 0000000000000000000000000000000000000000..a15affe0511d972531cf450b2bb9ecac41e1a664 GIT binary patch literal 6234 zcmeHLYi!$86uzO|)NSp?20|-H$YcnHhGRR4o!F$=N>XT{=}H^4ZbPtZ`#LpE>|ncX zIw4pKh#~f4ynY~FW!fa*2jVFF{L3c=4jWKUhsC z8f=oZY5L?uc=E|TONej#`yPI5e0nKyUFc+D_nXf(Oliu+Z$Ez9w*1oJ73&^j8XB%h zzrTn7yt(} z?GMeLHa0hdm3OnHZ3nYwu$fDHyYn6R8-ETI4jsi`7g&8g{Xc^*-lA{9Ps_@kTTK8~ z9kod)XP*^`%c4oSQm-k5G#o~j5Kp@b|*R7yq76e?|D2Ux)W+hi(C zI4sssTe#iUNDSn9iHvR_f{k#b#KfXRf{!pPA5F%uC89LLQFNSQc#??;_$N*i)lV3! zBZ!nCD?(rPhN?RJ)D|AJtg=8+qobpd(O5(`3Rs-yDLP6;qa@}a%`we_6;d)-twSs>E@t*4rqV5MgewlF6Zl*b1$WmufnuCMz9%GuguA9=Eus2>O-bJ zY)GhM1aEVlXN_s~p&6&nLmadF!j`I>L)KoYF$z`GbFkVgE~^uAJN_x`0wH4{Xz4~? z*NbU4mwG*K61d=T?ywP-SieQpWPQ}UCPmc?*$GsOhO*E?=_nnIku*cnY@SI9Y(j|9 z8|auo(*X#Vvj!oj%c?Rq3xUUi0>cCm0-Uu7xI$!T!Fgc>d3_XF5)|DiK`X76U;$BO zt&pN-h(4d6SVBfG>IU8rO2<;vTx4IyNjsrSGcBk|s4JVsgovuj0wY5x!l+ELQAH*b z@i;?@VmwMFrI->$GEK)47<-m@9dK5chV25uz;4TsbqRydY>DQ$q>_LNnUt9*$uLO9 zYB)rqq?lki7BVyxJv9NaIyIXO6*n|k3~<O93CS@@$lf1%6Bovbh zuOME8yyPjE1Ex+(;+SX7;kQ-+3#>{@IKd&TH=C^u6vyHm~r zlhb1gIB$nQ?0Bo~V@B1)18jzJ`k;z<(5C6-Jk6C9G+gs5=1E`MhlIo*@y zyzcooq>*29(je3dNKR9pF&^Qc8=AOC!$eYvk-W@_I3PGQJYpe9iwT6xoA(E+=H&bi2VmC+$NU=hXQIAv^@(w+tcBvAj`q zz74NVe5T=5q4DhL4WqwpI&^my5T6CDFLVHPRm=h7sq&m1@TxxprR*0k0Z*@Z%yPz$ zG?Suc(R?IlWxyUri^I;u$#XRRP>lVzDi~a@72JA$!QgVO;Edd@nrfw^RZVqdU&ekz zbYEop@vQ|)b1WNUQ&c^04!j$_q2t8!mtL8_@BTaY_O`5fsr`Dk^M$3$7Kw`vSxkn-Lin$DhNj}G ZH_lEBoxS7mG5hMetD`6T^7T6=egWw+lqvuK literal 0 HcmV?d00001 diff --git a/Web/static/img/tour/audios_playlists.png b/Web/static/img/tour/audios_playlists.png new file mode 100644 index 0000000000000000000000000000000000000000..a15affe0511d972531cf450b2bb9ecac41e1a664 GIT binary patch literal 6234 zcmeHLYi!$86uzO|)NSp?20|-H$YcnHhGRR4o!F$=N>XT{=}H^4ZbPtZ`#LpE>|ncX zIw4pKh#~f4ynY~FW!fa*2jVFF{L3c=4jWKUhsC z8f=oZY5L?uc=E|TONej#`yPI5e0nKyUFc+D_nXf(Oliu+Z$Ez9w*1oJ73&^j8XB%h zzrTn7yt(} z?GMeLHa0hdm3OnHZ3nYwu$fDHyYn6R8-ETI4jsi`7g&8g{Xc^*-lA{9Ps_@kTTK8~ z9kod)XP*^`%c4oSQm-k5G#o~j5Kp@b|*R7yq76e?|D2Ux)W+hi(C zI4sssTe#iUNDSn9iHvR_f{k#b#KfXRf{!pPA5F%uC89LLQFNSQc#??;_$N*i)lV3! zBZ!nCD?(rPhN?RJ)D|AJtg=8+qobpd(O5(`3Rs-yDLP6;qa@}a%`we_6;d)-twSs>E@t*4rqV5MgewlF6Zl*b1$WmufnuCMz9%GuguA9=Eus2>O-bJ zY)GhM1aEVlXN_s~p&6&nLmadF!j`I>L)KoYF$z`GbFkVgE~^uAJN_x`0wH4{Xz4~? z*NbU4mwG*K61d=T?ywP-SieQpWPQ}UCPmc?*$GsOhO*E?=_nnIku*cnY@SI9Y(j|9 z8|auo(*X#Vvj!oj%c?Rq3xUUi0>cCm0-Uu7xI$!T!Fgc>d3_XF5)|DiK`X76U;$BO zt&pN-h(4d6SVBfG>IU8rO2<;vTx4IyNjsrSGcBk|s4JVsgovuj0wY5x!l+ELQAH*b z@i;?@VmwMFrI->$GEK)47<-m@9dK5chV25uz;4TsbqRydY>DQ$q>_LNnUt9*$uLO9 zYB)rqq?lki7BVyxJv9NaIyIXO6*n|k3~<O93CS@@$lf1%6Bovbh zuOME8yyPjE1Ex+(;+SX7;kQ-+3#>{@IKd&TH=C^u6vyHm~r zlhb1gIB$nQ?0Bo~V@B1)18jzJ`k;z<(5C6-Jk6C9G+gs5=1E`MhlIo*@y zyzcooq>*29(je3dNKR9pF&^Qc8=AOC!$eYvk-W@_I3PGQJYpe9iwT6xoA(E+=H&bi2VmC+$NU=hXQIAv^@(w+tcBvAj`q zz74NVe5T=5q4DhL4WqwpI&^my5T6CDFLVHPRm=h7sq&m1@TxxprR*0k0Z*@Z%yPz$ zG?Suc(R?IlWxyUri^I;u$#XRRP>lVzDi~a@72JA$!QgVO;Edd@nrfw^RZVqdU&ekz zbYEop@vQ|)b1WNUQ&c^04!j$_q2t8!mtL8_@BTaY_O`5fsr`Dk^M$3$7Kw`vSxkn-Lin$DhNj}G ZH_lEBoxS7mG5hMetD`6T^7T6=egWw+lqvuK literal 0 HcmV?d00001 diff --git a/Web/static/img/tour/audios_search.png b/Web/static/img/tour/audios_search.png new file mode 100644 index 0000000000000000000000000000000000000000..a15affe0511d972531cf450b2bb9ecac41e1a664 GIT binary patch literal 6234 zcmeHLYi!$86uzO|)NSp?20|-H$YcnHhGRR4o!F$=N>XT{=}H^4ZbPtZ`#LpE>|ncX zIw4pKh#~f4ynY~FW!fa*2jVFF{L3c=4jWKUhsC z8f=oZY5L?uc=E|TONej#`yPI5e0nKyUFc+D_nXf(Oliu+Z$Ez9w*1oJ73&^j8XB%h zzrTn7yt(} z?GMeLHa0hdm3OnHZ3nYwu$fDHyYn6R8-ETI4jsi`7g&8g{Xc^*-lA{9Ps_@kTTK8~ z9kod)XP*^`%c4oSQm-k5G#o~j5Kp@b|*R7yq76e?|D2Ux)W+hi(C zI4sssTe#iUNDSn9iHvR_f{k#b#KfXRf{!pPA5F%uC89LLQFNSQc#??;_$N*i)lV3! zBZ!nCD?(rPhN?RJ)D|AJtg=8+qobpd(O5(`3Rs-yDLP6;qa@}a%`we_6;d)-twSs>E@t*4rqV5MgewlF6Zl*b1$WmufnuCMz9%GuguA9=Eus2>O-bJ zY)GhM1aEVlXN_s~p&6&nLmadF!j`I>L)KoYF$z`GbFkVgE~^uAJN_x`0wH4{Xz4~? z*NbU4mwG*K61d=T?ywP-SieQpWPQ}UCPmc?*$GsOhO*E?=_nnIku*cnY@SI9Y(j|9 z8|auo(*X#Vvj!oj%c?Rq3xUUi0>cCm0-Uu7xI$!T!Fgc>d3_XF5)|DiK`X76U;$BO zt&pN-h(4d6SVBfG>IU8rO2<;vTx4IyNjsrSGcBk|s4JVsgovuj0wY5x!l+ELQAH*b z@i;?@VmwMFrI->$GEK)47<-m@9dK5chV25uz;4TsbqRydY>DQ$q>_LNnUt9*$uLO9 zYB)rqq?lki7BVyxJv9NaIyIXO6*n|k3~<O93CS@@$lf1%6Bovbh zuOME8yyPjE1Ex+(;+SX7;kQ-+3#>{@IKd&TH=C^u6vyHm~r zlhb1gIB$nQ?0Bo~V@B1)18jzJ`k;z<(5C6-Jk6C9G+gs5=1E`MhlIo*@y zyzcooq>*29(je3dNKR9pF&^Qc8=AOC!$eYvk-W@_I3PGQJYpe9iwT6xoA(E+=H&bi2VmC+$NU=hXQIAv^@(w+tcBvAj`q zz74NVe5T=5q4DhL4WqwpI&^my5T6CDFLVHPRm=h7sq&m1@TxxprR*0k0Z*@Z%yPz$ zG?Suc(R?IlWxyUri^I;u$#XRRP>lVzDi~a@72JA$!QgVO;Edd@nrfw^RZVqdU&ekz zbYEop@vQ|)b1WNUQ&c^04!j$_q2t8!mtL8_@BTaY_O`5fsr`Dk^M$3$7Kw`vSxkn-Lin$DhNj}G ZH_lEBoxS7mG5hMetD`6T^7T6=egWw+lqvuK literal 0 HcmV?d00001 diff --git a/Web/static/img/tour/audios_upload.png b/Web/static/img/tour/audios_upload.png new file mode 100644 index 0000000000000000000000000000000000000000..a15affe0511d972531cf450b2bb9ecac41e1a664 GIT binary patch literal 6234 zcmeHLYi!$86uzO|)NSp?20|-H$YcnHhGRR4o!F$=N>XT{=}H^4ZbPtZ`#LpE>|ncX zIw4pKh#~f4ynY~FW!fa*2jVFF{L3c=4jWKUhsC z8f=oZY5L?uc=E|TONej#`yPI5e0nKyUFc+D_nXf(Oliu+Z$Ez9w*1oJ73&^j8XB%h zzrTn7yt(} z?GMeLHa0hdm3OnHZ3nYwu$fDHyYn6R8-ETI4jsi`7g&8g{Xc^*-lA{9Ps_@kTTK8~ z9kod)XP*^`%c4oSQm-k5G#o~j5Kp@b|*R7yq76e?|D2Ux)W+hi(C zI4sssTe#iUNDSn9iHvR_f{k#b#KfXRf{!pPA5F%uC89LLQFNSQc#??;_$N*i)lV3! zBZ!nCD?(rPhN?RJ)D|AJtg=8+qobpd(O5(`3Rs-yDLP6;qa@}a%`we_6;d)-twSs>E@t*4rqV5MgewlF6Zl*b1$WmufnuCMz9%GuguA9=Eus2>O-bJ zY)GhM1aEVlXN_s~p&6&nLmadF!j`I>L)KoYF$z`GbFkVgE~^uAJN_x`0wH4{Xz4~? z*NbU4mwG*K61d=T?ywP-SieQpWPQ}UCPmc?*$GsOhO*E?=_nnIku*cnY@SI9Y(j|9 z8|auo(*X#Vvj!oj%c?Rq3xUUi0>cCm0-Uu7xI$!T!Fgc>d3_XF5)|DiK`X76U;$BO zt&pN-h(4d6SVBfG>IU8rO2<;vTx4IyNjsrSGcBk|s4JVsgovuj0wY5x!l+ELQAH*b z@i;?@VmwMFrI->$GEK)47<-m@9dK5chV25uz;4TsbqRydY>DQ$q>_LNnUt9*$uLO9 zYB)rqq?lki7BVyxJv9NaIyIXO6*n|k3~<O93CS@@$lf1%6Bovbh zuOME8yyPjE1Ex+(;+SX7;kQ-+3#>{@IKd&TH=C^u6vyHm~r zlhb1gIB$nQ?0Bo~V@B1)18jzJ`k;z<(5C6-Jk6C9G+gs5=1E`MhlIo*@y zyzcooq>*29(je3dNKR9pF&^Qc8=AOC!$eYvk-W@_I3PGQJYpe9iwT6xoA(E+=H&bi2VmC+$NU=hXQIAv^@(w+tcBvAj`q zz74NVe5T=5q4DhL4WqwpI&^my5T6CDFLVHPRm=h7sq&m1@TxxprR*0k0Z*@Z%yPz$ zG?Suc(R?IlWxyUri^I;u$#XRRP>lVzDi~a@72JA$!QgVO;Edd@nrfw^RZVqdU&ekz zbYEop@vQ|)b1WNUQ&c^04!j$_q2t8!mtL8_@BTaY_O`5fsr`Dk^M$3$7Kw`vSxkn-Lin$DhNj}G ZH_lEBoxS7mG5hMetD`6T^7T6=egWw+lqvuK literal 0 HcmV?d00001 diff --git a/Web/static/js/al_music.js b/Web/static/js/al_music.js new file mode 100644 index 00000000..fbbe7c82 --- /dev/null +++ b/Web/static/js/al_music.js @@ -0,0 +1,1433 @@ +function fmtTime(time) { + const mins = String(Math.floor(time / 60)).padStart(2, '0'); + const secs = String(Math.floor(time % 60)).padStart(2, '0'); + return `${ mins}:${ secs}`; +} + +function fastError(message) { + MessageBox(tr("error"), message, [tr("ok")], [Function.noop]) +} + +// elapsed это вроде прошедшие, а оставшееся это remaining но ладно уже +function getElapsedTime(fullTime, time) { + let timer = fullTime - time + + if(timer < 0) return "-00:00" + + return "-" + fmtTime(timer) +} + +window.savedAudiosPages = {} + +class playersSearcher { + constructor(context_type, context_id) { + this.context_type = context_type + this.context_id = context_id + this.searchType = "by_name" + this.query = "" + this.page = 1 + this.successCallback = () => {} + this.errorCallback = () => {} + this.beforesendCallback = () => {} + this.clearContainer = () => {} + } + + execute() { + $.ajax({ + type: "POST", + url: "/audios/context", + data: { + context: this.context_type, + hash: u("meta[name=csrf]").attr("value"), + page: this.page, + query: this.query, + context_entity: this.context_id, + type: this.searchType, + returnPlayers: 1, + }, + beforeSend: () => { + this.beforesendCallback() + }, + error: () => { + this.errorCallback() + }, + success: (response) => { + this.successCallback(response, this) + } + }) + } + + movePage(page) { + this.page = page + this.execute() + } +} + +class bigPlayer { + tracks = { + currentTrack: null, + nextTrack: null, + previousTrack: null, + tracks: [] + } + + context = { + context_type: null, + context_id: 0, + pagesCount: 0, + playedPages: [], + object: [], + } + + nodes = { + dashPlayer: null, + audioPlayer: null, + thisPlayer: null, + playButtons: null, + } + + timeType = 0 + + findTrack(id) { + return this.tracks["tracks"].find(item => item.id == id) + } + + constructor(context, context_id, page = 1) { + this.context["context_type"] = context + this.context["context_id"] = context_id + this.context["playedPages"].push(String(page)) + + this.nodes["thisPlayer"] = document.querySelector(".bigPlayer") + this.nodes["thisPlayer"].classList.add("lagged") + this.nodes["audioPlayer"] = document.createElement("audio") + + this.player = () => { return this.nodes["audioPlayer"] } + this.nodes["playButtons"] = this.nodes["thisPlayer"].querySelector(".playButtons") + this.nodes["dashPlayer"] = dashjs.MediaPlayer().create() + + let formdata = new FormData() + formdata.append("context", context) + formdata.append("context_entity", context_id) + formdata.append("query", context_id) + formdata.append("hash", u("meta[name=csrf]").attr("value")) + formdata.append("page", page) + + ky.post("/audios/context", { + hooks: { + afterResponse: [ + async (_request, _options, response) => { + if(response.status !== 200) { + fastError(tr("unable_to_load_queue")) + return + } + + let contextObject = await response.json() + + if(!contextObject.success) { + fastError(tr("unable_to_load_queue")) + return + } + + this.nodes["thisPlayer"].classList.remove("lagged") + this.tracks["tracks"] = contextObject["items"] + this.context["pagesCount"] = contextObject["pagesCount"] + + if(localStorage.lastPlayedTrack != null && this.tracks["tracks"].find(item => item.id == localStorage.lastPlayedTrack) != null) { + this.setTrack(localStorage.lastPlayedTrack) + this.pause() + } + + console.info("Context is successfully loaded") + } + ] + }, + body: formdata + }) + + u(this.nodes["playButtons"].querySelector(".playButton")).on("click", (e) => { + if(this.player().paused) + this.play() + else + this.pause() + }) + + u(this.player()).on("timeupdate", (e) => { + const time = this.player().currentTime; + const ps = ((time * 100) / this.tracks["currentTrack"].length).toFixed(3) + this.nodes["thisPlayer"].querySelector(".time").innerHTML = fmtTime(time) + this.timeType == 0 ? this.nodes["thisPlayer"].querySelector(".elapsedTime").innerHTML = getElapsedTime(this.tracks["currentTrack"].length, time) + : null + + if (ps <= 100) + this.nodes["thisPlayer"].querySelector(".selectableTrack .slider").style.left = `${ ps}%`; + + }) + + u(this.player()).on("volumechange", (e) => { + const volume = this.player().volume; + const ps = Math.ceil((volume * 100) / 1); + + if (ps <= 100) + this.nodes["thisPlayer"].querySelector(".volumePanel .selectableTrack .slider").style.left = `${ ps}%`; + + localStorage.volume = volume + }) + + u(".bigPlayer .track > div").on("click mouseup", (e) => { + if(this.tracks["currentTrack"] == null) + return + + let rect = this.nodes["thisPlayer"].querySelector(".selectableTrack").getBoundingClientRect(); + + const width = e.clientX - rect.left; + const time = Math.ceil((width * this.tracks["currentTrack"].length) / (rect.right - rect.left)); + + this.player().currentTime = time; + }) + + u(".bigPlayer .trackPanel .selectableTrack").on("mousemove", (e) => { + if(this.tracks["currentTrack"] == null) + return + + let rect = this.nodes["thisPlayer"].querySelector(".selectableTrack").getBoundingClientRect(); + + const width = e.clientX - rect.left; + const time = Math.ceil((width * this.tracks["currentTrack"].length) / (rect.right - rect.left)); + + document.querySelector(".bigPlayer .track .bigPlayerTip").style.display = "block" + document.querySelector(".bigPlayer .track .bigPlayerTip").innerHTML = fmtTime(time) + document.querySelector(".bigPlayer .track .bigPlayerTip").style.left = `min(${width - 15}px, 315.5px)` + }) + + u(".bigPlayer .nextButton").on("mouseover mouseleave", (e) => { + if(this.tracks["currentTrack"] == null) + return + + if(e.type == "mouseleave") { + $(".nextTrackTip").remove() + return + } + + e.currentTarget.parentNode.insertAdjacentHTML("afterbegin", ` +
    + ${ovk_proc_strtr(escapeHtml(this.findTrack(this.tracks["previousTrack"]).name), 20) ?? ""} +
    + `) + + document.querySelector(".nextTrackTip").style.display = "block" + }) + + u(".bigPlayer .backButton").on("mouseover mouseleave", (e) => { + if(this.tracks["currentTrack"] == null) + return + + if(e.type == "mouseleave") { + $(".previousTrackTip").remove() + return + } + + e.currentTarget.parentNode.insertAdjacentHTML("afterbegin", ` +
    + ${ovk_proc_strtr(escapeHtml(this.findTrack(this.tracks["nextTrack"]).name), 20) ?? ""} +
    + `) + + document.querySelector(".previousTrackTip").style.display = "block" + }) + + u(".bigPlayer .trackPanel .selectableTrack").on("mouseleave", (e) => { + if(this.tracks["currentTrack"] == null) + return + + document.querySelector(".bigPlayer .track .bigPlayerTip").style.display = "none" + }) + + u(".bigPlayer .volumePanel > div").on("click mouseup mousemove", (e) => { + if(this.tracks["currentTrack"] == null) + return + + if(e.type == "mousemove") { + let buttonsPresseed = _bsdnUnwrapBitMask(e.buttons) + if(!buttonsPresseed[0]) + return; + } + + let rect = this.nodes["thisPlayer"].querySelector(".volumePanel .selectableTrack").getBoundingClientRect(); + + const width = e.clientX - rect.left; + const volume = Math.max(0, (width * 1) / (rect.right - rect.left)); + + this.player().volume = volume; + }) + + u(".bigPlayer .elapsedTime").on("click", (e) => { + if(this.tracks["currentTrack"] == null) + return + + this.timeType == 0 ? (this.timeType = 1) : (this.timeType = 0) + + localStorage.playerTimeType = this.timeType + + this.nodes["thisPlayer"].querySelector(".elapsedTime").innerHTML = this.timeType == 1 ? + fmtTime(this.tracks["currentTrack"].length) + : getElapsedTime(this.tracks["currentTrack"].length, this.player().currentTime) + }) + + u(".bigPlayer .additionalButtons .repeatButton").on("click", (e) => { + if(this.tracks["currentTrack"] == null) + return + + e.currentTarget.classList.toggle("pressed") + + if(e.currentTarget.classList.contains("pressed")) + this.player().loop = true + else + this.player().loop = false + }) + + u(".bigPlayer .additionalButtons .shuffleButton").on("click", (e) => { + if(this.tracks["currentTrack"] == null) + return + + this.tracks["tracks"].sort(() => Math.random() - 0.59) + this.setTrack(this.tracks["tracks"].at(0).id) + }) + + // хз что она делала в самом вк, но тут сделаем вид что это просто мут музыки + u(".bigPlayer .additionalButtons .deviceButton").on("click", (e) => { + if(this.tracks["currentTrack"] == null) + return + + e.currentTarget.classList.toggle("pressed") + + this.player().muted = e.currentTarget.classList.contains("pressed") + }) + + u(".bigPlayer .arrowsButtons .nextButton").on("click", (e) => { + this.showPreviousTrack() + }) + + u(".bigPlayer .arrowsButtons .backButton").on("click", (e) => { + this.showNextTrack() + }) + + u(".bigPlayer .trackInfo b").on("click", (e) => { + window.location.assign(`/search?query=${e.currentTarget.innerHTML}&type=audios&only_performers=on`) + }) + + u(document).on("keydown", (e) => { + if(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", " "].includes(e.key)) { + if(document.querySelector(".ovk-diag-cont") != null) + return + + e.preventDefault() + } + + switch(e.key) { + case "ArrowUp": + this.player().volume = Math.min(0.99, this.player().volume + 0.1) + break + case "ArrowDown": + this.player().volume = Math.max(0, this.player().volume - 0.1) + break + case "ArrowLeft": + this.player().currentTime = this.player().currentTime - 3 + break + case "ArrowRight": + this.player().currentTime = this.player().currentTime + 3 + break + // буквально + case " ": + if(this.player().paused) + this.play() + else + this.pause() + + break; + } + }) + + u(document).on("keyup", (e) => { + if([87, 65, 83, 68, 82, 77].includes(e.keyCode)) { + if(document.querySelector(".ovk-diag-cont") != null) + return + + e.preventDefault() + } + + switch(e.keyCode) { + case 87: + case 65: + this.showPreviousTrack() + break + case 83: + case 68: + this.showNextTrack() + break + case 82: + document.querySelector(".bigPlayer .additionalButtons .repeatButton").click() + break + case 77: + document.querySelector(".bigPlayer .additionalButtons .deviceButton").click() + break + } + }) + + u(this.player()).on("ended", (e) => { + e.preventDefault() + + // в начало очереди + if(!this.tracks.nextTrack) { + if(!this.context["playedPages"].includes("1")) { + $.ajax({ + type: "POST", + url: "/audios/context", + data: { + context: this["context"].context_type, + context_entity: this["context"].context_id, + hash: u("meta[name=csrf]").attr("value"), + page: 1 + }, + success: (response_2) => { + this.tracks["tracks"] = response_2["items"].concat(this.tracks["tracks"]) + this.context["playedPages"].push(String(1)) + + this.setTrack(this.tracks["tracks"][0].id) + } + }) + } else { + this.setTrack(this.tracks.tracks[0].id) + } + + return + } + + this.showNextTrack() + }) + + u(this.player()).on("loadstart", (e) => { + let playlist = this.context.context_type == "playlist_context" ? this.context.context_id : null + + let tempThisId = this.tracks.currentTrack.id + setTimeout(() => { + if(tempThisId != this.tracks.currentTrack.id) return + + $.ajax({ + type: "POST", + url: `/audio${this.tracks["currentTrack"].id}/listen`, + data: { + hash: u("meta[name=csrf]").attr("value"), + playlist: playlist + }, + success: (response) => { + if(response.success) { + console.info("Listen is counted.") + + if(response.new_playlists_listens) + document.querySelector("#listensCount").innerHTML = tr("listens_count", response.new_playlists_listens) + } else + console.info("Listen is not counted.") + } + }) + }, 2000) + }) + + if(localStorage.volume != null && localStorage.volume < 1 && localStorage.volume > 0) + this.player().volume = localStorage.volume + else + this.player().volume = 0.75 + + if(localStorage.playerTimeType == 'null' || localStorage.playerTimeType == null) + this.timeType = 0 + else + this.timeType = localStorage.playerTimeType + + navigator.mediaSession.setActionHandler('play', () => { this.play() }); + navigator.mediaSession.setActionHandler('pause', () => { this.pause() }); + navigator.mediaSession.setActionHandler('previoustrack', () => { this.showPreviousTrack() }); + navigator.mediaSession.setActionHandler('nexttrack', () => { this.showNextTrack() }); + navigator.mediaSession.setActionHandler("seekto", (details) => { + this.player().currentTime = details.seekTime; + }); + } + + play() { + if(this.tracks["currentTrack"] == null) + return + + document.querySelectorAll('audio').forEach(el => el.pause()); + document.querySelector(`.audioEmbed[data-realid='${this.tracks["currentTrack"].id}'] .audioEntry .playerButton .playIcon`) != null ? document.querySelector(`.audioEmbed[data-realid='${this.tracks["currentTrack"].id}'] .audioEntry .playerButton .playIcon`).classList.add("paused") : void(0) + this.player().play() + this.nodes["playButtons"].querySelector(".playButton").classList.add("pause") + document.querySelector('link[rel="icon"], link[rel="shortcut icon"]').setAttribute("href", "/assets/packages/static/openvk/img/favicons/favicon24_paused.png") + + navigator.mediaSession.playbackState = "playing" + } + + pause() { + if(this.tracks["currentTrack"] == null) + return + + document.querySelector(`.audioEmbed[data-realid='${this.tracks["currentTrack"].id}'] .audioEntry .playerButton .playIcon`) != null ? document.querySelector(`.audioEmbed[data-realid='${this.tracks["currentTrack"].id}'] .audioEntry .playerButton .playIcon`).classList.remove("paused") : void(0) + this.player().pause() + this.nodes["playButtons"].querySelector(".playButton").classList.remove("pause") + document.querySelector('link[rel="icon"], link[rel="shortcut icon"]').setAttribute("href", "/assets/packages/static/openvk/img/favicons/favicon24_playing.png") + + navigator.mediaSession.playbackState = "paused" + } + + showPreviousTrack() { + if(this.tracks["currentTrack"] == null || this.tracks["previousTrack"] == null) + return + + this.setTrack(this.tracks["previousTrack"]) + } + + showNextTrack() { + if(this.tracks["currentTrack"] == null || this.tracks["nextTrack"] == null) + return + + this.setTrack(this.tracks["nextTrack"]) + } + + updateButtons() { + // перепутал некст и бек. + let prevButton = this.nodes["thisPlayer"].querySelector(".nextButton") + let nextButton = this.nodes["thisPlayer"].querySelector(".backButton") + + if(this.tracks["previousTrack"] == null) + prevButton.classList.add("lagged") + else + prevButton.classList.remove("lagged") + + if(this.tracks["nextTrack"] == null) + nextButton.classList.add("lagged") + else + nextButton.classList.remove("lagged") + + if(document.querySelector(".nextTrackTip") != null) { + let track = this.findTrack(this.tracks["previousTrack"]) + document.querySelector(".nextTrackTip").innerHTML = ` + ${track != null ? ovk_proc_strtr(escapeHtml(track.name), 20) : ""} + ` + } + + if(document.querySelector(".previousTrackTip") != null) { + let track = this.findTrack(this.tracks["nextTrack"]) + document.querySelector(".previousTrackTip").innerHTML = ` + ${track != null ? ovk_proc_strtr(escapeHtml(track.name ?? ""), 20) : ""} + ` + } + } + + setTrack(id) { + if(this.tracks["tracks"] == null) { + console.info("Context is not loaded yet. Wait please") + return 0; + } + + document.querySelectorAll(".audioEntry.nowPlaying").forEach(el => el.classList.remove("nowPlaying")) + let obj = this.tracks["tracks"].find(item => item.id == id) + + if(obj == null) { + fastError("No audio in context") + return + } + + this.nodes["thisPlayer"].querySelector(".trackInfo span").innerHTML = escapeHtml(obj.name) + this.nodes["thisPlayer"].querySelector(".trackInfo b").innerHTML = escapeHtml(obj.performer) + this.nodes["thisPlayer"].querySelector(".trackInfo .time").innerHTML = fmtTime(obj.length) + this.tracks["currentTrack"] = obj + + let indexOfCurrentTrack = this.tracks["tracks"].indexOf(obj) ?? 0 + this.tracks["nextTrack"] = this.tracks["tracks"].at(indexOfCurrentTrack + 1) != null ? this.tracks["tracks"].at(indexOfCurrentTrack + 1).id : null + + if(indexOfCurrentTrack - 1 >= 0) + this.tracks["previousTrack"] = this.tracks["tracks"].at(indexOfCurrentTrack - 1).id + else + this.tracks["previousTrack"] = null + + if(this.tracks["nextTrack"] == null && Math.max(...this.context["playedPages"]) < this.context["pagesCount"] + || this.tracks["previousTrack"] == null && (Math.min(...this.context["playedPages"]) > 1)) { + + // idk how it works + let lesser = this.tracks["previousTrack"] == null ? (Math.min(...this.context["playedPages"]) > 1) + : Math.max(...this.context["playedPages"]) > this.context["pagesCount"] + + let formdata = new FormData() + formdata.append("context", this.context["context_type"]) + formdata.append("context_entity", this.context["context_id"]) + formdata.append("hash", u("meta[name=csrf]").attr("value")) + + if(lesser) + formdata.append("page", Math.min(...this.context["playedPages"]) - 1) + else + formdata.append("page", Number(Math.max(...this.context["playedPages"])) + 1) + + ky.post("/audios/context", { + hooks: { + afterResponse: [ + async (_request, _options, response) => { + let newArr = await response.json() + + if(lesser) + this.tracks["tracks"] = newArr["items"].concat(this.tracks["tracks"]) + else + this.tracks["tracks"] = this.tracks["tracks"].concat(newArr["items"]) + + this.context["playedPages"].push(String(newArr["page"])) + + if(lesser) + this.tracks["previousTrack"] = this.tracks["tracks"].at(this.tracks["tracks"].indexOf(obj) - 1).id + else + this.tracks["nextTrack"] = this.tracks["tracks"].at(indexOfCurrentTrack + 1) != null ? this.tracks["tracks"].at(indexOfCurrentTrack + 1).id : null + + this.updateButtons() + console.info("Context is successfully loaded") + } + ] + }, + body: formdata + }) + } + + if(this.tracks["currentTrack"].available == false || this.tracks["currentTrack"].withdrawn) + this.showNextTrack() + + this.updateButtons() + + const protData = { + "org.w3.clearkey": { + "clearkeys": obj.keys + } + }; + + this.nodes["dashPlayer"].initialize(this.player(), obj.url, false); + this.nodes["dashPlayer"].setProtectionData(protData); + + this.play() + + let playerAtPage = document.querySelector(`.audioEmbed[data-realid='${this.tracks["currentTrack"].id}'] .audioEntry`) + if(playerAtPage != null) + playerAtPage.classList.add("nowPlaying") + + document.querySelectorAll(`.audioEntry .playerButton .playIcon.paused`).forEach(el => el.classList.remove("paused")) + + localStorage.lastPlayedTrack = this.tracks["currentTrack"].id + + if(this.timeType == 1) + this.nodes["thisPlayer"].querySelector(".elapsedTime").innerHTML = fmtTime(this.tracks["currentTrack"].length) + + let album = document.querySelector(".playlistBlock") + + navigator.mediaSession.metadata = new MediaMetadata({ + title: obj.name, + artist: obj.performer, + album: album == null ? "OpenVK Audios" : album.querySelector(".playlistInfo h4").innerHTML, + artwork: [{ src: album == null ? "/assets/packages/static/openvk/img/song.jpg" : album.querySelector(".playlistCover img").src }], + }); + + navigator.mediaSession.setPositionState({ + duration: this.tracks["currentTrack"].length + }) + } +} + +document.addEventListener("DOMContentLoaded", function() { + if(document.querySelector(".bigPlayer") != null) { + let context = document.querySelector("input[name='bigplayer_context']") + + if(!context) + return + + let type = context.dataset.type + let entity = context.dataset.entity + window.player = new bigPlayer(type, entity, context.dataset.page) + + let bigplayer = document.querySelector('.bigPlayerDetector') + + let bigPlayerObserver = new IntersectionObserver(entries => { + entries.forEach(x => { + if(x.isIntersecting) { + document.querySelector('.bigPlayer').classList.remove("floating") + document.querySelector('.searchOptions .searchList').classList.remove("floating") + document.querySelector('.bigPlayerDetector').style.marginTop = "0px" + } else { + document.querySelector('.searchOptions .searchList').classList.add("floating") + document.querySelector('.bigPlayer').classList.add("floating") + document.querySelector('.bigPlayerDetector').style.marginTop = "46px" + } + }); + }, { + root: null, + rootMargin: "0px", + threshold: 0 + }); + + if(bigplayer != null) + bigPlayerObserver.observe(bigplayer); + } + + $(`.audioEntry .mediaInfo`).on("mouseover mouseleave", (e) => { + const info = e.currentTarget.closest(".mediaInfo") + const overfl = info.querySelector(".info") + + if(e.originalEvent.type == "mouseleave" || e.originalEvent.type == "mouseout") { + info.classList.add("noOverflow") + info.classList.remove("overflowedName") + } else { + info.classList.remove("noOverflow") + info.classList.add("overflowedName") + } + }) +}) + +$(document).on("click", ".audioEmbed > *", (e) => { + const player = e.currentTarget.closest(".audioEmbed") + + if(player.classList.contains("inited")) return + + initPlayer(player.id.replace("audioEmbed-", ""), + JSON.parse(player.dataset.keys), + player.dataset.url, + player.dataset.length) + + if(e.target.classList.contains("playIcon")) + e.target.click() +}) + +function initPlayer(id, keys, url, length) { + document.querySelector(`#audioEmbed-${ id}`).classList.add("inited") + const audio = document.querySelector(`#audioEmbed-${ id} .audio`); + const playButton = u(`#audioEmbed-${ id} .playerButton > .playIcon`); + const trackDiv = u(`#audioEmbed-${ id} .track > div > div`); + const volumeSpan = u(`#audioEmbed-${ id} .volume span`); + const rect = document.querySelector(`#audioEmbed-${ id} .selectableTrack`).getBoundingClientRect(); + + const playerObject = document.querySelector(`#audioEmbed-${ id}`) + + if(document.querySelector(".bigPlayer") != null) { + playButton.on("click", () => { + if(window.player.tracks["tracks"] == null) + return + + if(window.player.tracks["currentTrack"] == null || window.player.tracks["currentTrack"].id != playerObject.dataset.realid) + window.player.setTrack(playerObject.dataset.realid) + else + document.querySelector(".bigPlayer .playButton").click() + }) + + return + } + + const protData = { + "org.w3.clearkey": { + "clearkeys": keys + } + }; + + const player = dashjs.MediaPlayer().create(); + player.initialize(audio, url, false); + player.setProtectionData(protData); + + playButton.on("click", () => { + if (audio.paused) { + document.querySelectorAll('audio').forEach(el => el.pause()); + audio.play(); + } else { + audio.pause(); + } + }); + + u(audio).on("timeupdate", () => { + const time = audio.currentTime; + const ps = ((time * 100) / length).toFixed(3); + volumeSpan.html(fmtTime(Math.floor(time))); + + if (ps <= 100) + playerObject.querySelector(".lengthTrack .slider").style.left = `${ ps}%`; + }); + + u(audio).on("volumechange", (e) => { + const volume = audio.volume; + const ps = Math.ceil((volume * 100) / 1); + + if (ps <= 100) + playerObject.querySelector(".volumeTrack .slider").style.left = `${ ps}%`; + }) + + const playButtonImageUpdate = () => { + if (!audio.paused) { + playButton.addClass("paused") + document.querySelector('link[rel="icon"], link[rel="shortcut icon"]').setAttribute("href", "/assets/packages/static/openvk/img/favicons/favicon24_paused.png") + } else { + playButton.removeClass("paused") + document.querySelector('link[rel="icon"], link[rel="shortcut icon"]').setAttribute("href", "/assets/packages/static/openvk/img/favicons/favicon24_playing.png") + } + + if(!$(`#audioEmbed-${ id}`).hasClass("havePlayed")) { + $(`#audioEmbed-${ id}`).addClass("havePlayed") + + $(`#audioEmbed-${ id} .track`).toggle() + + $.post(`/audio${playerObject.dataset.realid}/listen`, { + hash: u("meta[name=csrf]").attr("value") + }); + } + }; + + const hideTracks = () => { + $(`#audioEmbed-${ id} .track`).toggle() + $(`#audioEmbed-${ id}`).removeClass("havePlayed") + } + + u(audio).on("play", playButtonImageUpdate); + u(audio).on(["pause", "suspended"], playButtonImageUpdate); + u(audio).on("ended", (e) => { + let thisPlayer = e.target.closest(".audioEmbed") + let nextPlayer = null + if(thisPlayer.closest(".attachment") != null) { + try { + nextPlayer = thisPlayer.closest(".attachment").nextElementSibling.querySelector(".audioEmbed") + } catch(e) {return} + } else if(thisPlayer.closest(".audio") != null) { + try { + nextPlayer = thisPlayer.closest(".audio").nextElementSibling.querySelector(".audioEmbed") + } catch(e) {return} + } else { + nextPlayer = thisPlayer.nextElementSibling + } + + playButtonImageUpdate() + + if(!nextPlayer) return + + initPlayer(nextPlayer.id.replace("audioEmbed-", ""), + JSON.parse(nextPlayer.dataset.keys), + nextPlayer.dataset.url, + nextPlayer.dataset.length) + + nextPlayer.querySelector(".playIcon").click() + hideTracks() + }) + + u(`#audioEmbed-${ id} .lengthTrack > div`).on("click", (e) => { + let rect = document.querySelector("#audioEmbed-" + id + " .selectableTrack").getBoundingClientRect(); + const width = e.clientX - rect.left; + const time = Math.ceil((width * length) / (rect.right - rect.left)); + + audio.currentTime = time; + }); + + u(`#audioEmbed-${ id} .volumeTrack > div`).on("click mouseup mousemove", (e) => { + if(e.type == "mousemove") { + let buttonsPresseed = _bsdnUnwrapBitMask(e.buttons) + if(!buttonsPresseed[0]) + return; + } + + let rect = document.querySelector("#audioEmbed-" + id + " .volumeTrack").getBoundingClientRect(); + + const width = e.clientX - rect.left; + const volume = (width * 1) / (rect.right - rect.left); + + audio.volume = volume; + }); + + audio.volume = localStorage.volume ?? 0.75 + u(audio).trigger("volumechange") +} + +$(document).on("click", ".musicIcon.edit-icon", (e) => { + let player = e.currentTarget.closest(".audioEmbed") + let id = Number(player.dataset.realid) + let performer = e.currentTarget.dataset.performer + let name = e.currentTarget.dataset.title + let genre = player.dataset.genre + let lyrics = e.currentTarget.dataset.lyrics + + MessageBox(tr("edit_audio"), ` +
    + ${tr("performer")} + +
    + +
    + ${tr("audio_name")} + +
    + +
    + ${tr("genre")} + +
    + +
    + ${tr("lyrics")} + +
    + +
    +
    + +
    + ${tr("fully_delete_audio")} +
    + `, [tr("save"), tr("cancel")], [ + function() { + let t_name = $(".ovk-diag-body input[name=name]").val(); + let t_perf = $(".ovk-diag-body input[name=performer]").val(); + let t_genre = $(".ovk-diag-body select[name=genre]").val(); + let t_lyrics = $(".ovk-diag-body textarea[name=lyrics]").val(); + let t_explicit = document.querySelector(".ovk-diag-body input[name=explicit]").checked; + let t_unlisted = document.querySelector(".ovk-diag-body input[name=searchable]").checked; + + $.ajax({ + type: "POST", + url: `/audio${id}/action?act=edit`, + data: { + name: t_name, + performer: t_perf, + genre: t_genre, + lyrics: t_lyrics, + unlisted: Number(t_unlisted), + explicit: Number(t_explicit), + hash: u("meta[name=csrf]").attr("value") + }, + success: (response) => { + if(response.success) { + let perf = player.querySelector(".performer a") + perf.innerHTML = escapeHtml(response.new_info.performer) + perf.setAttribute("href", "/search?query=&type=audios&sort=id&only_performers=on&query="+response.new_info.performer) + + e.target.setAttribute("data-performer", escapeHtml(response.new_info.performer)) + + let name = player.querySelector(".title") + name.innerHTML = escapeHtml(response.new_info.name) + + e.target.setAttribute("data-title", escapeHtml(response.new_info.name)) + + if(response.new_info.lyrics_unformatted != "") { + if(player.querySelector(".lyrics") != null) { + player.querySelector(".lyrics").innerHTML = response.new_info.lyrics + player.querySelector(".title").classList.add("withLyrics") + } else { + player.insertAdjacentHTML("beforeend", ` +
    + ${response.new_info.lyrics} +
    + `) + + player.querySelector(".title").classList.add("withLyrics") + } + } else { + $(player.querySelector(".lyrics")).remove() + player.querySelector(".title").classList.remove("withLyrics") + } + + e.target.setAttribute("data-lyrics", response.new_info.lyrics_unformatted) + e.target.setAttribute("data-explicit", Number(response.new_info.explicit)) + + if(Number(response.new_info.explicit) == 1) { + if(!player.querySelector(".mediaInfo .explicitMark")) + player.querySelector(".mediaInfo").insertAdjacentHTML("beforeend", ` +
    + `) + } else { + $(player.querySelector(".mediaInfo .explicitMark")).remove() + } + + e.target.setAttribute("data-searchable", Number(!response.new_info.unlisted)) + player.setAttribute("data-genre", response.new_info.genre) + + let url = new URL(location.href) + let page = "1" + + if(url.searchParams.p != null) + page = String(url.searchParams.p) + + window.savedAudiosPages[page] = null + } else + fastError(response.flash.message) + } + }); + }, + + Function.noop + ]); + + window.openvk.audio_genres.forEach(elGenre => { + document.querySelector(".ovk-diag-body select[name=genre]").insertAdjacentHTML("beforeend", ` + + `) + }) + + u(".ovk-diag-body #_fullyDeleteAudio").on("click", (e) => { + u("body").removeClass("dimmed"); + document.querySelector("html").style.overflowY = "scroll" + + u(".ovk-diag-cont").remove(); + + $.ajax({ + type: "POST", + url: `/audio${id}/action?act=delete`, + data: { + hash: u("meta[name=csrf]").attr("value") + }, + success: (response) => { + if(response.success) + u(player).remove() + else + fastError(response.flash.message) + } + }); + }) +}) + +$(document).on("click", ".title.withLyrics", (e) => { + let parent = e.currentTarget.closest(".audioEmbed") + + parent.querySelector(".lyrics").classList.toggle("showed") +}) + +$(document).on("click", ".musicIcon.remove-icon", (e) => { + e.stopImmediatePropagation() + + let id = e.currentTarget.dataset.id + + let formdata = new FormData() + formdata.append("hash", u("meta[name=csrf]").attr("value")) + + ky.post(`/audio${id}/action?act=remove`, { + hooks: { + beforeRequest: [ + (_request) => { + e.target.classList.add("lagged") + } + ], + afterResponse: [ + async (_request, _options, response) => { + let json = await response.json() + + if(json.success) { + e.target.classList.remove("remove-icon") + e.target.classList.add("add-icon") + e.target.classList.remove("lagged") + + let withd = e.target.closest(".audioEmbed.withdrawn") + + if(withd != null) + u(withd).remove() + } else + fastError(json.flash.message) + } + ] + }, body: formdata + }) +}) + +$(document).on("click", ".musicIcon.remove-icon-group", (e) => { + e.stopImmediatePropagation() + + let id = e.currentTarget.dataset.id + + let formdata = new FormData() + formdata.append("hash", u("meta[name=csrf]").attr("value")) + formdata.append("club", e.currentTarget.dataset.club) + + ky.post(`/audio${id}/action?act=remove_club`, { + hooks: { + beforeRequest: [ + (_request) => { + e.currentTarget.classList.add("lagged") + } + ], + afterResponse: [ + async (_request, _options, response) => { + let json = await response.json() + + if(json.success) + $(e.currentTarget.closest(".audioEmbed")).remove() + else + fastError(json.flash.message) + } + ] + }, body: formdata + }) +}) + +$(document).on("click", ".musicIcon.add-icon-group", async (ev) => { + let body = ` + ${tr("what_club_add")} +
    + + +
    + + ` + MessageBox(tr("add_audio_to_club"), body, [tr("close")], [Function.noop]) + + document.querySelector(".ovk-diag-body").style.padding = "11px" + + if(window.openvk.writeableClubs == null) { + try { + window.openvk.writeableClubs = await API.Groups.getWriteableClubs() + } catch (e) { + document.querySelector(".errorPlace").innerHTML = tr("no_access_clubs") + document.querySelector(".ovk-diag-body input[name='addButton']").classList.add("lagged") + + return + } + } + + window.openvk.writeableClubs.forEach(el => { + document.querySelector("#addIconsWindow").insertAdjacentHTML("beforeend", ` + + `) + }) + + $(".ovk-diag-body").on("click", "input[name='addButton']", (e) => { + $.ajax({ + type: "POST", + url: `/audio${ev.target.dataset.id}/action?act=add_to_club`, + data: { + hash: u("meta[name=csrf]").attr("value"), + club: document.querySelector("#addIconsWindow").value + }, + beforeSend: () => { + e.target.classList.add("lagged") + document.querySelector(".errorPlace").innerHTML = "" + }, + success: (response) => { + if(!response.success) + document.querySelector(".errorPlace").innerHTML = response.flash.message + + e.currentTarget.classList.remove("lagged") + } + }) + }) +}) + +$(document).on("click", ".musicIcon.add-icon", (e) => { + let id = e.currentTarget.dataset.id + + let formdata = new FormData() + formdata.append("hash", u("meta[name=csrf]").attr("value")) + + ky.post(`/audio${id}/action?act=add`, { + hooks: { + beforeRequest: [ + (_request) => { + e.target.classList.add("lagged") + } + ], + afterResponse: [ + async (_request, _options, response) => { + let json = await response.json() + + if(json.success) { + e.target.classList.remove("add-icon") + e.target.classList.add("remove-icon") + e.target.classList.remove("lagged") + } else + fastError(json.flash.message) + } + ] + }, body: formdata + }) +}) + +$(document).on("click", "#_deletePlaylist", (e) => { + let id = e.currentTarget.dataset.id + + MessageBox(tr("warning"), tr("sure_delete_playlist"), [tr("yes"), tr("no")], [() => { + $.ajax({ + type: "POST", + url: `/playlist${id}/action?act=delete`, + data: { + hash: u("meta[name=csrf]").attr("value"), + }, + beforeSend: () => { + e.currentTarget.classList.add("lagged") + }, + success: (response) => { + if(response.success) { + window.location.assign("/playlists" + response.id) + } else { + fastError(response.flash.message) + } + } + }) + }, Function.noop]) +}) + +$(document).on("click", "#_audioAttachment", (e) => { + let form = e.currentTarget.closest("form") + let body = ` + + +
    + ` + MessageBox(tr("select_audio"), body, [tr("ok")], [Function.noop]) + + document.querySelector(".ovk-diag-body").style.padding = "0" + document.querySelector(".ovk-diag-cont").style.width = "580px" + document.querySelector(".ovk-diag-body").style.height = "335px" + + let searcher = new playersSearcher("entity_audios", 0) + searcher.successCallback = (response, thisc) => { + let domparser = new DOMParser() + let result = domparser.parseFromString(response, "text/html") + + let pagesCount = result.querySelector("input[name='pagesCount']").value + let count = Number(result.querySelector("input[name='count']").value) + + if(count < 1) { + document.querySelector(".audiosInsert").innerHTML = thisc.context_type == "entity_audios" ? tr("no_audios_thisuser") : tr("no_results") + return + } + + result.querySelectorAll(".audioEmbed").forEach(el => { + let id = el.dataset.prettyid + let name = el.dataset.name + let isAttached = (form.querySelector("input[name='audios']").value.includes(`${id},`)) + document.querySelector(".audiosInsert").insertAdjacentHTML("beforeend", ` +
    +
    ${el.outerHTML}
    +
    + ${isAttached ? tr("detach_audio") : tr("attach_audio")} +
    +
    + `) + }) + + u("#loader").remove() + + if(thisc.page < pagesCount) { + document.querySelector(".audiosInsert").insertAdjacentHTML("beforeend", ` +
    + ${tr("show_more_audios")} +
    `) + } + } + + searcher.errorCallback = () => { + fastError("Error when loading players.") + } + + searcher.beforesendCallback = () => { + document.querySelector(".audiosInsert").insertAdjacentHTML("beforeend", ``) + } + + searcher.clearContainer = () => { + document.querySelector(".audiosInsert").innerHTML = "" + } + + searcher.movePage(1) + + $(".audiosInsert").on("click", "#showMoreAudios", (e) => { + u(e.currentTarget).remove() + searcher.movePage(Number(e.currentTarget.dataset.page)) + }) + + $(".searchBox input").on("change", async (e) => { + await new Promise(r => setTimeout(r, 500)); + + if(e.currentTarget.value === document.querySelector(".searchBox input").value) { + searcher.clearContainer() + + if(e.currentTarget.value == "") { + searcher.context_type = "entity_audios" + searcher.context_id = 0 + searcher.query = "" + + searcher.movePage(1) + + return + } + + searcher.context_type = "search_context" + searcher.context_id = 0 + searcher.query = e.currentTarget.value + + searcher.movePage(1) + return; + } + }) + + $(".searchBox select").on("change", async (e) => { + searcher.clearContainer() + searcher.searchType = e.currentTarget.value + + $(".searchBox input").trigger("change") + return; + }) + + function insertAttachment(id) { + let audios = form.querySelector("input[name='audios']") + + if(!audios.value.includes(id + ",")) { + if(audios.value.split(",").length > 10) { + NewNotification(tr("error"), tr("max_attached_audios")) + return false + } + + form.querySelector("input[name='audios']").value += (id + ",") + + return true + } else { + form.querySelector("input[name='audios']").value = form.querySelector("input[name='audios']").value.replace(id + ",", "") + + return false + } + } + + $(".audiosInsert").on("click", ".attachAudio", (ev) => { + if(!insertAttachment(ev.currentTarget.dataset.attachmentdata)) { + u(`.post-has-audios .post-has-audio[data-id='${ev.currentTarget.dataset.attachmentdata}']`).remove() + ev.currentTarget.querySelector("span").innerHTML = tr("attach_audio") + } else { + ev.currentTarget.querySelector("span").innerHTML = tr("detach_audio") + + form.querySelector(".post-has-audios").insertAdjacentHTML("beforeend", ` +
    + ${ovk_proc_strtr(escapeHtml(ev.currentTarget.dataset.name), 40)} +
    + `) + + u(`#unattachAudio[data-id='${ev.currentTarget.dataset.attachmentdata}']`).on("click", (e) => { + let id = ev.currentTarget.dataset.attachmentdata + form.querySelector("input[name='audios']").value = form.querySelector("input[name='audios']").value.replace(id + ",", "") + + u(e.currentTarget).remove() + }) + } + }) +}) + +$(document).on("click", ".audioEmbed.processed", (e) => { + MessageBox(tr("error"), tr("audio_embed_processing"), [tr("ok")], [Function.noop]) +}) + +$(document).on("click", ".audioEmbed.withdrawn", (e) => { + MessageBox(tr("error"), tr("audio_embed_withdrawn"), [tr("ok")], [Function.noop]) +}) + +$(document).on("click", ".musicIcon.report-icon", (e) => { + MessageBox(tr("report_question"), ` + ${tr("going_to_report_audio")} +
    ${tr("report_question_text")} +

    ${tr("report_reason")}: `, [tr("confirm_m"), tr("cancel")], [(function() { + + res = document.querySelector("#uReportMsgInput").value; + xhr = new XMLHttpRequest(); + xhr.open("GET", "/report/" + e.target.dataset.id + "?reason=" + res + "&type=audio", true); + xhr.onload = (function() { + if(xhr.responseText.indexOf("reason") === -1) + MessageBox(tr("error"), tr("error_sending_report"), ["OK"], [Function.noop]); + else + MessageBox(tr("action_successfully"), tr("will_be_watched"), ["OK"], [Function.noop]); + }); + xhr.send(null) + }), + + Function.noop]) +}) + +$(document).on("click", ".audiosContainer .paginator a", (e) => { + e.preventDefault() + let url = new URL(e.currentTarget.href) + let page = url.searchParams.get("p") + + function searchNode(id) { + let node = document.querySelector(`.audioEmbed[data-realid='${id}'] .audioEntry`) + + if(node != null) { + node.classList.add("nowPlaying") + } + } + + if(window.savedAudiosPages[page] != null) { + history.pushState({}, "", e.currentTarget.href) + document.querySelector(".audiosContainer").innerHTML = window.savedAudiosPages[page].innerHTML + searchNode(window.player["tracks"].currentTrack != null ? window.player["tracks"].currentTrack.id : 0) + + return + } + + e.currentTarget.parentNode.classList.add("lagged") + $.ajax({ + type: "GET", + url: e.currentTarget.href, + success: (response) => { + let domparser = new DOMParser() + let result = domparser.parseFromString(response, "text/html") + + document.querySelector(".audiosContainer").innerHTML = result.querySelector(".audiosContainer").innerHTML + history.pushState({}, "", e.currentTarget.href) + window.savedAudiosPages[page] = result.querySelector(".audiosContainer") + searchNode(window.player["tracks"].currentTrack != null ? window.player["tracks"].currentTrack.id : 0) + + if(!window.player.context["playedPages"].includes(page)) { + $.ajax({ + type: "POST", + url: "/audios/context", + data: { + context: window.player["context"].context_type, + context_entity: window.player["context"].context_id, + hash: u("meta[name=csrf]").attr("value"), + page: page + }, + success: (response_2) => { + window.player.tracks["tracks"] = window.player.tracks["tracks"].concat(response_2["items"]) + window.player.context["playedPages"].push(String(page)) + console.info("Page is switched") + } + }) + } + } + }) +}) + +$(document).on("click", ".addToPlaylist", (e) => { + let audios = document.querySelector("input[name='audios']") + let id = e.currentTarget.dataset.id + + if(!audios.value.includes(id + ",")) { + document.querySelector("input[name='audios']").value += (id + ",") + e.currentTarget.querySelector("span").innerHTML = tr("remove_from_playlist") + } else { + document.querySelector("input[name='audios']").value = document.querySelector("input[name='audios']").value.replace(id + ",", "") + e.currentTarget.querySelector("span").innerHTML = tr("add_to_playlist") + } +}) + +$(document).on("click", "#bookmarkPlaylist, #unbookmarkPlaylist", (e) => { + let target = e.currentTarget + let id = target.id + + $.ajax({ + type: "POST", + url: `/playlist${e.currentTarget.dataset.id}/action?act=${id == "unbookmarkPlaylist" ? "unbookmark" : "bookmark"}`, + data: { + hash: u("meta[name=csrf]").attr("value"), + }, + beforeSend: () => { + e.currentTarget.classList.add("lagged") + }, + success: (response) => { + if(response.success) { + e.currentTarget.setAttribute("id", id == "unbookmarkPlaylist" ? "bookmarkPlaylist" : "unbookmarkPlaylist") + e.currentTarget.innerHTML = id == "unbookmarkPlaylist" ? tr("bookmark") : tr("unbookmark") + e.currentTarget.classList.remove("lagged") + } else + fastError(response.flash.message) + } + }) +}) diff --git a/Web/static/js/al_playlists.js b/Web/static/js/al_playlists.js new file mode 100644 index 00000000..cf61c45d --- /dev/null +++ b/Web/static/js/al_playlists.js @@ -0,0 +1,113 @@ +let context_type = "entity_audios" +let context_id = 0 + +if(document.querySelector("#editPlaylistForm")) { + context_type = "playlist_context" + context_id = document.querySelector("#editPlaylistForm").dataset.id +} + +if(document.querySelector(".showMoreAudiosPlaylist") && document.querySelector(".showMoreAudiosPlaylist").dataset.club != null) { + context_type = "entity_audios" + context_id = Number(document.querySelector(".showMoreAudiosPlaylist").dataset.club) * -1 +} + +let searcher = new playersSearcher(context_type, context_id) + +searcher.successCallback = (response, thisc) => { + let domparser = new DOMParser() + let result = domparser.parseFromString(response, "text/html") + let pagesCount = Number(result.querySelector("input[name='pagesCount']").value) + let count = Number(result.querySelector("input[name='count']").value) + + result.querySelectorAll(".audioEmbed").forEach(el => { + let id = Number(el.dataset.realid) + let isAttached = (document.querySelector("input[name='audios']").value.includes(`${id},`)) + + document.querySelector(".playlistAudiosContainer").insertAdjacentHTML("beforeend", ` +
    +
    + ${el.outerHTML} +
    +
    + ${isAttached ? tr("remove_from_playlist") : tr("add_to_playlist")} +
    +
    + `) + }) + + if(count < 1) + document.querySelector(".playlistAudiosContainer").insertAdjacentHTML("beforeend", ` + ${tr("no_results")} + `) + + if(Number(thisc.page) >= pagesCount) + u(".showMoreAudiosPlaylist").remove() + else { + if(document.querySelector(".showMoreAudiosPlaylist") != null) { + document.querySelector(".showMoreAudiosPlaylist").setAttribute("data-page", thisc.page + 1) + + if(thisc.query != "") { + document.querySelector(".showMoreAudiosPlaylist").setAttribute("data-query", thisc.query) + } + + document.querySelector(".showMoreAudiosPlaylist").style.display = "block" + } else { + document.querySelector(".playlistAudiosContainer").parentNode.insertAdjacentHTML("beforeend", ` +
    + ${tr("show_more_audios")} +
    + `) + } + } + + u("#loader").remove() +} + +searcher.beforesendCallback = () => { + document.querySelector(".playlistAudiosContainer").parentNode.insertAdjacentHTML("beforeend", ``) + + if(document.querySelector(".showMoreAudiosPlaylist") != null) + document.querySelector(".showMoreAudiosPlaylist").style.display = "none" +} + +searcher.errorCallback = () => { + fastError("Error when loading players") +} + +searcher.clearContainer = () => { + document.querySelector(".playlistAudiosContainer").innerHTML = "" +} + +$(document).on("click", ".showMoreAudiosPlaylist", (e) => { + searcher.movePage(Number(e.currentTarget.dataset.page)) +}) + +$(document).on("change", "input#playlist_query", async (e) => { + e.preventDefault() + + await new Promise(r => setTimeout(r, 500)); + + if(e.currentTarget.value === document.querySelector("input#playlist_query").value) { + searcher.clearContainer() + + if(e.currentTarget.value == "") { + searcher.context_type = "entity_audios" + searcher.context_id = 0 + searcher.query = "" + + searcher.movePage(1) + + return + } + + searcher.context_type = "search_context" + searcher.context_id = 0 + searcher.query = e.currentTarget.value + + searcher.movePage(1) + return; + } +}) diff --git a/Web/static/js/al_wall.js b/Web/static/js/al_wall.js index 39ee2199..5acba57f 100644 --- a/Web/static/js/al_wall.js +++ b/Web/static/js/al_wall.js @@ -155,18 +155,6 @@ function setupWallPostInputHandlers(id) { return; } }); - - u("#wall-post-input" + id).on("input", function(e) { - var boost = 5; - var textArea = e.target; - textArea.style.height = "5px"; - var newHeight = textArea.scrollHeight; - textArea.style.height = newHeight + boost + "px"; - return; - - // 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() @@ -182,6 +170,18 @@ function setupWallPostInputHandlers(id) { }); } +u(document).on("input", "textarea", function(e) { + var boost = 5; + var textArea = e.target; + textArea.style.height = "5px"; + var newHeight = textArea.scrollHeight; + textArea.style.height = newHeight + boost + "px"; + return; + + // revert to original size if it is larger (possibly changed by user) + // textArea.style.height = (newHeight > originalHeight ? (newHeight + boost) : originalHeight) + "px"; +}); + function OpenMiniature(e, photo, post, photo_id, type = "post") { /* костыли но смешные однако diff --git a/Web/static/js/package.json b/Web/static/js/package.json index f9459ce4..f5c37008 100644 --- a/Web/static/js/package.json +++ b/Web/static/js/package.json @@ -2,6 +2,8 @@ "dependencies": { "@atlassian/aui": "^9.6.0", "create-react-class": "^15.7.0", + "dashjs": "^4.3.0", + "id3js": "^2.1.1", "handlebars": "^4.7.7", "jquery": "^3.0.0", "knockout": "^3.5.1", diff --git a/Web/static/js/yarn.lock b/Web/static/js/yarn.lock index fa61ed9b..289fc81e 100644 --- a/Web/static/js/yarn.lock +++ b/Web/static/js/yarn.lock @@ -41,6 +41,11 @@ backbone@1.4.1: dependencies: underscore ">=1.8.3" +codem-isoboxer@0.3.6: + version "0.3.6" + resolved "https://registry.yarnpkg.com/codem-isoboxer/-/codem-isoboxer-0.3.6.tgz#867f670459b881d44f39168d5ff2a8f14c16151d" + integrity sha512-LuO8/7LW6XuR5ERn1yavXAfodGRhuY2yP60JTZIw5yNYMCE5lUVbk3NFUCJxjnphQH+Xemp5hOGb1LgUXm00Xw== + core-js@^1.0.0: version "1.2.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" @@ -64,6 +69,18 @@ dompurify@2.4.5: resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.5.tgz#0e89a27601f0bad978f9a924e7a05d5d2cccdd87" integrity sha512-jggCCd+8Iqp4Tsz0nIvpcb22InKEBrGz5dw3EQJMs8HPJDsKbFIO3STYtAvCfDx26Muevn1MHVI0XxjgFfmiSA== +dashjs@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/dashjs/-/dashjs-4.3.0.tgz#cccda5a490cabf6c3b48aa887ec8c8ac0df1a233" + integrity sha512-cqpnJaPQpEY4DsEdF9prwD00+5dp5EGHCFc7yo9n2uuAH9k4zPkZJwXQ8dXmVRhPf3M89JfKSoAYIP3dbXmqcg== + dependencies: + codem-isoboxer "0.3.6" + es6-promise "^4.2.8" + fast-deep-equal "2.0.1" + html-entities "^1.2.1" + imsc "^1.0.2" + localforage "^1.7.1" + encoding@^0.1.11: version "0.1.13" resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" @@ -71,6 +88,11 @@ encoding@^0.1.11: dependencies: iconv-lite "^0.6.2" +es6-promise@^4.2.8: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + event-lite@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/event-lite/-/event-lite-0.1.2.tgz#838a3e0fdddef8cc90f128006c8e55a4e4e4c11b" @@ -81,6 +103,11 @@ fancy-file-input@2.0.4: resolved "https://registry.yarnpkg.com/fancy-file-input/-/fancy-file-input-2.0.4.tgz#698c216482e07649a827681c4db3054fddc9a32b" integrity sha512-l+J0WwDl4nM/zMJ/C8qleYnXMUJKsLng7c5uWH/miAiHoTvPDtEoLW1tmVO6Cy2O8i/1VfA+2YOwg/Q3+kgO6w== +fast-deep-equal@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" + integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= + fbjs@^0.8.0: version "0.8.17" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd" @@ -94,6 +121,10 @@ fbjs@^0.8.0: setimmediate "^1.0.5" ua-parser-js "^0.7.18" +html-entities@^1.2.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.4.0.tgz#cfbd1b01d2afaf9adca1b10ae7dffab98c71d2dc" + integrity sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA== handlebars@^4.7.7: version "4.7.7" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" @@ -113,11 +144,28 @@ iconv-lite@^0.6.2: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +id3js@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/id3js/-/id3js-2.1.1.tgz#0c307d0d2f194bc5fa7a809bbed0b1a93577f16d" + integrity sha512-9Gi+sG0RHSa5qn8hkwi2KCl+2jV8YrtiZidXbOO3uLfRAxc2jilRg0fiQ3CbeoAmR7G7ap3RVs1kqUVhIyZaog== + ieee754@^1.1.8: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= + +imsc@^1.0.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/imsc/-/imsc-1.1.3.tgz#e96a60a50d4000dd7b44097272768b9fd6a4891d" + integrity sha512-IY0hMkVTNoqoYwKEp5UvNNKp/A5jeJUOrIO7judgOyhHT+xC6PA4VBOMAOhdtAYbMRHx9DTgI8p6Z6jhYQPFDA== + dependencies: + sax "1.2.1" + int64-buffer@^0.1.9: version "0.1.10" resolved "https://registry.yarnpkg.com/int64-buffer/-/int64-buffer-0.1.10.tgz#277b228a87d95ad777d07c13832022406a473423" @@ -173,6 +221,13 @@ ky@^0.19.0: resolved "https://registry.yarnpkg.com/ky/-/ky-0.19.0.tgz#d6ad117e89efe2d85a1c2e91462d48ca1cda1f7a" integrity sha512-RkDgbg5ahMv1MjHfJI2WJA2+Qbxq0iNSLWhreYiCHeHry9Q12sedCnP5KYGPt7sydDvsyH+8UcG6Kanq5mpsyw== +lie@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" + integrity sha1-mkNrLMd0bKWd56QfpGmz77dr2H4= + dependencies: + immediate "~3.0.5" + literallycanvas@^0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/literallycanvas/-/literallycanvas-0.5.2.tgz#7d4800a8d9c4b38a593e91695d52466689586abd" @@ -180,6 +235,13 @@ literallycanvas@^0.5.2: dependencies: react-addons-pure-render-mixin "^15.1" +localforage@^1.7.1: + version "1.10.0" + resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4" + integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg== + dependencies: + lie "3.1.1" + loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -273,6 +335,11 @@ requirejs@^2.3.6: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sax@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" + integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= + setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" diff --git a/bootstrap.php b/bootstrap.php index faa798f6..4b67a0b7 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -64,6 +64,33 @@ function ovk_proc_strtr(string $string, int $length = 0): string return $newString . ($string !== $newString ? "…" : ""); #if cut hasn't happened, don't append "..." } +function knuth_shuffle(iterable $arr, int $seed): array +{ + $data = is_array($arr) ? $arr : iterator_to_array($arr); + $retVal = []; + $ind = []; + $count = sizeof($data); + + srand($seed, MT_RAND_PHP); + + for($i = 0; $i < $count; ++$i) + $ind[$i] = 0; + + for($i = 0; $i < $count; ++$i) { + do { + $index = rand() % $count; + } while($ind[$index] != 0); + + $ind[$index] = 1; + $retVal[$i] = $data[$index]; + } + + # Reseed + srand(hexdec(bin2hex(openssl_random_pseudo_bytes(4)))); + + return $retVal; +} + function bmask(int $input, array $options = []): Bitmask { return new Bitmask($input, $options["length"] ?? 1, $options["mappings"] ?? []); diff --git a/composer.json b/composer.json index c68dd4d1..a248197a 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ "vearutop/php-obscene-censor-rus": "dev-master", "erusev/parsedown": "dev-master", "bhaktaraz/php-rss-generator": "dev-master", + "ext-openssl": "*", "ext-simplexml": "*", "symfony/console": "5.4.x-dev", "wapmorgan/morphos": "dev-master", diff --git a/install/init-static-db.sql b/install/init-static-db.sql index 78b5e64a..7678587e 100644 --- a/install/init-static-db.sql +++ b/install/init-static-db.sql @@ -54,27 +54,6 @@ CREATE TABLE `attachments` ( `index` bigint(20) UNSIGNED NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; -CREATE TABLE `audios` ( - `id` bigint(20) UNSIGNED NOT NULL, - `owner` bigint(20) UNSIGNED NOT NULL, - `virtual_id` bigint(20) UNSIGNED NOT NULL, - `created` bigint(20) UNSIGNED NOT NULL, - `edited` bigint(20) UNSIGNED DEFAULT NULL, - `hash` char(128) COLLATE utf8mb4_unicode_520_ci NOT NULL, - `deleted` tinyint(4) DEFAULT 0, - `name` varchar(190) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '(no name)', - `performer` varchar(190) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT 'Unknown', - `genre` varchar(190) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT 'K-POP', - `lyrics` longtext COLLATE utf8mb4_unicode_520_ci DEFAULT NULL, - `explicit` tinyint(4) NOT NULL DEFAULT 0 -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -CREATE TABLE `audio_relations` ( - `user` bigint(20) UNSIGNED NOT NULL, - `audio` bigint(20) UNSIGNED NOT NULL, - `index` bigint(20) UNSIGNED NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - CREATE TABLE `comments` ( `id` bigint(20) UNSIGNED NOT NULL, `owner` bigint(20) NOT NULL, diff --git a/install/sqls/00041-music.sql b/install/sqls/00041-music.sql new file mode 100644 index 00000000..49edbaf0 --- /dev/null +++ b/install/sqls/00041-music.sql @@ -0,0 +1,100 @@ +-- Apply these two commands if you installed OpenVK before 12th November 2023 OR if it's just doesn't work out of box, then apply this file again +-- DROP TABLE `audios`; +-- DROP TABLE `audio_relations`; + +CREATE TABLE IF NOT EXISTS `audios` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `owner` bigint unsigned NOT NULL, + `virtual_id` bigint unsigned NOT NULL, + `created` bigint unsigned NOT NULL, + `edited` bigint unsigned DEFAULT NULL, + `hash` char(128) NOT NULL, + `length` smallint unsigned NOT NULL, + `segment_size` decimal(20,6) NOT NULL DEFAULT '6.000000' COMMENT 'Size in seconds of each segment', + `kid` binary(16) NOT NULL, + `key` binary(16) NOT NULL, + `token` binary(28) NOT NULL COMMENT 'Key to access original file', + `listens` bigint unsigned NOT NULL DEFAULT '0', + `performer` varchar(256) NOT NULL, + `name` varchar(256) NOT NULL, + `lyrics` text, + `genre` enum('Blues','Big Band','Classic Rock','Chorus','Country','Easy Listening','Dance','Acoustic','Disco','Humour','Funk','Speech','Grunge','Chanson','Hip-Hop','Opera','Jazz','Chamber Music','Metal','Sonata','New Age','Symphony','Oldies','Booty Bass','Other','Primus','Pop','Porn Groove','R&B','Satire','Rap','Slow Jam','Reggae','Club','Rock','Tango','Techno','Samba','Industrial','Folklore','Alternative','Ballad','Ska','Power Ballad','Death Metal','Rhythmic Soul','Pranks','Freestyle','Soundtrack','Duet','Euro-Techno','Punk Rock','Ambient','Drum Solo','Trip-Hop','A Cappella','Vocal','Euro-House','Jazz+Funk','Dance Hall','Fusion','Goa','Trance','Drum & Bass','Classical','Club-House','Instrumental','Hardcore','Acid','Terror','House','Indie','Game','BritPop','Sound Clip','Negerpunk','Gospel','Polsk Punk','Noise','Beat','AlternRock','Christian Gangsta Rap','Bass','Heavy Metal','Soul','Black Metal','Punk','Crossover','Space','Contemporary Christian','Meditative','Christian Rock','Instrumental Pop','Merengue','Instrumental Rock','Salsa','Ethnic','Thrash Metal','Gothic','Anime','Darkwave','JPop','Techno-Industrial','Synthpop','Electronic','Abstract','Pop-Folk','Art Rock','Eurodance','Baroque','Dream','Bhangra','Southern Rock','Big Beat','Comedy','Breakbeat','Cult','Chillout','Gangsta Rap','Downtempo','Top 40','Dub','Christian Rap','EBM','Pop / Funk','Eclectic','Jungle','Electro','Native American','Electroclash','Cabaret','Emo','New Wave','Experimental','Psychedelic','Garage','Rave','Global','Showtunes','IDM','Trailer','Illbient','Lo-Fi','Industro-Goth','Tribal','Jam Band','Acid Punk','Krautrock','Acid Jazz','Leftfield','Polka','Lounge','Retro','Math Rock','Musical','New Romantic','Rock & Roll','Nu-Breakz','Hard Rock','Post-Punk','Folk','Post-Rock','Folk-Rock','Psytrance','National Folk','Shoegaze','Swing','Space Rock','Fast Fusion','Trop Rock','Bebob','World Music','Latin','Neoclassical','Revival','Audiobook','Celtic','Audio Theatre','Bluegrass','Neue Deutsche Welle','Avantgarde','Podcast','Gothic Rock','Indie Rock','Progressive Rock','G-Funk','Psychedelic Rock','Dubstep','Symphonic Rock','Garage Rock','Slow Rock','Psybient','Psychobilly','Touhou') DEFAULT NULL, + `explicit` tinyint(1) NOT NULL DEFAULT '0', + `withdrawn` tinyint(1) NOT NULL DEFAULT '0', + `processed` tinyint unsigned NOT NULL DEFAULT '0', + `checked` bigint NOT NULL DEFAULT '0' COMMENT 'Last time the audio availability was checked', + `unlisted` tinyint(1) NOT NULL DEFAULT '0', + `deleted` tinyint(1) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `owner_virtual_id` (`owner`,`virtual_id`), + KEY `genre` (`genre`), + KEY `unlisted` (`unlisted`), + KEY `listens` (`listens`), + KEY `deleted` (`deleted`), + KEY `length` (`length`), + KEY `listens_genre` (`listens`,`genre`), + FULLTEXT KEY `performer_name` (`performer`,`name`), + FULLTEXT KEY `lyrics` (`lyrics`), + FULLTEXT KEY `performer` (`performer`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +CREATE TABLE IF NOT EXISTS `audio_listens` ( + `entity` bigint NOT NULL, + `audio` bigint unsigned NOT NULL, + `time` bigint unsigned NOT NULL, + `index` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'Workaround for Nette DBE bug', + `playlist` bigint(20) UNSIGNED DEFAULT NULL, + PRIMARY KEY (`index`), + KEY `audio` (`audio`), + KEY `user` (`entity`) USING BTREE, + KEY `user_time` (`entity`,`time`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +CREATE TABLE IF NOT EXISTS `audio_relations` ( + `entity` bigint NOT NULL, + `audio` bigint unsigned NOT NULL, + `index` bigint unsigned NOT NULL AUTO_INCREMENT, + PRIMARY KEY (`index`), + KEY `user` (`entity`) USING BTREE, + KEY `entity_audio` (`entity`,`audio`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +CREATE TABLE IF NOT EXISTS `playlists` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `owner` bigint NOT NULL, + `name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci NOT NULL, + `description` varchar(2048) DEFAULT NULL, + `cover_photo_id` bigint unsigned DEFAULT NULL, + `length` int unsigned NOT NULL DEFAULT '0', + `special_type` tinyint unsigned NOT NULL DEFAULT '0', + `created` bigint unsigned DEFAULT NULL, + `listens` bigint(20) unsigned NOT NULL DEFAULT 0, + `edited` bigint unsigned DEFAULT NULL, + `deleted` tinyint unsigned DEFAULT '0', + PRIMARY KEY (`id`), + KEY `owner_deleted` (`owner`,`deleted`), + FULLTEXT KEY `title_description` (`name`,`description`), + FULLTEXT KEY `title` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +CREATE TABLE IF NOT EXISTS `playlist_imports` ( + `entity` bigint NOT NULL, + `playlist` bigint unsigned NOT NULL, + `index` bigint unsigned NOT NULL AUTO_INCREMENT, + PRIMARY KEY (`index`) USING BTREE, + KEY `user` (`entity`) USING BTREE, + KEY `entity_audio` (`entity`,`playlist`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +CREATE TABLE IF NOT EXISTS `playlist_relations` ( + `collection` bigint unsigned NOT NULL, + `media` bigint unsigned NOT NULL, + `index` bigint unsigned NOT NULL AUTO_INCREMENT, + PRIMARY KEY (`index`) USING BTREE, + KEY `playlist` (`collection`) USING BTREE, + KEY `audio` (`media`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +ALTER TABLE `groups` ADD `everyone_can_upload_audios` TINYINT(1) NOT NULL DEFAULT '0' AFTER `backdrop_2`; +ALTER TABLE `profiles` ADD `last_played_track` BIGINT(20) UNSIGNED NULL DEFAULT NULL AFTER `client_name`, ADD `audio_broadcast_enabled` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `last_played_track`; +ALTER TABLE `groups` ADD `last_played_track` BIGINT(20) UNSIGNED NULL DEFAULT NULL AFTER `everyone_can_upload_audios`, ADD `audio_broadcast_enabled` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `last_played_track`; diff --git a/locales/en.strings b/locales/en.strings index b1d211d1..7fbe255e 100644 --- a/locales/en.strings +++ b/locales/en.strings @@ -479,6 +479,7 @@ "my_videos" = "My Videos"; "my_messages" = "My Messages"; "my_notes" = "My Notes"; +"my_audios" = "My Audios"; "my_groups" = "My Groups"; "my_feed" = "My Feed"; "my_feedback" = "My Feedback"; @@ -568,6 +569,7 @@ "privacy_setting_add_to_friends" = "Who can add me to friends"; "privacy_setting_write_wall" = "Who can publish post on my wall"; "privacy_setting_write_messages" = "Who can write messages to me"; +"privacy_setting_view_audio" = "Who can see my audios"; "privacy_value_anybody" = "Anybody"; "privacy_value_anybody_dative" = "Anybody"; "privacy_value_users" = "OpenVK users"; @@ -713,12 +715,133 @@ "upload_new_video" = "Upload new video"; "max_attached_videos" = "Max is 10 videos"; "max_attached_photos" = "Max is 10 photos"; +"max_attached_audios" = "Max is 10 audios"; "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."; + +/* Audios */ + +"audios" = "Audios"; +"audio" = "Audio"; +"playlist" = "Playlist"; +"upload_audio" = "Upload audio"; +"upload_audio_to_group" = "Upload audio to group"; + +"performer" = "Performer"; +"audio_name" = "Name"; +"genre" = "Genre"; +"lyrics" = "Lyrics"; + +"select_another_file" = "Select another file"; + +"limits" = "Limits"; +"select_audio" = "Select audio from your computer"; +"audio_requirements" = "Audio must be between $1s to $2 minutes, weights to $3MB and contain audio stream."; +"audio_requirements_2" = "Audio must not infringe copyright and related rights"; +"you_can_also_add_audio_using" = "You can also add audio from among the files you have already downloaded using"; +"search_audio_inst" = "audios search"; + +"audio_embed_not_found" = "Audio not found"; +"audio_embed_deleted" = "Audio was deleted"; +"audio_embed_withdrawn" = "The audio was withdrawn at the request of the copyright holder"; +"audio_embed_forbidden" = "The user's privacy settings do not allow this embed this audio"; +"audio_embed_processing" = "Audio is still being processed, or has not been processed correctly."; + +"audios_count_zero" = "No audios"; +"audios_count_one" = "One audio"; +"audios_count_few" = "$1 audios"; +"audios_count_many" = "$1 audios"; +"audios_count_other" = "$1 audios"; + +"track_unknown" = "Unknown"; +"track_noname" = "Without name"; + +"my_music" = "My music"; +"music_user" = "User's music"; +"music_club" = "Club's music"; +"audio_new" = "New"; +"audio_popular" = "Popular"; +"audio_search" = "Search"; + +"my_audios_small" = "My audios"; +"my_playlists" = "My playlists"; +"playlists" = "Playlists"; +"audios_explicit" = "Contains obscene language"; +"withdrawn" = "Withdrawn"; +"deleted" = "Deleted"; +"owner" = "Owner"; +"searchable" = "Searchable"; + +"select_audio" = "Select audios"; +"no_playlists_thisuser" = "You haven't added any playlists yet."; +"no_playlists_user" = "This user has not added any playlists yet."; +"no_playlists_club" = "This group hasn't added playlists yet."; + +"no_audios_thisuser" = "You haven't added any audios yet."; +"no_audios_user" = "This user has not added any audios yet."; +"no_audios_club" = "This group has not added any audios yet."; + +"new_playlist" = "New playlist"; +"created_playlist" = "created"; +"updated_playlist" = "updated"; +"bookmark" = "Add to collection"; +"unbookmark" = "Remove from collection"; +"empty_playlist" = "There are no audios in this playlist."; +"edit_playlist" = "Edit playlist"; +"unable_to_load_queue" = "Error when loading queue."; + +"fully_delete_audio" = "Fully delete audio"; +"attach_audio" = "Attach audio"; +"detach_audio" = "Detach audio"; + +"show_more_audios" = "Show more audios"; +"add_to_playlist" = "Add to playlist"; +"remove_from_playlist" = "Remove from playlist"; +"delete_playlist" = "Delete playlist"; +"playlist_cover" = "Playlist cover"; + +"playlists_user" = "Users playlists"; +"playlists_club" = "Groups playlists"; +"change_cover" = "Change cover"; +"playlist_cover" = "Playlist's cover"; + +"minutes_count_zero" = "lasts no minutes"; +"minutes_count_one" = "lasts one minute"; +"minutes_count_few" = "lasts $1 minutes"; +"minutes_count_many" = "lasts $1 minutes"; +"minutes_count_other" = "lasts $1 minutes"; + +"listens_count_zero" = "no listens"; +"listens_count_one" = "one listen"; +"listens_count_few" = "$1 listens"; +"listens_count_many" = "$1 listens"; +"listens_count_other" = "$1 listens"; + +"add_audio_to_club" = "Add audio to group"; +"what_club_add" = "Which group do you want to add the song to?"; +"group_has_audio" = "This group already has this song."; +"group_hasnt_audio" = "This group doesn't have this song."; + +"by_name" = "by name"; +"by_performer" = "by performer"; +"no_access_clubs" = "There are no groups where you are an administrator."; +"audio_successfully_uploaded" = "Audio has been successfully uploaded and is currently being processed."; + +"broadcast_audio" = "Broadcast audio to status"; +"sure_delete_playlist" = "Do you sure want to delete this playlist?"; +"edit_audio" = "Edit audio"; +"audios_group" = "Audios from group"; +"playlists_group" = "Playlists from group"; + +"play_tip" = "Play/pause"; +"repeat_tip" = "Repeat"; +"shuffle_tip" = "Shuffle"; +"mute_tip" = "Mute"; + /* Notifications */ "feedback" = "Feedback"; @@ -983,6 +1106,7 @@ "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_audio" = "You are about to report this audio."; "going_to_report_post" = "You are about to report this post."; "going_to_report_comment" = "You are about to report this comment."; @@ -1111,6 +1235,7 @@ "created" = "Created"; "everyone_can_create_topics" = "Everyone can create topics"; +"everyone_can_upload_audios" = "Everyone can upload audios"; "display_list_of_topics_above_wall" = "Display a list of topics above the wall"; "topic_changes_saved_comment" = "The updated title and settings will appear on the topic page."; @@ -1287,6 +1412,22 @@ "description_too_long" = "Description is too long."; +"invalid_audio" = "Invalid audio."; +"do_not_have_audio" = "You don't have this audio."; +"do_have_audio" = "You already have this audio."; + +"set_playlist_name" = "Enter the playlist name."; +"playlist_already_bookmarked" = "This playlist is already in your collection."; +"playlist_not_bookmarked" = "This playlist is not in your collection."; +"invalid_cover_photo" = "Error when loading cover photo."; +"not_a_photo" = "Uploaded file doesn't look like a photo."; +"file_too_big" = "File is too big."; +"file_loaded_partially" = "The file has been uploaded partially."; +"file_not_uploaded" = "Failed to upload the file."; +"error_code" = "Error code: $1."; +"ffmpeg_timeout" = "Timed out waiting ffmpeg. Try to upload file again."; +"ffmpeg_not_installed" = "Failed to proccess the file. It looks like ffmpeg is not installed on this server."; + /* Admin actions */ "login_as" = "Login as $1"; @@ -1393,6 +1534,10 @@ "admin_gift_moved_successfully" = "Gift moved successfully"; "admin_gift_moved_to_recycle" = "This gift will now be in Recycle Bin."; +"admin_original_file" = "Original file"; +"admin_audio_length" = "Length"; +"admin_cover_id" = "Cover (photo ID)"; +"admin_music" = "Music"; "logs" = "Logs"; "logs_anything" = "Anything"; @@ -1620,8 +1765,16 @@ "tour_section_5_text_3" = "In addition to uploading videos directly, the site also supports embedding videos from YouTube"; -"tour_section_6_title_1" = "Audios section, which doesn't exist yet xdddd"; -"tour_section_6_text_1" = "I would love to do a tutorial on this section, but sunshine Vriska didn't make the music :c"; +"tour_section_6_title_1" = "Listen to music"; +"tour_section_6_text_1" = "You can listen to music in \"My Audios\""; +"tour_section_6_text_2" = "This section is also controlled by the privacy settings."; +"tour_section_6_text_3" = "The most listened songs are in \"Popular\", and recently uploaded songs are in \"New\""; +"tour_section_6_text_4" = "To add a song to your collection, hover over it and click on the \"plus\". You can search for the song you want."; +"tour_section_6_text_5" = "If you can't find the song you want, you can upload it yourself"; +"tour_section_6_bottom_text_1" = "Important: the song must not infringe copyright"; +"tour_section_6_title_2" = "Create playlists"; +"tour_section_6_text_6" = "You can create playlists in the \"My Playlists\" tab"; +"tour_section_6_text_7" = "You can also add another's playlists to your collection"; "tour_section_7_title_1" = "Follow what your friends write"; @@ -1732,6 +1885,8 @@ "s_order_by_name" = "By name"; "s_order_by_random" = "By random"; "s_order_by_rating" = "By rating"; +"s_order_by_length" = "By length"; +"s_order_by_listens" = "By listens count"; "s_order_invert" = "Invert"; "s_by_date" = "By date"; @@ -1753,6 +1908,8 @@ "deleted_target_comment" = "This comment belongs to deleted post"; "no_results" = "No results"; +"s_only_performers" = "Performers only"; +"s_with_lyrics" = "With lyrics"; /* BadBrowser */ @@ -1792,6 +1949,7 @@ /* Mobile */ "mobile_friends" = "Friends"; "mobile_photos" = "Photos"; +"mobile_audios" = "Audios"; "mobile_videos" = "Videos"; "mobile_messages" = "Messages"; "mobile_notes" = "Notes"; @@ -1805,6 +1963,9 @@ "mobile_user_info_hide" = "Hide"; "mobile_user_info_show_details" = "Show details"; +"my" = "My"; +"enter_a_name_or_artist" = "Enter a name or artist..."; + /* Moderation */ "section" = "Section"; diff --git a/locales/ru.strings b/locales/ru.strings index 49871399..63f40462 100644 --- a/locales/ru.strings +++ b/locales/ru.strings @@ -461,6 +461,7 @@ "my_videos" = "Мои Видеозаписи"; "my_messages" = "Мои Сообщения"; "my_notes" = "Мои Заметки"; +"my_audios" = "Мои Аудиозаписи"; "my_groups" = "Мои Группы"; "my_feed" = "Мои Новости"; "my_feedback" = "Мои Ответы"; @@ -540,6 +541,7 @@ "privacy_setting_add_to_friends" = "Кто может называть меня другом"; "privacy_setting_write_wall" = "Кто может писать у меня на стене"; "privacy_setting_write_messages" = "Кто может писать мне сообщения"; +"privacy_setting_view_audio" = "Кому видно мои аудиозаписи"; "privacy_value_anybody" = "Все желающие"; "privacy_value_anybody_dative" = "Всем желающим"; "privacy_value_users" = "Пользователям OpenVK"; @@ -672,9 +674,128 @@ "upload_new_video" = "Загрузить новое видео"; "max_attached_videos" = "Максимум 10 видеозаписей"; "max_attached_photos" = "Максимум 10 фотографий"; +"max_attached_audios" = "Максимум 10 аудиозаписей"; "no_videos" = "У вас нет видео."; "no_videos_results" = "Нет результатов."; +/* Audios */ + +"audios" = "Аудиозаписи"; +"audio" = "Аудиозапись"; +"playlist" = "Плейлист"; +"upload_audio" = "Загрузить аудио"; +"upload_audio_to_group" = "Загрузить аудио в группу"; + +"performer" = "Исполнитель"; +"audio_name" = "Название"; +"genre" = "Жанр"; +"lyrics" = "Текст"; + +"select_another_file" = "Выбрать другой файл"; + +"limits" = "Ограничения"; +"select_audio" = "Выберите аудиозапись на Вашем компьютере"; +"audio_requirements" = "Аудиозапись должна быть длинной от $1c до $2 минут, весить до $3мб и содержать аудиопоток."; +"audio_requirements_2" = "Аудиозапись не должна нарушать авторские и смежные права."; +"you_can_also_add_audio_using" = "Вы также можете добавить аудиозапись из числа уже загруженных файлов, воспользовавшись"; +"search_audio_inst" = "поиском по аудио"; + +"audio_embed_not_found" = "Аудиозапись не найдена"; +"audio_embed_deleted" = "Аудиозапись была удалена"; +"audio_embed_withdrawn" = "Аудиозапись была изъята по обращению правообладателя."; +"audio_embed_forbidden" = "Настройки приватности пользователя не позволяют встраивать эту композицию"; +"audio_embed_processing" = "Аудио ещё обрабатывается, либо обработалось неправильно."; + +"audios_count_zero" = "Нет аудиозаписей"; +"audios_count_one" = "Одна аудиозапись"; /* сингл */ +"audios_count_few" = "$1 аудиозаписи"; +"audios_count_many" = "$1 аудиозаписей"; +"audios_count_other" = "$1 аудиозаписей"; + +"track_unknown" = "Неизвестен"; +"track_noname" = "Без названия"; + +"my_music" = "Моя музыка"; +"music_user" = "Музыка пользователя"; +"music_club" = "Музыка группы"; +"audio_new" = "Новое"; +"audio_popular" = "Популярное"; +"audio_search" = "Поиск"; + +"my_audios_small" = "Мои аудиозаписи"; +"my_playlists" = "Мои плейлисты"; +"playlists" = "Плейлисты"; +"audios_explicit" = "Содержит нецензурную лексику"; +"withdrawn" = "Изъято"; +"deleted" = "Удалено"; +"owner" = "Владелец"; +"searchable" = "Доступно в поиске"; + +"select_audio" = "Выбрать аудиозаписи"; +"no_playlists_thisuser" = "Вы ещё не добавляли плейлистов."; +"no_playlists_user" = "Этот пользователь ещё не добавлял плейлистов."; +"no_playlists_club" = "Эта группа ещё не добавляла плейлистов."; + +"no_audios_thisuser" = "Вы ещё не добавляли аудиозаписей."; +"no_audios_user" = "Этот пользователь ещё не добавлял аудиозаписей."; +"no_audios_club" = "Эта группа ещё не добавляла аудиозаписей."; + +"new_playlist" = "Новый плейлист"; +"created_playlist" = "создан"; +"updated_playlist" = "обновлён"; +"bookmark" = "Добавить в коллекцию"; +"unbookmark" = "Убрать из коллекции"; +"empty_playlist" = "В этом плейлисте нет аудиозаписей."; +"edit_playlist" = "Редактировать плейлист"; +"unable_to_load_queue" = "Не удалось загрузить очередь."; + +"fully_delete_audio" = "Полностью удалить аудиозапись"; +"attach_audio" = "Прикрепить аудиозапись"; +"detach_audio" = "Открепить аудиозапись"; + +"show_more_audios" = "Показать больше аудиозаписей"; +"add_to_playlist" = "Добавить в плейлист"; +"remove_from_playlist" = "Удалить из плейлиста"; +"delete_playlist" = "Удалить плейлист"; +"playlist_cover" = "Обложка плейлиста"; +"playlists_user" = "Плейлисты польз."; +"playlists_club" = "Плейлисты группы"; +"change_cover" = "Сменить обложку"; +"playlist_cover" = "Обложка плейлиста"; + +"minutes_count_zero" = "длится ноль минут"; +"minutes_count_one" = "длится одну минуту"; +"minutes_count_few" = "длится $1 минуты"; +"minutes_count_many" = "длится $1 минут"; +"minutes_count_other" = "длится $1 минут"; + +"listens_count_zero" = "нет прослушиваний"; +"listens_count_one" = "одно прослушивание"; +"listens_count_few" = "$1 прослушивания"; +"listens_count_many" = "$1 прослушиваний"; +"listens_count_other" = "$1 прослушиваний"; + +"add_audio_to_club" = "Добавить аудио в группу"; +"what_club_add" = "В какую группу вы хотите добавить песню?"; +"group_has_audio" = "У группы уже есть эта песня."; +"group_hasnt_audio" = "У группы нет этой песни."; + +"by_name" = "по композициям"; +"by_performer" = "по исполнителю"; +"no_access_clubs" = "Нет групп, где вы являетесь администратором."; +"audio_successfully_uploaded" = "Аудио успешно загружено и на данный момент обрабатывается."; + +"broadcast_audio" = "Транслировать аудио в статус"; +"sure_delete_playlist" = "Вы действительно хотите удалить этот плейлист?"; +"edit_audio" = "Редактировать аудиозапись"; +"audios_group" = "Аудиозаписи группы"; +"playlists_group" = "Плейлисты группы"; + +"play_tip" = "Проигрывание/пауза"; +"repeat_tip" = "Повторение"; +"shuffle_tip" = "Перемешать"; +"mute_tip" = "Заглушить"; + /* Notifications */ "feedback" = "Ответы"; @@ -915,6 +1036,7 @@ "going_to_report_photo" = "Вы собираетесь пожаловаться на данную фотографию."; "going_to_report_user" = "Вы собираетесь пожаловаться на данного пользователя."; "going_to_report_video" = "Вы собираетесь пожаловаться на данную видеозапись."; +"going_to_report_audio" = "Вы собираетесь пожаловаться на данную аудиозапись."; "going_to_report_post" = "Вы собираетесь пожаловаться на данную запись."; "going_to_report_comment" = "Вы собираетесь пожаловаться на данный комментарий."; @@ -1030,6 +1152,7 @@ "topics_other" = "$1 тем"; "created" = "Создано"; "everyone_can_create_topics" = "Все могут создавать темы"; +"everyone_can_upload_audios" = "Все могут загружать аудиозаписи"; "display_list_of_topics_above_wall" = "Отображать список тем над стеной"; "topic_changes_saved_comment" = "Обновлённый заголовок и настройки появятся на странице с темой."; "failed_to_create_topic" = "Не удалось создать тему"; @@ -1047,6 +1170,7 @@ "no_data" = "Нет данных"; "no_data_description" = "Тут ничего нет... Пока..."; "error" = "Ошибка"; +"error_generic" = "Произошла ошибка общего характера: "; "error_shorturl" = "Данный короткий адрес уже занят."; "error_segmentation" = "Ошибка сегментации"; "error_upload_failed" = "Не удалось загрузить фото"; @@ -1055,6 +1179,8 @@ "error_weak_password" = "Ненадёжный пароль. Пароль должен содержать не менее 8 символов, цифры, прописные и строчные буквы"; "error_shorturl_incorrect" = "Короткий адрес имеет некорректный формат."; "error_repost_fail" = "Не удалось поделиться записью"; + +"error_insufficient_info" = "Вы не указали необходимую информацию."; "error_data_too_big" = "Аттрибут '$1' не может быть длиннее $2 $3"; "forbidden" = "Ошибка доступа"; "unknown_error" = "Неизвестная ошибка"; @@ -1181,6 +1307,21 @@ "group_owner_is_banned" = "Создатель сообщества успешно забанен."; "group_is_banned" = "Сообщество успешно забанено"; "description_too_long" = "Описание слишком длинное."; +"invalid_audio" = "Такой аудиозаписи не существует."; +"do_not_have_audio" = "У вас нет этой аудиозаписи."; +"do_have_audio" = "У вас уже есть эта аудиозапись."; + +"set_playlist_name" = "Укажите название плейлиста."; +"playlist_already_bookmarked" = "Плейлист уже есть в вашей коллекции."; +"playlist_not_bookmarked" = "Плейлиста нет в вашей коллекции."; +"invalid_cover_photo" = "Не удалось сохранить обложку плейлиста."; +"not_a_photo" = "Загруженный файл не похож на фотографию."; +"file_too_big" = "Файл слишком большой."; +"file_loaded_partially" = "Файл загрузился частично."; +"file_not_uploaded" = "Не удалось загрузить файл."; +"error_code" = "Код ошибки: $1."; +"ffmpeg_timeout" = "Превышено время ожидания обработки ffmpeg. Попробуйте загрузить файл снова."; +"ffmpeg_not_installed" = "Не удалось обработать файл. Похоже, на сервере не установлен ffmpeg."; /* Admin actions */ @@ -1277,6 +1418,10 @@ "admin_gift_moved_successfully" = "Подарок успешно перемещён"; "admin_gift_moved_to_recycle" = "Теперь подарок находится в корзине."; +"admin_original_file" = "Оригинальный файл"; +"admin_audio_length" = "Длина"; +"admin_cover_id" = "Обложка (ID фото)"; +"admin_music" = "Музыка"; "logs" = "Логи"; "logs_anything" = "Любое"; @@ -1508,8 +1653,16 @@ "tour_section_5_text_3" = "Кроме загрузки видео напрямую, сайт поддерживает и встраивание видео из YouTube"; -"tour_section_6_title_1" = "Аудиозаписи, которых пока что нет XD"; -"tour_section_6_text_1" = "Я был бы очень рад сделать туториал по этому разделу, но солнышко Вриска не сделала музыку"; +"tour_section_6_title_1" = "Слушайте аудиозаписи"; +"tour_section_6_text_1" = "Вы можете слушать аудиозаписи в разделе \"Мои Аудиозаписи\""; +"tour_section_6_text_2" = "Этот раздел также регулируется настройками приватности"; +"tour_section_6_text_3" = "Самые прослушиваемые песни находятся во вкладке \"Популярное\", а недавно загруженные — во вкладке \"Новое\""; +"tour_section_6_text_4" = "Чтобы добавить песню в свою коллекцию, наведите на неё и нажмите на плюс. Найти нужную песню можно в поиске"; +"tour_section_6_text_5" = "Если вы не можете найти нужную песню, вы можете загрузить её самостоятельно"; +"tour_section_6_bottom_text_1" = "Важно: песня не должна нарушать авторские права"; +"tour_section_6_title_2" = "Создавайте плейлисты"; +"tour_section_6_text_6" = "Вы можете создавать сборники треков во вкладке \"Мои плейлисты\""; +"tour_section_6_text_7" = "Можно также добавлять чужие плейлисты в свою коллекцию"; "tour_section_7_title_1" = "Следите за тем, что пишут ваши друзья"; @@ -1620,6 +1773,8 @@ "s_order_by_name" = "По имени"; "s_order_by_random" = "По случайности"; "s_order_by_rating" = "По рейтингу"; +"s_order_by_length" = "По длине"; +"s_order_by_listens" = "По числу прослушиваний"; "s_order_invert" = "Инвертировать"; "s_by_date" = "По дате"; @@ -1641,6 +1796,8 @@ "deleted_target_comment" = "Этот комментарий принадлежит к удалённой записи"; "no_results" = "Результатов нет"; +"s_only_performers" = "Только исполнители"; +"s_with_lyrics" = "С текстом"; /* BadBrowser */ @@ -1680,6 +1837,7 @@ /* Mobile */ "mobile_friends" = "Друзья"; "mobile_photos" = "Фотографии"; +"mobile_audios" = "Аудиозаписи"; "mobile_videos" = "Видеозаписи"; "mobile_messages" = "Сообщения"; "mobile_notes" = "Заметки"; @@ -1693,6 +1851,9 @@ "mobile_user_info_hide" = "Скрыть"; "mobile_user_info_show_details" = "Показать подробнее"; +"my" = "Мои"; +"enter_a_name_or_artist" = "Введите название или автора..."; + /* Moderation */ "section" = "Раздел"; diff --git a/openvk-example.yml b/openvk-example.yml index 4b45e7ba..308610e2 100644 --- a/openvk-example.yml +++ b/openvk-example.yml @@ -50,6 +50,8 @@ openvk: - "Good luck filling! If you are a regular support agent, inform the administrator that he forgot to fill the config" messages: strict: false + music: + exposeOriginalURLs: true wall: christian: false anonymousPosting: diff --git a/themepacks/midnight/stylesheet.css b/themepacks/midnight/stylesheet.css index 634917c6..73030b40 100644 --- a/themepacks/midnight/stylesheet.css +++ b/themepacks/midnight/stylesheet.css @@ -235,10 +235,133 @@ input[type="radio"] { } .searchList #used { - background: linear-gradient(#453e5e,#473f61); + background: #463f60 !important; } #backdropEditor { background-image: url("/themepack/midnight/0.0.2.8/resource/backdrop-editor.gif") !important; border-color: #473e66 !important; -} \ No newline at end of file +} + +.bigPlayer { + background-color: rgb(30, 26, 43) !important; +} + +.bigPlayer .selectableTrack, .audioEmbed .track > .selectableTrack { + border-top: #b9b9b9 1px solid !important; +} + +.bigPlayer .paddingLayer .slider, .audioEmbed .track .slider { + background: #b9b9b9 !important; +} + +.musicIcon { + filter: invert(81%) !important; +} + +.audioEntry.nowPlaying { + background: #463f60 !important; + border: 1px solid #645a86 !important; +} + +.preformer { + color: #b7b7b7 !important; +} + +.bigPlayer .paddingLayer .trackPanel .track .timeTip { + background: #b9b9b9 !important; + color: black !important; +} + +.audioEntry.nowPlaying:hover { + background: #50486f !important; +} + +.audioEntry:hover { + background: #19142D !important; +} + +.audioEntry .performer a { + color: #a2a1a1 !important; +} + +.musicIcon.lagged { + opacity: 49%; +} + +.bigPlayer .paddingLayer .bigPlayerTip { + color: black !important; +} + +.searchList a { + color: #bbb !important; +} + +.searchList a:hover { + color: #eeeeee !important; + background: #332d46 !important; +} + +.friendsAudiosList .elem:hover { + background: #332d46 !important; +} + +.audioEntry .playerButton .playIcon { + filter: invert(81%); +} + +img[src$='/assets/packages/static/openvk/img/camera_200.png'], img[src$='/assets/packages/static/openvk/img/song.jpg'] { + filter: invert(100%); +} + +.audioStatus { + color: #8E8E8E !important; +} + +.audioEntry .withLyrics { + color: #6f6497 !important; +} + +#listensCount { + color: unset !important; +} + +#upload_container, .whiteBox { + background: #1d1928 !important; + border: 1px solid #383052 !important; +} + +ul { + color: #8b9ab5 !important; +} + +#audio_upload { + border: 2px solid #383052 !important; + background-color: #262133 !important; +} + +/* вот бы css в овк был бы написан на var()'ах( */ +#upload_container.uploading { + background: #121017 url('/assets/packages/static/openvk/img/progressbar.gif') !important; +} + +.musicIcon.pressed { + opacity: 41% !important; +} + +.ovk-diag-body .searchBox { + background: #1e1a2b !important; +} + +.audioEntry.nowPlaying .title { + color: #fff !important; +} + +.attachAudio:hover { + background: #19142D !important; + cursor: pointer; +} + +.showMore, .showMoreAudiosPlaylist { + background: #181826 !important; +} diff --git a/themepacks/openvk_modern/stylesheet.css b/themepacks/openvk_modern/stylesheet.css index 178e8094..841e92ec 100644 --- a/themepacks/openvk_modern/stylesheet.css +++ b/themepacks/openvk_modern/stylesheet.css @@ -279,18 +279,9 @@ input[type=checkbox] { box-shadow: none; } -.searchList #used -{ - margin-left:0px; - color: white; - padding: 2px; - padding-top: 5px; - padding-bottom: 5px; - border: none; - background: #a4a4a4; - margin-bottom: 2px; - padding-left: 5px; - width: 90%; +.searchList #used { + background: #3c3c3c !important; + border: unset !important; } .searchList #used a @@ -337,3 +328,35 @@ input[type=checkbox] { { border-top: 1px solid #2f2f2f; } + +.musicIcon { + filter: contrast(202%) !important; +} + +.audioEntry .playerButton .playIcon { + filter: contrast(7) !important; +} + +.audioEmbed .track > .selectableTrack, .bigPlayer .selectableTrack { + border-top: #404040 1px solid !important; +} + +.bigPlayer .paddingLayer .slider, .audioEmbed .track .slider { + background: #3c3c3c !important; +} + +.audioEntry.nowPlaying { + background: #4b4b4b !important; +} + +.audioEntry.nowPlaying:hover { + background: #373737 !important; +} + +.musicIcon.pressed { + filter: brightness(150%) !important; +} + +.musicIcon.lagged { + opacity: 50%; +} From eb64376c3a02356c771280353c486e789521afe7 Mon Sep 17 00:00:00 2001 From: DeathPleiad <43928323+Parad1seF0x@users.noreply.github.com> Date: Sun, 12 Nov 2023 03:14:01 +0300 Subject: [PATCH 070/231] Add -vn flag to FFMPEG scripts (#1020) --- Web/Models/shell/processAudio.ps1 | 2 +- Web/Models/shell/processAudio.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Web/Models/shell/processAudio.ps1 b/Web/Models/shell/processAudio.ps1 index 206e11e1..f60a9aed 100644 --- a/Web/Models/shell/processAudio.ps1 +++ b/Web/Models/shell/processAudio.ps1 @@ -23,7 +23,7 @@ Set-Location -Path $temp Move-Item $filename $audioFile ffmpeg -i $audioFile -f dash -encryption_scheme cenc-aes-ctr -encryption_key $key ` - -encryption_kid $keyID -map 0:a -c:a aac -ar 44100 -seg_duration $seg ` + -encryption_kid $keyID -map 0:a -vn -c:a aac -ar 44100 -seg_duration $seg ` -use_timeline 1 -use_template 1 -init_seg_name ($fileHash + '_fragments/0_0.$ext$') ` -media_seg_name ($fileHash + '_fragments/chunk$Number%06d$_$RepresentationID$.$ext$') -adaptation_sets 'id=0,streams=a' ` "$fileHash.mpd" diff --git a/Web/Models/shell/processAudio.sh b/Web/Models/shell/processAudio.sh index ab5a5c55..fa8346e0 100644 --- a/Web/Models/shell/processAudio.sh +++ b/Web/Models/shell/processAudio.sh @@ -19,7 +19,7 @@ cd "$temp" mv "$filename" "$audioFile" ffmpeg -i "$audioFile" -f dash -encryption_scheme cenc-aes-ctr -encryption_key "$key" \ - -encryption_kid "$keyID" -map 0 -c:a aac -ar 44100 -seg_duration "$seg" \ + -encryption_kid "$keyID" -map 0 -vn -c:a aac -ar 44100 -seg_duration "$seg" \ -use_timeline 1 -use_template 1 -init_seg_name "$fileHash"_fragments/0_0."\$ext\$" \ -media_seg_name "$fileHash"_fragments/chunk"\$Number"%06d\$_"\$RepresentationID\$"."\$ext\$" -adaptation_sets 'id=0,streams=a' \ "$fileHash.mpd" From f65d790654bb716af1d74b38031781701327d5e1 Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Sun, 12 Nov 2023 12:58:04 +0300 Subject: [PATCH 071/231] Set context timeout to 20s and maybe fix broadcas t list --- Web/Models/Entities/User.php | 2 +- Web/static/js/al_music.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Web/Models/Entities/User.php b/Web/Models/Entities/User.php index 291dfb6a..4ff3236b 100644 --- a/Web/Models/Entities/User.php +++ b/Web/Models/Entities/User.php @@ -1279,7 +1279,7 @@ class User extends RowModel foreach($entityIds as $id) { $entit = $id > 0 ? (new Users)->get($id) : (new Clubs)->get(abs($id)); - if($id > 0 && $entit->isDeleted()) return; + if($id > 0 && $entit->isDeleted()) continue; $returnArr[] = $entit; } diff --git a/Web/static/js/al_music.js b/Web/static/js/al_music.js index fbbe7c82..85952a00 100644 --- a/Web/static/js/al_music.js +++ b/Web/static/js/al_music.js @@ -141,7 +141,8 @@ class bigPlayer { } ] }, - body: formdata + body: formdata, + timeout: 20000, }) u(this.nodes["playButtons"].querySelector(".playButton")).on("click", (e) => { From 5bb6e097fb0c03117817657d53b96edc739aafae Mon Sep 17 00:00:00 2001 From: Evgeniy Khramov <65224669+thejenja@users.noreply.github.com> Date: Sun, 12 Nov 2023 17:04:13 +0500 Subject: [PATCH 072/231] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81=20=D0=BA=D1=80?= =?UTF-8?q?=D1=83=D0=B3=D0=BB=D1=8B=D1=85=20=D0=B0=D0=B2=D0=B0=D1=82=D0=B0?= =?UTF-8?q?=D1=80=D0=BE=D0=BA=20=D0=B2=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA?= =?UTF-8?q?=D0=B5=20=D0=BC=D1=83=D0=B7=D1=8B=D0=BA=D0=B8=20=D0=B4=D1=80?= =?UTF-8?q?=D1=83=D0=B7=D0=B5=D0=B9=20(#1024)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Web/static/css/avatar.2.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Web/static/css/avatar.2.css b/Web/static/css/avatar.2.css index 2990eb3d..42daa14d 100644 --- a/Web/static/css/avatar.2.css +++ b/Web/static/css/avatar.2.css @@ -81,3 +81,10 @@ div.ovk-video > div > img object-fit: cover; border-radius: 100px; } + +.friendsAudiosList .elem img { + width: 30px; + border-radius: 100px; + height: 31px; + min-width: 30px; +} From 1632d54d52065681d1f2e822ce4ad9132d1bc737 Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Sun, 12 Nov 2023 15:46:00 +0300 Subject: [PATCH 073/231] ya eblan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Исправлен server error из-за списка друзей. Он подгружает абсолютно все группы и всех друзей, на которые подписан человек и из-за этого серверу очень плохо. Забыл, бывает. Теперь он подгружает только 10 такого --- Web/Models/Entities/User.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Web/Models/Entities/User.php b/Web/Models/Entities/User.php index 4ff3236b..66b93713 100644 --- a/Web/Models/Entities/User.php +++ b/Web/Models/Entities/User.php @@ -1267,6 +1267,8 @@ class User extends RowModel $entityIds[] = $_rel->model == "openvk\\Web\\Models\\Entities\\Club" ? $_rel->target * -1 : $_rel->target; } + $entityIds = array_slice($entityIds, 0, 10); + if($shuffle) { $shuffleSeed = openssl_random_pseudo_bytes(6); $shuffleSeed = hexdec(bin2hex($shuffleSeed)); From 08499cd3b4dd3ff575e6819fd8931d20f6647f7a Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Sun, 12 Nov 2023 17:02:33 +0300 Subject: [PATCH 074/231] Some broadcast list fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - При скроллинге вниз на странице с аудио вкладки не идут за вами (баговано) - Теперь перемешка списка друзей на странице аудио должна работать нормально --- Web/Models/Entities/User.php | 4 ++-- Web/static/js/al_music.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Web/Models/Entities/User.php b/Web/Models/Entities/User.php index 66b93713..40ef7aef 100644 --- a/Web/Models/Entities/User.php +++ b/Web/Models/Entities/User.php @@ -1267,14 +1267,14 @@ class User extends RowModel $entityIds[] = $_rel->model == "openvk\\Web\\Models\\Entities\\Club" ? $_rel->target * -1 : $_rel->target; } - $entityIds = array_slice($entityIds, 0, 10); - if($shuffle) { $shuffleSeed = openssl_random_pseudo_bytes(6); $shuffleSeed = hexdec(bin2hex($shuffleSeed)); $entityIds = knuth_shuffle($entityIds, $shuffleSeed); } + + $entityIds = array_slice($entityIds, 0, 10); $returnArr = []; diff --git a/Web/static/js/al_music.js b/Web/static/js/al_music.js index 85952a00..691b6edb 100644 --- a/Web/static/js/al_music.js +++ b/Web/static/js/al_music.js @@ -651,10 +651,10 @@ document.addEventListener("DOMContentLoaded", function() { entries.forEach(x => { if(x.isIntersecting) { document.querySelector('.bigPlayer').classList.remove("floating") - document.querySelector('.searchOptions .searchList').classList.remove("floating") + //document.querySelector('.searchOptions .searchList').classList.remove("floating") document.querySelector('.bigPlayerDetector').style.marginTop = "0px" } else { - document.querySelector('.searchOptions .searchList').classList.add("floating") + //document.querySelector('.searchOptions .searchList').classList.add("floating") document.querySelector('.bigPlayer').classList.add("floating") document.querySelector('.bigPlayerDetector').style.marginTop = "46px" } From b7160d78a04c53f0dca5d231a89f12b7c10fa34f Mon Sep 17 00:00:00 2001 From: veselcraft Date: Sun, 12 Nov 2023 23:00:19 +0300 Subject: [PATCH 075/231] Global: Gender -> Sex --- locales/en.strings | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/en.strings b/locales/en.strings index 7fbe255e..53385ff9 100644 --- a/locales/en.strings +++ b/locales/en.strings @@ -72,7 +72,7 @@ "change_status" = "change status"; "name" = "Name"; "surname" = "Surname"; -"gender" = "Gender"; +"gender" = "Sex"; "male" = "male"; "female" = "female"; "description" = "Description"; @@ -1457,7 +1457,7 @@ "admin_verification" = "Verification"; "admin_banreason" = "Ban reason"; "admin_banned" = "banned"; -"admin_gender" = "Gender"; +"admin_gender" = "Sex"; "admin_registrationdate" = "Registration date"; "admin_actions" = "Actions"; "admin_image" = "Image"; From 784b19aaf76708e7bb39b4ec8a83e29892ba2477 Mon Sep 17 00:00:00 2001 From: veselcraft Date: Sun, 12 Nov 2023 23:07:02 +0300 Subject: [PATCH 076/231] VKAPI: add smth idk --- VKAPI/Handlers/Users.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/VKAPI/Handlers/Users.php b/VKAPI/Handlers/Users.php index 68bf828f..6eccab1b 100644 --- a/VKAPI/Handlers/Users.php +++ b/VKAPI/Handlers/Users.php @@ -165,6 +165,18 @@ final class Users extends VKAPIRequestHandler case "interests": $response[$i]->interests = $usr->getInterests(); break; + case "quotes": + $response[$i]->interests = $usr->getFavoriteQuote(); + break; + case "email": + $response[$i]->interests = $usr->getEmail(); + break; + case "telegram": + $response[$i]->interests = $usr->getTelegram(); + break; + case "about": + $response[$i]->interests = $usr->getDescription(); + break; case "rating": $response[$i]->rating = $usr->getRating(); break; From 626b5b49bb9169e024f9e9584c6c49692c61aab2 Mon Sep 17 00:00:00 2001 From: veselcraft Date: Mon, 13 Nov 2023 12:36:46 +0300 Subject: [PATCH 077/231] VKAPI: fix php 8 issue when DELETED appears in empty friend list --- VKAPI/Handlers/Users.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/VKAPI/Handlers/Users.php b/VKAPI/Handlers/Users.php index 6eccab1b..65b7c191 100644 --- a/VKAPI/Handlers/Users.php +++ b/VKAPI/Handlers/Users.php @@ -12,6 +12,9 @@ final class Users extends VKAPIRequestHandler $users = new UsersRepo; if($user_ids == "0") $user_ids = (string) $authuser->getId(); + + if($user_ids == "") + return array(); $usrs = explode(',', $user_ids); $response = array(); From d183b1a8a3df8bb14f490b5473353443b4348ee3 Mon Sep 17 00:00:00 2001 From: veselcraft Date: Mon, 13 Nov 2023 12:37:15 +0300 Subject: [PATCH 078/231] compatibility fixxxxxx --- VKAPI/Handlers/Friends.php | 6 +++++- VKAPI/Handlers/Wall.php | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/VKAPI/Handlers/Friends.php b/VKAPI/Handlers/Friends.php index 56de3294..59cf5679 100644 --- a/VKAPI/Handlers/Friends.php +++ b/VKAPI/Handlers/Friends.php @@ -4,7 +4,7 @@ use openvk\Web\Models\Repositories\Users as UsersRepo; final class Friends extends VKAPIRequestHandler { - function get(int $user_id, string $fields = "", int $offset = 0, int $count = 100): object + function get(int $user_id = 0, string $fields = "", int $offset = 0, int $count = 100): object { $i = 0; $offset++; @@ -14,6 +14,10 @@ final class Friends extends VKAPIRequestHandler $this->requireUser(); + if ($user_id == 0) { + $user_id = $this->getUser()->getId(); + } + if (is_null($users->get($user_id))) { $this->fail(100, "One of the parameters specified was missing or invalid"); } diff --git a/VKAPI/Handlers/Wall.php b/VKAPI/Handlers/Wall.php index 693ca3cb..add2159b 100644 --- a/VKAPI/Handlers/Wall.php +++ b/VKAPI/Handlers/Wall.php @@ -802,7 +802,7 @@ final class Wall extends VKAPIRequestHandler return [ "type" => "photo", "photo" => [ - "album_id" => $attachment->getAlbum() ? $attachment->getAlbum()->getId() : NULL, + "album_id" => $attachment->getAlbum() ? $attachment->getAlbum()->getId() : 0, "date" => $attachment->getPublicationTime()->timestamp(), "id" => $attachment->getVirtualId(), "owner_id" => $attachment->getOwner()->getId(), From 63702d44d1587d85049cb334bb8564c81e1b9b70 Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Mon, 13 Nov 2023 15:08:18 +0300 Subject: [PATCH 079/231] Something related with audios MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Теперь аудиозаписи в wall.get,getById,getComments,getComment выглядит нормально - Теперь при создании плейлиста можно выбрать до тысячи песен - По идее, название трека теперь нормально обрезается и раскрывается при наведении - Добавлена проверка на существование коммента в wall.getComment - Плейлисты теперь не вылетают, если пользователь не залогинен. --- VKAPI/Handlers/Wall.php | 23 +++++++++++++++++---- Web/Presenters/AudioPresenter.php | 8 +++---- Web/Presenters/templates/Audio/Playlist.xml | 2 +- Web/Presenters/templates/Audio/player.xml | 8 +++---- Web/static/js/al_music.js | 2 +- 5 files changed, 29 insertions(+), 14 deletions(-) diff --git a/VKAPI/Handlers/Wall.php b/VKAPI/Handlers/Wall.php index add2159b..179d5e0e 100644 --- a/VKAPI/Handlers/Wall.php +++ b/VKAPI/Handlers/Wall.php @@ -60,7 +60,10 @@ final class Wall extends VKAPIRequestHandler } else if ($attachment instanceof \openvk\Web\Models\Entities\Note) { $attachments[] = $attachment->toVkApiStruct(); } else if ($attachment instanceof \openvk\Web\Models\Entities\Audio) { - $attachments[] = $attachment->toVkApiStruct($this->getUser()); + $attachments[] = [ + "type" => "audio", + "audio" => $attachment->toVkApiStruct($this->getUser()), + ]; } else if ($attachment instanceof \openvk\Web\Models\Entities\Post) { $repostAttachments = []; @@ -237,7 +240,10 @@ final class Wall extends VKAPIRequestHandler } else if ($attachment instanceof \openvk\Web\Models\Entities\Note) { $attachments[] = $attachment->toVkApiStruct(); } else if ($attachment instanceof \openvk\Web\Models\Entities\Audio) { - $attachments[] = $attachment->toVkApiStruct($this->getUser()); + $attachments[] = [ + "type" => "audio", + "audio" => $attachment->toVkApiStruct($this->getUser()) + ]; } else if ($attachment instanceof \openvk\Web\Models\Entities\Post) { $repostAttachments = []; @@ -576,7 +582,10 @@ final class Wall extends VKAPIRequestHandler } elseif($attachment instanceof \openvk\Web\Models\Entities\Note) { $attachments[] = $attachment->toVkApiStruct(); } elseif($attachment instanceof \openvk\Web\Models\Entities\Audio) { - $attachments[] = $attachment->toVkApiStruct($this->getUser()); + $attachments[] = [ + "type" => "audio", + "audio" => $attachment->toVkApiStruct($this->getUser()), + ]; } } @@ -636,6 +645,9 @@ final class Wall extends VKAPIRequestHandler $comment = (new CommentsRepo)->get($comment_id); # один хуй айди всех комментов общий + if(!$comment || $comment->isDeleted()) + $this->fail(100, "Invalid comment"); + $profiles = []; $attachments = []; @@ -644,7 +656,10 @@ final class Wall extends VKAPIRequestHandler if($attachment instanceof \openvk\Web\Models\Entities\Photo) { $attachments[] = $this->getApiPhoto($attachment); } elseif($attachment instanceof \openvk\Web\Models\Entities\Audio) { - $attachments[] = $attachment->toVkApiStruct($this->getUser()); + $attachments[] = [ + "type" => "audio", + "audio" => $attachment->toVkApiStruct($this->getUser()), + ]; } } diff --git a/Web/Presenters/AudioPresenter.php b/Web/Presenters/AudioPresenter.php index 3bae710c..8ec17012 100644 --- a/Web/Presenters/AudioPresenter.php +++ b/Web/Presenters/AudioPresenter.php @@ -304,7 +304,7 @@ final class AudioPresenter extends OpenVKPresenter if ($_SERVER["REQUEST_METHOD"] === "POST") { $title = $this->postParam("title"); $description = $this->postParam("description"); - $audios = !empty($this->postParam("audios")) ? array_slice(explode(",", $this->postParam("audios")), 0, 100) : []; + $audios = !empty($this->postParam("audios")) ? array_slice(explode(",", $this->postParam("audios")), 0, 1000) : []; if(empty($title) || iconv_strlen($title) < 1) $this->flashFail("err", tr("error"), tr("set_playlist_name")); @@ -478,9 +478,9 @@ final class AudioPresenter extends OpenVKPresenter $this->template->audios = iterator_to_array($playlist->fetch($page, 10)); $this->template->ownerId = $owner_id; $this->template->owner = $playlist->getOwner(); - $this->template->isBookmarked = $playlist->isBookmarkedBy($this->user->identity); - $this->template->isMy = $playlist->getOwner()->getId() === $this->user->id; - $this->template->canEdit = $playlist->canBeModifiedBy($this->user->identity); + $this->template->isBookmarked = $this->user->identity && $playlist->isBookmarkedBy($this->user->identity); + $this->template->isMy = $this->user->identity && $playlist->getOwner()->getId() === $this->user->id; + $this->template->canEdit = $this->user->identity && $playlist->canBeModifiedBy($this->user->identity); } function renderAction(int $audio_id): void diff --git a/Web/Presenters/templates/Audio/Playlist.xml b/Web/Presenters/templates/Audio/Playlist.xml index f14b6ac1..175dfc3a 100644 --- a/Web/Presenters/templates/Audio/Playlist.xml +++ b/Web/Presenters/templates/Audio/Playlist.xml @@ -39,7 +39,7 @@ {_playlist_cover} -