From 50f8b7f7ae3f4f14b8d28b13eeae8233cac173ef Mon Sep 17 00:00:00 2001 From: CJ <72030708+cj12312021@users.noreply.github.com> Date: Mon, 20 Oct 2025 23:39:07 -0500 Subject: [PATCH] Secondary Performer Image Plugin (#576) --- plugins/SecondaryPerformerImage/README.md | 12 ++++++++++++ .../SecondaryPerformerImage.css | 1 + .../SecondaryPerformerImage.js | 1 + .../SecondaryPerformerImage.yml | 16 ++++++++++++++++ 4 files changed, 30 insertions(+) create mode 100644 plugins/SecondaryPerformerImage/README.md create mode 100644 plugins/SecondaryPerformerImage/SecondaryPerformerImage.css create mode 100644 plugins/SecondaryPerformerImage/SecondaryPerformerImage.js create mode 100644 plugins/SecondaryPerformerImage/SecondaryPerformerImage.yml diff --git a/plugins/SecondaryPerformerImage/README.md b/plugins/SecondaryPerformerImage/README.md new file mode 100644 index 0000000..acecdf4 --- /dev/null +++ b/plugins/SecondaryPerformerImage/README.md @@ -0,0 +1,12 @@ +# Secondary Performer Images + +This plugin adds support for a secondary performer image on the performer details page, enabling more customization. A new `Set secondary image...` button will be available on the performer edit page to provide this new image. Performers without a secondary image will continue to behave as they've always done. + +## Modes +The plugin offers three different display modes that can be set for the use of the secondary image. The modes range from 0-2, with 0 being the default mode. Any values that fall outside this range will be processed as the default mode. + +0 - Specify 0 to enable the secondary image to be used in the expanded view. You might consider using this mode if your performer image currently consists of headshots and you want to provide fuller body images for the expanded view. + +1 - Specify 1 to enable the secondary image to be used in the collapsed view. You might consider using this mode if your performer image currently consists of body shots and you want to provide headshots for the collapsed view. + +2 - Specify 2 to enable a button to be used to flip between the primary and secondary image. This mode gives you the option to provide whatever combination of images you want to use together. Some might consider using this option to provide front and back images of the performer. diff --git a/plugins/SecondaryPerformerImage/SecondaryPerformerImage.css b/plugins/SecondaryPerformerImage/SecondaryPerformerImage.css new file mode 100644 index 0000000..28348c2 --- /dev/null +++ b/plugins/SecondaryPerformerImage/SecondaryPerformerImage.css @@ -0,0 +1 @@ +#performer-page .detail-header:not(.edit) .perf-images{position:relative;z-index:1}#performer-page .detail-header:not(.edit) .perf-images button.btn.btn-link{padding:0;position:relative;transition:all .3s;z-index:1}#performer-page .detail-header:not(.edit) .perf-images .active button{z-index:2}#performer-page .detail-header:not(.edit) .perf-images .inactive img{display:none}#performer-page .detail-header:not(.edit) .perf-images .inactive .detail-header-image{padding:0}#performer-page .detail-header:not(.edit) .perf-images .inactive button{opacity:.5;padding:0;transform:rotateY(180deg)}#performer-page .detail-header:not(.edit) .perf-images button.flip{align-items:center;border-radius:50%;bottom:-5px;display:flex;font-size:20px;height:40px;justify-content:center;padding:0;position:absolute;right:-5px;width:40px;z-index:2}#performer-page:has(button.flip) .perf-images{display:flex;float:left;height:auto;justify-content:center}.edit .secondary-image{display:none}.secondary-image-popover{padding:5px}.secondary-image-popover .secondary-image-thumbnail{max-width:22rem;object-fit:cover;object-position:top}.performer-head .custom-fields .detail-item.alt-image{display:none} diff --git a/plugins/SecondaryPerformerImage/SecondaryPerformerImage.js b/plugins/SecondaryPerformerImage/SecondaryPerformerImage.js new file mode 100644 index 0000000..58e4cf2 --- /dev/null +++ b/plugins/SecondaryPerformerImage/SecondaryPerformerImage.js @@ -0,0 +1 @@ +(()=>{"use strict";var e={264:(e,t,a)=>{a.r(t)},577:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0});const{PluginApi:a}=window,{React:n}=a,r=(e,t)=>{const a=new FileReader;a.onloadend=()=>{a.error||t(a.result)},a.readAsDataURL(e)},o={onImageChange:(e,t)=>{var a,n;const o=null===(n=null===(a=null==e?void 0:e.currentTarget)||void 0===a?void 0:a.files)||void 0===n?void 0:n[0];o&&r(o,t)},usePasteImage:(e,t=!0)=>{const a=n.useCallback((t=>{e(t)}),[e]);return n.useEffect((()=>{const e=e=>((e,t)=>{var a;const n=null===(a=null==e?void 0:e.clipboardData)||void 0===a?void 0:a.files;if(!(null==n?void 0:n.length))return;const o=n[0];r(o,t)})(e,a);return t&&document.addEventListener("paste",e),()=>document.removeEventListener("paste",e)}),[t,a]),!1},imageToDataURL:async e=>{const t=await fetch(e),a=await t.blob();return new Promise(((e,t)=>{const n=new FileReader;n.onloadend=()=>{e(n.result)},n.onerror=t,n.readAsDataURL(a)}))}};t.default=o},604:function(e,t,a){var n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.handlePerformerHeaderImagePatch=function(){o.patch.instead("PerformerHeaderImage",(function(e,t,a){var n,r;const{encodingImage:s,collapsed:u,activeImage:m,lightboxImages:d,performer:g}=e,{Button:v}=o.libraries.Bootstrap,{Icon:f}=o.components,{faRefresh:p}=o.libraries.FontAwesomeSolid,[y,h]=l.useState(!0),E=i.useConfigurationQuery(),[b]=c.usePerformerUpdate();void 0===(null===(n=g.custom_fields)||void 0===n?void 0:n.alt_image)&&b({variables:{input:{id:g.id,custom_fields:{full:{alt_image:""}}}}});const I=a({...e});return!E.loading&&(null===(r=g.custom_fields)||void 0===r?void 0:r.alt_image)?l.createElement(l.Fragment,null,function(){var e,t;const n=a({encodingImage:s,activeImage:null===(e=g.custom_fields)||void 0===e?void 0:e.alt_image,lightboxImages:d,performer:g}),r=E.data.configuration.plugins.SecondaryPerformerImage;let o=null!==(t=null==r?void 0:r.imageMode)&&void 0!==t?t:0;if((o<0||o>2)&&(o=0),2==o)return l.createElement("div",{className:"perf-images"},l.createElement("div",{className:"primary-image "+(y?"active":"inactive")},I),l.createElement("div",{className:"secondary-image "+(y?"inactive":"active")},n),l.createElement(v,{className:"flip",onClick:()=>h(!y)},l.createElement(f,{icon:p})));{let e=0==o?u:!u;return l.createElement("div",{className:"perf-images"},l.createElement("div",{className:"primary-image "+(e?"active":"inactive")},I),l.createElement("div",{className:"secondary-image "+(e?"inactive":"active")},n))}}()):l.createElement(l.Fragment,null,I)})),o.patch.instead("ImageInput",(function(e,t,a){var n;const{isEditing:o,text:i,onImageChange:c,onImageURL:s,acceptSVG:u}=e,m=document.querySelector("#performer-page"),d=null===(n=Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,"value"))||void 0===n?void 0:n.set,g=document.querySelector("div.custom-fields-input > button"),v=document.querySelector("div.custom-fields-input > div"),f=document.querySelector('input[placeholder="alt_image"]');async function p(e){if(!e||!f||!d)return;var t;"collapse"==v.getAttribute("class")&&(g.focus(),g.click(),await(t=200,new Promise((e=>setTimeout(e,t)))),f.focus()),null==d||d.call(f,e);const a=new Event("change",{bubbles:!0});f.dispatchEvent(a),f.focus()}const y=a({...e}),h=a({isEditing:o,text:"Set secondary image...",onImageChange:function(e){r.default.onImageChange(e,p)},onImageURL:p,acceptSVG:u});return m?l.createElement(l.Fragment,null,y,h):l.createElement(l.Fragment,null,y)})),o.patch.instead("CustomFieldInput",(function(e,t,a){const{field:n,value:r,onChange:i,isNew:c=!1,error:s}=e,{HoverPopover:u}=o.components,m=l.useMemo((()=>l.createElement("div",{className:"secondary-image-popover"},l.createElement("img",{className:"secondary-image-thumbnail",alt:n,src:r}))),[r]),d=a({...e});return"alt_image"===n&&r?l.createElement(u,{className:"scene-card__performer",placement:"top",content:m,leaveDelay:100},d):l.createElement(l.Fragment,null,d)})),o.patch.instead("CustomFields",(function(e,t,a){const{values:n}=e;if(Object.keys(n).length<=1)return l.createElement(l.Fragment,null);const r=a({...e});return l.createElement(l.Fragment,null,r)}))};const r=n(a(577)),{PluginApi:o}=window,{GQL:i,React:l}=o,{StashService:c}=window.PluginApi.utils}},t={};function a(n){var r=t[n];if(void 0!==r)return r.exports;var o=t[n]={exports:{}};return e[n].call(o.exports,o,o.exports,a),o.exports}a.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};(()=>{const e=a(604);a(264),(0,e.handlePerformerHeaderImagePatch)()})()})(); \ No newline at end of file diff --git a/plugins/SecondaryPerformerImage/SecondaryPerformerImage.yml b/plugins/SecondaryPerformerImage/SecondaryPerformerImage.yml new file mode 100644 index 0000000..b6ddc7d --- /dev/null +++ b/plugins/SecondaryPerformerImage/SecondaryPerformerImage.yml @@ -0,0 +1,16 @@ +name: Add secondary perfomrer image. +description: Adds support for a secondary perfomrer image on the perform details page. +url: https://github.com/stashapp/CommunityScripts +version: 1.0 +settings: + imageMode: + displayName: Image Mode + description: Mode at which to display the performer image. There are only 3 valid values. Specify 0 to enable the secondary image to be used in the expanded view. Specify 1 to enable the secondary image to be used in the collapsed view. Specify 2 to enable a button to be used to flip between the primary and secondary image. + type: NUMBER +ui: + javascript: + - SecondaryPerformerImage.js + css: + - SecondaryPerformerImage.css + assets: + /: .