diff --git a/src/admin/gallery.php b/src/admin/gallery.php index c65fea5..77442a6 100644 --- a/src/admin/gallery.php +++ b/src/admin/gallery.php @@ -16,7 +16,6 @@ function gallery_app_init($hook) { $gallery_app_dir = plugin_dir_url(__FILE__) . 'gallery'; - /* * Dependencies */ diff --git a/src/admin/gallery/html.php b/src/admin/gallery/html.php index f614ed4..59c9462 100644 --- a/src/admin/gallery/html.php +++ b/src/admin/gallery/html.php @@ -6,14 +6,26 @@ function admin_gallery_html($args) {
${message}
`; + + document.querySelector('#notice-container').appendChild(element); + + jQuery(document).trigger('wp-updates-notice-added'); +} + +function createSuccessNotice(message) { + return createNotice("success", message); +} + +function createErrorNotice(message) { + return createNotice("error", message); +} + class ResizableBox { constructor(parent, bounding, resolution, aspectRatio = null) { @@ -206,8 +225,8 @@ class ResizableBox { const rect = getRelativeClientRect(this.element); return new DOMRect( - Math.floor(rect.x * this.scale.x), - Math.floor(rect.y * this.scale.y), + Math.floor((rect.x - this.bounding.x) * this.scale.x), + Math.floor((rect.y - this.bounding.y) * this.scale.y), Math.floor(rect.width * this.scale.x), Math.floor(rect.height * this.scale.y) ); @@ -316,9 +335,13 @@ class Editor { this.aspectRatio = aspectRatio; this._imgElement = undefined; + + this._img_id = null; } - loadImage(url) { + loadImage(img_id, url) { + this.img_id = img_id; + this._imgElement = this._editor.querySelector(".editor-image"); this._imgElement.src = url; @@ -343,24 +366,268 @@ class Editor { console.log(region) - fetch(wpAPISettings.root + 'theatergf/gallery/v1/crop/new', { + fetch(wpAPISettings.root + 'theatergf/gallery/v1/images/new', { method: 'POST', headers: { 'X-WP-Nonce': wpAPISettings.nonce, 'Content-Type': 'application/json' }, body: JSON.stringify({ - img_id: 53, + img_id: this.img_id, x: region.x, y: region.y, width: region.width, height: region.height }) - }).then((response) => response.json().then((json) => console.log(json))).catch((error) => console.log(error)); + }).then((response) => { + + response.json().then((json) => { + if (response.ok) { + console.log("response:", json) + + createSuccessNotice("Successfully created"); + } else { + createErrorNotice(`Failed to create image: ${json?.error ?? "Unknown Error"}`); + } + }).catch((error) => { + createErrorNotice(`Failed to parse response: ${error}`); + }) + + }).catch((error) => { + createErrorNotice(`Failed to connect to backend: ${error}`); + }); } } +class GalleryItemElement extends HTMLElement { + + static get observedAttributes() { + return ["src", "selected"]; + } + + get imageId() { return this.getAttribute("image-id"); } + set imageId(value) { this.setAttribute("image-id", value); } + + get src() { return this.getAttribute("src"); } + set src(value) { this.setAttribute("src", value); } + + get selected() { return this.getAttribute("selected"); } + set selected(value) { this.setAttribute("selected", value); } + + constructor() { + super(); + + const shadow = this.attachShadow({ mode: "open" }); + + // Container + this.container = document.createElement("div"); + this.container.classList.add("image-container"); + + this.numbering = document.createElement("div"); + this.numbering.classList.add("numbering"); + + // Spinner + this.spinner = document.createElement("span"); + this.spinner.classList.add("spinner", "is-active"); + + // Image + this.image = document.createElement("img"); + + this.image.addEventListener("load", () => { + this.spinner.classList.remove("is-active"); + }); + + this.image.addEventListener("error", () => { + this.spinner.classList.remove("is-active"); + }); + + this.container.appendChild(this.spinner); + this.container.appendChild(this.numbering); + this.container.appendChild(this.image); + + // Styles + const style = document.createElement("style"); + style.textContent = ` + .image-container { + width: 100px; + height: 100px; + position: relative; + border: black solid 1px; + box-sizing: border-box; + } + + .image-container.selected { + border: blue solid 3px; + } + + img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + } + + .numbering { + position: absolute; + right: -3px; + bottom: -3px; + + width: 1.5em; + height: 1.5em; + + text-align: center; + line-height: 1.5em; + + background-color: blue; + color: white; + + visibility: hidden; + } + .image-container.selected .numbering { + visibility: visible; + } + + .spinner { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + } + + .spinner:not(.is-active) { + display: none; + } + `; + + shadow.appendChild(style); + shadow.appendChild(this.container); + } + + connectedCallback() { + this.updateImage(); + this.updateSelected(); + } + + attributeChangedCallback(name, oldValue, newValue) { + if (oldValue === newValue) return; + + if (name === "src") { + this.updateImage(); + } + + if (name === "selected") { + this.updateSelected(); + } + } + + updateImage() { + const src = this.getAttribute("src"); + + if (!src) return; + + this.image.src = src; + this.spinner.classList.add("is-active"); + } + + updateSelected() { + if (this.hasAttribute("selected") && this.getAttribute("selected")) { + this.container.classList.add("selected"); + this.numbering.innerHTML = this.getAttribute("selected"); + } else { + this.container.classList.remove("selected"); + } + } + +} + +customElements.define("ttgf-gallery-item", GalleryItemElement) + +/*function create_gallery_element(image_src) { + const element = document.createElement("div"); + element.classList.add("image-container") + + const spinner = document.createElement("span"); + spinner.classList.add("spinner", "is-active"); + element.appendChild(spinner); + + const image_element = new Image(); + + image_element.addEventListener('load', (_) => { + spinner.classList.remove("is-active") + }) + image_element.addEventListener('error', (_) => { + spinner.classList.remove("is-active") + }) + + image_element.src = image_src + element.appendChild(image_element) + + return element; +}*/ + +function create_gallery_element(id, image_src, selected) { + const e = document.createElement("ttgf-gallery-item"); + e.imageId = id; + e.src = image_src; + e.selected = selected; + + return e; +} + +async function load_gallery() { + + const spinner = document.querySelector('.gallery-container .spinner'); + spinner.classList.add("is-active"); + + const gallery = document.querySelector('.gallery'); + + try { + const response = await fetch(wpAPISettings.root + 'theatergf/gallery/v1/images/all', { + method: 'GET', + headers: { + 'X-WP-Nonce': wpAPISettings.nonce + }, + }) + + const json = await response.json() + console.log(json); + + if (!Array.isArray(json)) { + throw TypeError(`Did not receive an array of posts as a result! Got ${typeof json}`) + } + + if (json.length == 0) { + gallery.innerHTML = "No images yet
"; + return; + } + + json.forEach((v) => { + const item = create_gallery_element(v["ID"], v["thumbnail_src"], v["selected"]) + item.addEventListener("click", (e) => { + selected_items = Array.from(gallery.querySelectorAll("ttgf-gallery-item")).filter((v) => v.selected > 0) + if (e.target.selected) { + + selected_items.filter((v) => v.selected > e.target.selected).forEach((v) => v.selected = (Number(v.selected)-1).toString()) + + e.target.selected = "" + } else { + selected_items.forEach((v) => v.selected = (Number(v.selected)+1).toString()); + e.target.selected = "1" + } + }) + gallery.appendChild(item) + }) + + + } catch (e) { + createErrorNotice(`Failed to load available images: ${e}`) + + gallery.innerHTML = "Error loading images
"; + } finally { + spinner.classList.remove("is-active"); + } +} jQuery(document).ready( ($) => { let wp_file_selector_frame = null; @@ -388,7 +655,7 @@ jQuery(document).ready( ($) => { console.log(file) - editor.loadImage(file.url); + editor.loadImage(file.id, file.url); }); wp_file_selector_frame.open(); @@ -398,5 +665,48 @@ jQuery(document).ready( ($) => { event.preventDefault(); editor.save(); + + load_gallery(); }) + + $("#theatergf-save-selected").on('click', (event) => { + event.preventDefault(); + + console.log("click"); + + const gallery = document.querySelector('.gallery'); + + const items = gallery.querySelectorAll("ttgf-gallery-item"); + const payload = Array.from(items, (v) => ({ ID: v.imageId, selected: v.selected })) + + fetch(wpAPISettings.root + 'theatergf/gallery/v1/images/selected/set', { + method: 'POST', + headers: { + 'X-WP-Nonce': wpAPISettings.nonce, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + imgs: payload + }) + }).then((response) => { + + response.json().then((json) => { + if (response.ok) { + console.log("response:", json) + + createSuccessNotice("Saved."); + } else { + createErrorNotice(`Failed to save selected image: ${json?.code ?? "Unknown Error"}`); + } + }).catch((error) => { + createErrorNotice(`Failed to parse response: ${error}`); + }) + + }).catch((error) => { + createErrorNotice(`Failed to connect to backend: ${error}`); + }); + + }) + + load_gallery(); }) diff --git a/src/admin/gallery/style.css b/src/admin/gallery/style.css index c23ef11..f0778ee 100644 --- a/src/admin/gallery/style.css +++ b/src/admin/gallery/style.css @@ -1,6 +1,90 @@ +#theatergf-editor-status { + .hidden { + display: none; + } +} + .theatergf { + .gallery-container { + width: 75%; + + display: flex; + flex-direction: column; + align-content: center; + justify-content: center; + + .gallery { + border: black 1px solid; + + display: flex; + flex-direction: row; + align-content: top; + justify-content: left; + flex-wrap: wrap; + gap: 5px; + margin: 5px; + padding: 5px; + + p { + width: 100%; + text-align: center; + } + + .image-container { + width: 100px; + height: 100px; + + position: relative; + + border: black solid 1px; + + &.selected { + border: blue solid 3px; + } + + img { + width: 100%; + height: 100%; + } + + .numbering { + position: absolute; + right: 0px; + bottom: 0px; + + width: 1em; + height: 1em; + + background-color: blue; + color: white; + } + + .spinner { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + } + .spinner:not(.is-active) { + display: none; + } + } + } + + .spinner-container { + width: 100%; + display: block; + } + .spinner { + display: block; + float: none; + + margin: auto; + } + } + .image-editor { aspect-ratio: 2 / 1; width: 75%; diff --git a/src/backend/backend.php b/src/backend/backend.php index 0a485c4..9db131e 100644 --- a/src/backend/backend.php +++ b/src/backend/backend.php @@ -7,7 +7,35 @@ require_once __DIR__ . '/endpoints/crop.php'; add_action( 'rest_api_init', function () { $namespace = 'theatergf/gallery/v1'; - $crop_controller = new Rest\CROP_Endpoints($namespace, 'crop'); + $crop_controller = new Rest\CROP_Endpoints($namespace, 'images'); $crop_controller->register_routes(); }); +add_action( 'delete_attachment', function ( $post_id ) { + $selected = get_post_meta($post_id, '_ttgf_gallery_selected', true); + + error_log("deleting: " . $post_id . ", selected: " . $selected); + + if ( (! $selected) || empty($selected) ) { + return; + } + + $posts = get_posts(array( + 'numberposts' => -1, + 'post_type' => "attachment", + 'meta_key' => "_ttgf_gallery_selected", + 'meta_value' => (int)$selected, + 'meta_type' => 'NUMERIC', + 'meta_compare' => '>' + )); + + foreach ( $posts as $post ) { + $s = get_post_meta($post->ID, '_ttgf_gallery_selected', true); + + if ( (! $selected) || empty($selected) ) { + continue; + } + + update_post_meta($post->ID, '_ttgf_gallery_selected', ((int)$s - 1)); + } +}); diff --git a/src/backend/endpoints/crop.php b/src/backend/endpoints/crop.php index d10e692..5ca3089 100644 --- a/src/backend/endpoints/crop.php +++ b/src/backend/endpoints/crop.php @@ -2,7 +2,15 @@ namespace TheaterGF\Gallery\Backend\Rest; -class CROP_Endpoints extends \WP_REST_Controller { +require_once ABSPATH . "wp-content/plugins/theatergf-core/src/media.php"; + +use \WP_Error; +use \WP_REST_Controller; +use \WP_REST_Server; + +use function \TheaterGF\Core\get_media_manager; + +class CROP_Endpoints extends WP_REST_Controller { public function __construct( $namespace, $base_path ) { $this->namespace = $namespace; @@ -11,53 +19,183 @@ class CROP_Endpoints extends \WP_REST_Controller { public function register_routes() { + register_rest_route($this->namespace, '/' . $this->rest_base . '/all', [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'get_items' ], + 'permission_callback' => [ $this, 'get_items_permissions_check'], + 'args' => [] + ]); + + register_rest_route($this->namespace, '/' . $this->rest_base . '/selected/set', [ + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => [ $this, 'set_selected' ], + 'permission_callback' => [ $this, 'create_item_permissions_check' ], + 'args' => [ + 'imgs' => [ + 'required' => true, + 'validate_callback' => function ( $param, $request, $key) { + if ( ! is_array($param) ) { + return false; + } + foreach ( $param as $img ) { + if ( ! (wp_attachment_is_image($img['ID']) && (is_numeric($img['selected']) || empty($img['selected']))) ) { + return false; + } + } + } + ] + ] + ]); + register_rest_route($this->namespace, '/' . $this->rest_base . '/new', [ - 'methods' => \WP_REST_Server::CREATABLE, - 'callback' => [ $this, 'create_item' ], - 'permission_callback' => [ $this, 'create_item_permissions_check' ], + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => [ $this, 'create_item' ], + 'permission_callback' => [ $this, 'create_item_permissions_check' ], 'args' => [ 'img_id' => [ - 'required' => true, + 'required' => true, 'validate_callback' => function ( $param, $request, $key) { return wp_attachment_is_image($param); } ], 'x' => [ - 'required' => true, + 'required' => true, 'validate_callback' => function ( $param, $request, $key) { return is_numeric($param) && $param >= 0; } ], 'y' => [ - 'required' => true, + 'required' => true, 'validate_callback' => function ( $param, $request, $key) { return is_numeric($param) && $param >= 0; } ], 'width' => [ - 'required' => true, + 'required' => true, 'validate_callback' => function ( $param, $request, $key) { return is_numeric($param) && $param > 0; } ], 'height' => [ - 'required' => true, + 'required' => true, 'validate_callback' => function ( $param, $request, $key) { return is_numeric($param) && $param > 0; } ] ] ]); } + public function get_items_permissions_check( $request ) { + return $this->editable_permission_check($request); + } + public function create_item_permissions_check( $request ) { + return $this->editable_permission_check($request); + } - if ( ! is_user_logged_in()) { - return new \WP_Error( 'unauthenticated', 'Log in to interact with this endpoint.', array( 'status' => 401 )); + + public function get_items( $request ) { + + $posts = get_posts(array( + 'numberposts' => -1, + 'post_type' => "attachment", + 'meta_key' => "_ttgf_is_gallery_crop", + 'meta_value' => true + )); + + $return_data = []; + + foreach ($posts as $post) { + $thumbnail_src = wp_get_attachment_image_src($post->ID, "thumbnail"); + if ( is_wp_error($thumbnail_src) ) { + error_log($thumbnail_src->get_error_message()); + continue; + } + $image_src = wp_get_attachment_image_src($post->ID, "full"); + if ( is_wp_error($image_src) ) { + error_log($image_src->get_error_message()); + continue; + } + + $selected = get_post_meta($post->ID, '_ttgf_gallery_selected', true); + + $return_data[] = [ + "post" => $post, + "ID" => $post->ID, + "thumbnail_src" => $thumbnail_src[0], + "image_src" => $image_src[0], + "selected" => $selected + ]; } - if ( ! (is_user_logged_in() && current_user_can( 'edit_pages' )) ) { - return new \WP_Error( 'forbidden', 'No Permission for this endpoint.', array( 'status' => 403 )); + return $return_data; + } + + public function set_selected( $request ) { + $params = $request->get_json_params(); + + foreach ( $params["imgs"] as $img ) { + update_post_meta($img["ID"], '_ttgf_gallery_selected', $img["selected"]); } - return true; + return $params; } public function create_item( $request ) { - //$image = get_attached_file() + $params = $request->get_json_params(); + + $image_name = basename(wp_get_original_image_path($params['img_id'])); + + // load wp_crop_image + require_once ABSPATH . 'wp-admin/includes/image.php'; + + $cropped = wp_crop_image( + $params["img_id"], // id of base image + $params["x"], // coordinates + $params["y"], + $params["width"], // size of rectangle in source image + $params["height"], + $params["width"], // size of destination image (resized) + $params["height"], + false, // coordinates are relative to image + \TheaterGF\Gallery\get_uploads_dir() . "/cropped-" . time() . "-" . $image_name + ); + + if ( is_wp_error($cropped) ) { + return $cropped; + } + + $attachment_data = [ + 'guid' => \TheaterGF\Gallery\generate_guid($cropped), + 'post_mime_type' => wp_get_image_mime($cropped), + 'post_title' => basename($cropped), + /*// optional + 'post_excerpt' => $caption, + 'post_content' => $description*/ + ]; + $new_attachment_id = wp_insert_attachment($attachment_data, $cropped, 0, true); + + if ( is_wp_error($new_attachment_id) ) { + return $new_attachment_id; + } + + $metadata = wp_generate_attachment_metadata($new_attachment_id, $cropped); + wp_update_attachment_metadata($new_attachment_id, $metadata); + + update_post_meta($new_attachment_id, '_ttgf_is_gallery_crop', true); + update_post_meta($new_attachment_id, '_ttgf_original_img_id', $params["img_id"]); + + $tags = get_media_manager()->tags->get_from_post($params["img_id"]); + + get_media_manager()->post_add_tags($new_attachment_id, $tags); + get_media_manager()->post_mark_generated($new_attachment_id); + + return [ "params" => $params, "image" => get_post_meta($new_attachment_id) ]; + } + + + protected function editable_permission_check( $request ) { + + if ( ! is_user_logged_in()) { + return new WP_Error( 'unauthenticated', 'Log in to interact with this endpoint.', array( 'status' => 401 )); + } + + if ( ! (is_user_logged_in() && current_user_can( 'edit_pages' )) ) { + return new WP_Error( 'forbidden', 'No Permission for this endpoint.', array( 'status' => 403 )); + } return true; } - } diff --git a/src/backend/endpoints/gallery.php b/src/backend/endpoints/gallery.php new file mode 100644 index 0000000..95a2ec8 --- /dev/null +++ b/src/backend/endpoints/gallery.php @@ -0,0 +1,46 @@ +namespace = $namespace; + $this->rest_base = $base_path; + } + + + public function register_routes() { + register_rest_route($this->namespace, '/' . $this->rest_base, [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'get_items' ], + 'permission_callback' => [ $this, 'get_items_permissions_check'], + 'args' => [] + ]); + } + + public function get_items_permissions_check( $request ) { + return true; + } + + public function get_items( $request ) { + $posts = get_posts(array( + 'numberposts' => -1, + 'post_type' => "attachment", + 'meta_key' => "_ttgf_gallery_selected", + )); + + $data = [] + + foreach ( $posts as $post ) { + $data[] = wp_get_attachment_image_src($post->ID, "full"); + } + + return data; + } + +} diff --git a/src/backend/util.php b/src/backend/util.php new file mode 100644 index 0000000..b09e8ad --- /dev/null +++ b/src/backend/util.php @@ -0,0 +1,18 @@ +