From 14d5caaf9f5e0a4517bda5ba71165890b087c8a3 Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Thu, 14 Sep 2023 20:36:29 +0300 Subject: [PATCH] Photos: AJAX support (#980) * aj * Drag'n'drop * add good view --- Web/Presenters/PhotosPresenter.php | 94 ++++++--- Web/Presenters/templates/Photos/EditAlbum.xml | 9 + .../templates/Photos/UploadPhoto.xml | 77 +++++--- Web/static/css/main.css | 98 +++++++++- Web/static/js/al_photos.js | 179 ++++++++++++++++++ locales/en.strings | 21 ++ locales/ru.strings | 21 ++ 7 files changed, 442 insertions(+), 57 deletions(-) create mode 100644 Web/static/js/al_photos.js diff --git a/Web/Presenters/PhotosPresenter.php b/Web/Presenters/PhotosPresenter.php index 9d64ba20..11026d00 100644 --- a/Web/Presenters/PhotosPresenter.php +++ b/Web/Presenters/PhotosPresenter.php @@ -27,7 +27,7 @@ final class PhotosPresenter extends OpenVKPresenter if(!$user) $this->notFound(); if (!$user->getPrivacyPermission('photos.read', $this->user->identity ?? NULL)) $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); - $this->template->albums = $this->albums->getUserAlbums($user, $this->queryParam("p") ?? 1); + $this->template->albums = $this->albums->getUserAlbums($user, (int)($this->queryParam("p") ?? 1)); $this->template->count = $this->albums->getUserAlbumsCount($user); $this->template->owner = $user; $this->template->canEdit = false; @@ -36,7 +36,7 @@ final class PhotosPresenter extends OpenVKPresenter } else { $club = (new Clubs)->get(abs($owner)); if(!$club) $this->notFound(); - $this->template->albums = $this->albums->getClubAlbums($club, $this->queryParam("p") ?? 1); + $this->template->albums = $this->albums->getClubAlbums($club, (int)($this->queryParam("p") ?? 1)); $this->template->count = $this->albums->getClubAlbumsCount($club); $this->template->owner = $club; $this->template->canEdit = false; @@ -46,7 +46,7 @@ final class PhotosPresenter extends OpenVKPresenter $this->template->paginatorConf = (object) [ "count" => $this->template->count, - "page" => $this->queryParam("p") ?? 1, + "page" => (int)($this->queryParam("p") ?? 1), "amount" => NULL, "perPage" => OPENVK_DEFAULT_PER_PAGE, ]; @@ -147,7 +147,7 @@ final class PhotosPresenter extends OpenVKPresenter $this->template->photos = iterator_to_array( $album->getPhotos( (int) ($this->queryParam("p") ?? 1), 20) ); $this->template->paginatorConf = (object) [ "count" => $album->getPhotosCount(), - "page" => $this->queryParam("p") ?? 1, + "page" => (int)($this->queryParam("p") ?? 1), "amount" => sizeof($this->template->photos), "perPage" => 20, "atBottom" => true @@ -221,39 +221,74 @@ final class PhotosPresenter extends OpenVKPresenter function renderUploadPhoto(): void { $this->assertUserLoggedIn(); - $this->willExecuteWriteAction(); + $this->willExecuteWriteAction(true); if(is_null($this->queryParam("album"))) - $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в DELETED."); + $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в DELETED.", 500, true); [$owner, $id] = explode("_", $this->queryParam("album")); $album = $this->albums->get((int) $id); if(!$album) - $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в DELETED."); + $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в DELETED.", 500, true); if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity)) - $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); + $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса.", 500, true); if($_SERVER["REQUEST_METHOD"] === "POST") { - if(!isset($_FILES["blob"])) - $this->flashFail("err", "Нету фотографии", "Выберите файл."); - - try { - $photo = new Photo; - $photo->setOwner($this->user->id); - $photo->setDescription($this->postParam("desc")); - $photo->setFile($_FILES["blob"]); - $photo->setCreated(time()); - $photo->save(); - } catch(ISE $ex) { - $name = $album->getName(); - $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в $name."); - } - - $album->addPhoto($photo); - $album->setEdited(time()); - $album->save(); + if($this->queryParam("act") == "finish") { + $result = json_decode($this->postParam("photos"), true); + + foreach($result as $photoId => $description) { + $phot = $this->photos->get($photoId); - $this->redirect("/photo" . $photo->getPrettyId() . "?from=album" . $album->getId()); + if(!$phot || $phot->isDeleted() || $phot->getOwner()->getId() != $this->user->id) + continue; + + if(iconv_strlen($description) > 255) + $this->flashFail("err", tr("error"), tr("description_too_long"), 500, true); + + $phot->setDescription($description); + $phot->save(); + + $album = $phot->getAlbum(); + } + + $this->returnJson(["success" => true, + "album" => $album->getId(), + "owner" => $album->getOwner() instanceof User ? $album->getOwner()->getId() : $album->getOwner()->getId() * -1]); + } + + if(!isset($_FILES)) + $this->flashFail("err", "Нету фотографии", "Выберите файл.", 500, true); + + $photos = []; + for($i = 0; $i < $this->postParam("count"); $i++) { + try { + $photo = new Photo; + $photo->setOwner($this->user->id); + $photo->setDescription(""); + $photo->setFile($_FILES["photo_".$i]); + $photo->setCreated(time()); + $photo->save(); + + $photos[] = [ + "url" => $photo->getURLBySizeId("tiny"), + "id" => $photo->getId(), + "vid" => $photo->getVirtualId(), + "owner" => $photo->getOwner()->getId(), + "link" => $photo->getURL() + ]; + } catch(ISE $ex) { + $name = $album->getName(); + $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в $name.", 500, true); + } + + $album->addPhoto($photo); + $album->setEdited(time()); + $album->save(); + } + + $this->returnJson(["success" => true, + "photos" => $photos]); } else { $this->template->album = $album; } @@ -285,7 +320,7 @@ final class PhotosPresenter extends OpenVKPresenter function renderDeletePhoto(int $ownerId, int $photoId): void { $this->assertUserLoggedIn(); - $this->willExecuteWriteAction(); + $this->willExecuteWriteAction($_SERVER["REQUEST_METHOD"] === "POST"); $this->assertNoCSRF(); $photo = $this->photos->getByOwnerAndVID($ownerId, $photoId); @@ -298,6 +333,9 @@ final class PhotosPresenter extends OpenVKPresenter $photo->isolate(); $photo->delete(); + if($_SERVER["REQUEST_METHOD"] === "POST") + $this->returnJson(["success" => true]); + $this->flash("succ", "Фотография удалена", "Эта фотография была успешно удалена."); $this->redirect($redirect); } diff --git a/Web/Presenters/templates/Photos/EditAlbum.xml b/Web/Presenters/templates/Photos/EditAlbum.xml index 271d1034..a10b5c7f 100644 --- a/Web/Presenters/templates/Photos/EditAlbum.xml +++ b/Web/Presenters/templates/Photos/EditAlbum.xml @@ -14,6 +14,15 @@ {/block} {block content} +
+
+ {_edit_album} +
+
+ {_add_photos} +
+
+
diff --git a/Web/Presenters/templates/Photos/UploadPhoto.xml b/Web/Presenters/templates/Photos/UploadPhoto.xml index 9876e5b9..6ea987cb 100644 --- a/Web/Presenters/templates/Photos/UploadPhoto.xml +++ b/Web/Presenters/templates/Photos/UploadPhoto.xml @@ -12,32 +12,53 @@ {/block} {block content} - -
- - - - - - - - - - - - - - -
{_description}:
{_photo}: - -
-
- - -
- - -
+
+
+ {_edit_album} +
+
+ {_add_photos} +
+
+ + + +
+
+
+
+

{_uploading_photos_from_computer}

+ +
+ {_admin_limits} +
    +
  • {_supported_formats}
  • +
  • {_max_load_photos}
  • +
+ +
+ +
+ +
+ {_tip}: {_tip_ctrl} +
+
+
+
+ +
+ + +
+ + + + +{/block} + +{block bodyScripts} + {script "js/al_photos.js"} {/block} diff --git a/Web/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 */