|
1 | 1 | (async () => { |
| 2 | + const PluginApi = window.PluginApi; |
| 3 | + const React = PluginApi.React; |
| 4 | + |
2 | 5 | let pluginSettings = {}; |
3 | 6 | const defaultPluginSettings = { |
4 | 7 | createIfNotExists: false, |
5 | 8 | requireConfirmation: false, |
6 | 9 | }; |
7 | 10 |
|
8 | | - var objID = null; |
9 | | - var objType = null; |
10 | | - |
11 | | - // helper function to get the innerText of all elements matching a selector |
12 | | - const getAllInnerText = (selector) => Array.from(document.querySelectorAll(selector)) |
13 | | - .map((el) => el.innerText.trim()) |
14 | | - .filter((text) => text !== ""); |
15 | | - |
16 | | - // On image page, get data about gallery (image's position within gallery, next/prev image IDs), |
17 | | - // add arrow buttons to page, and register arrow keypress handlers, |
18 | | - async function setupTagCopyPaste(objTypeTriggered) { |
| 11 | + // Helper functions for handling array of tags. |
| 12 | + const getTagNameArray = (tagArray) => tagArray.map((value) => value.name); |
| 13 | + const getTagNameString = (tagArray) => getTagNameArray(tagArray).join(", "); |
| 14 | + const sortTagArray = (tagArray) => |
| 15 | + tagArray.sort((a, b) => { |
| 16 | + var aCompStr = a.sort_name ? a.sort_name : a.name; |
| 17 | + var bCompStr = b.sort_name ? b.sort_name : b.name; |
| 18 | + return aCompStr.localeCompare(bCompStr); |
| 19 | + }); |
| 20 | + |
| 21 | + async function setupTagCopyPaste() { |
19 | 22 | // Get plugin settings. |
20 | 23 | const configSettings = await csLib.getConfiguration("tagCopyPaste", {}); // getConfiguration is from cs-ui-lib.js |
21 | 24 | pluginSettings = { |
22 | 25 | ...defaultPluginSettings, |
23 | 26 | ...configSettings, |
24 | 27 | }; |
25 | 28 |
|
26 | | - objID = window.location.pathname.split("/")[2]; |
27 | | - objType = objTypeTriggered; |
28 | | - |
29 | | - // Add UI elements. |
30 | | - if (objID !== "new") { |
31 | | - insertCopyPasteButtons(); |
32 | | - } |
33 | | - } |
34 | | - |
35 | | - function copyEventHandler(event) { |
36 | | - event.preventDefault(); |
37 | | - handleCopyClick(); |
38 | | - } |
39 | | - |
40 | | - function pasteEventHandler(event) { |
41 | | - event.preventDefault(); |
42 | | - handlePasteClick(); |
43 | | - } |
44 | | - |
45 | | - function insertCopyPasteButtons() { |
46 | | - // listen for copy and paste events within tag input box |
47 | | - // find tag input box |
48 | | - const tagInputBox = document.querySelector("label[for='tag_ids'] + div .react-select__value-container"); |
49 | | - if (tagInputBox) { |
50 | | - tagInputBox.removeEventListener("copy", copyEventHandler); |
51 | | - tagInputBox.removeEventListener("paste", pasteEventHandler); |
52 | | - tagInputBox.addEventListener("copy", copyEventHandler); |
53 | | - tagInputBox.addEventListener("paste", pasteEventHandler); |
54 | | - } |
55 | | - |
56 | | - var copyButton = document.createElement("button"); |
57 | | - copyButton.className = "imageGalleryNav-copyButton btn btn-secondary"; |
58 | | - copyButton.innerText = "Copy"; |
59 | | - copyButton.onclick = (event) => { |
60 | | - event.preventDefault(); |
61 | | - handleCopyClick(); |
62 | | - } |
63 | | - |
64 | | - var pasteButton = document.createElement("button"); |
65 | | - pasteButton.className = "imageGalleryNav-pasteButton btn btn-secondary"; |
66 | | - pasteButton.innerText = "Paste"; |
67 | | - pasteButton.onclick = (event) => { |
68 | | - event.preventDefault(); |
69 | | - handlePasteClick(); |
70 | | - } |
71 | | - |
72 | | - if (document.querySelector("button.imageGalleryNav-pasteButton") == null) { |
73 | | - document.querySelector("label[for='tag_ids']").append(pasteButton); |
74 | | - } |
75 | | - if (document.querySelector("button.imageGalleryNav-copyButton") == null) { |
76 | | - document.querySelector("label[for='tag_ids']").append(copyButton); |
77 | | - } |
| 29 | + // Patch TagSelect to add copy/paste buttons. |
| 30 | + PluginApi.patch.after("TagSelect", function (props, _, originalComponent) { |
| 31 | + const copyButtonRef = React.useRef(null); |
| 32 | + const pasteButtonRef = React.useRef(null); |
| 33 | + const propsRef = props; |
| 34 | + |
| 35 | + // Copy Button click handler |
| 36 | + const copyClickHandler = (event) => { |
| 37 | + event.preventDefault(); |
| 38 | + handleCopyClick(propsRef.values); |
| 39 | + }; |
| 40 | + |
| 41 | + // Paste Button click handler |
| 42 | + const pasteClickHandler = (event) => { |
| 43 | + event.preventDefault(); |
| 44 | + handlePasteClick(propsRef.onSelect, propsRef.values); |
| 45 | + }; |
| 46 | + |
| 47 | + React.useEffect(() => { |
| 48 | + // Not the ideal way to handle this, but it works. |
| 49 | + // Wait for the buttons to render and then add the onCopy/onPaste handlers to select control DOM element. |
| 50 | + if (copyButtonRef && copyButtonRef.current) { |
| 51 | + var mainCopyPasteWrapper = |
| 52 | + copyButtonRef.current.parentElement.parentElement; |
| 53 | + var tagInputBox = mainCopyPasteWrapper.querySelector( |
| 54 | + ".react-select__value-container", |
| 55 | + ); |
| 56 | + |
| 57 | + const copyEventHandler = (e) => { |
| 58 | + e.preventDefault(); |
| 59 | + copyButtonRef.current.click(); |
| 60 | + }; |
| 61 | + |
| 62 | + const pasteEventHandler = (e) => { |
| 63 | + e.preventDefault(); |
| 64 | + pasteButtonRef.current.click(); |
| 65 | + }; |
| 66 | + |
| 67 | + if (tagInputBox) { |
| 68 | + tagInputBox.addEventListener("copy", copyEventHandler); |
| 69 | + tagInputBox.addEventListener("paste", pasteEventHandler); |
| 70 | + } |
| 71 | + } |
| 72 | + }, []); |
| 73 | + |
| 74 | + return React.createElement("div", { className: "tagCopyPaste" }, [ |
| 75 | + React.createElement( |
| 76 | + "div", |
| 77 | + { |
| 78 | + className: "btn-group", |
| 79 | + }, |
| 80 | + [ |
| 81 | + React.createElement( |
| 82 | + "button", |
| 83 | + { |
| 84 | + type: "button", |
| 85 | + ref: copyButtonRef, |
| 86 | + onClick: copyClickHandler, |
| 87 | + className: |
| 88 | + "imageGalleryNav-copyButton btn btn-secondary btn-sm", |
| 89 | + }, |
| 90 | + "Copy", |
| 91 | + ), |
| 92 | + React.createElement( |
| 93 | + "button", |
| 94 | + { |
| 95 | + type: "button", |
| 96 | + ref: pasteButtonRef, |
| 97 | + onClick: pasteClickHandler, |
| 98 | + className: |
| 99 | + "imageGalleryNav-pasteButton btn btn-secondary btn-sm", |
| 100 | + }, |
| 101 | + "Paste", |
| 102 | + ), |
| 103 | + ], |
| 104 | + ), |
| 105 | + originalComponent, |
| 106 | + ]); |
| 107 | + }); |
78 | 108 | } |
79 | 109 |
|
80 | 110 | // Handle copy click. Return delimited list of current tags. |
81 | | - async function handleCopyClick() { |
| 111 | + async function handleCopyClick(propValues) { |
82 | 112 | // Get tags from input box |
83 | 113 | // join as comma delimited list |
84 | | - const tagList = getAllInnerText("label[for='tag_ids'] + div .react-select__multi-value__label").join(",") |
85 | | - // write to clipboard. |
| 114 | + const tagList = getTagNameString(propValues); |
86 | 115 | navigator.clipboard.writeText(tagList); |
87 | 116 | } |
88 | 117 |
|
89 | 118 | // Handle paste click. |
90 | | - async function handlePasteClick() { |
| 119 | + async function handlePasteClick(onSelect, propValues) { |
91 | 120 | // Parse tag list from comma delimited string. |
92 | 121 | const tagInput = await navigator.clipboard.readText(); |
93 | | - var inputTagList = tagInput.split(/\r?\n|\r|,/).map(s => s.trim()).filter((text) => text !== "") // do de-duplication later |
| 122 | + var inputTagList = tagInput |
| 123 | + .split(/\r?\n|\r|,/) |
| 124 | + .map((s) => s.trim()) |
| 125 | + .filter((text) => text !== ""); // do de-duplication later |
94 | 126 |
|
95 | 127 | // Get tags from input box and also add to tag list. |
96 | | - const existingTagList = getAllInnerText("label[for='tag_ids'] + div .react-select__multi-value__label"); |
| 128 | + const existingTagList = getTagNameArray(propValues); |
97 | 129 |
|
98 | 130 | inputTagList = [...new Set([...inputTagList, ...existingTagList])].sort(); |
99 | 131 |
|
100 | | - var missingTags = []; |
| 132 | + var missingTagNames = []; |
101 | 133 | var existingTags = []; |
102 | 134 | var tagUpdateList = []; |
103 | 135 |
|
|
108 | 140 | existingTags.push(inputTag); |
109 | 141 | tagUpdateList.push(tagID[0]); |
110 | 142 | } else { |
111 | | - missingTags.push(inputTag); |
| 143 | + missingTagNames.push(inputTag); |
112 | 144 | } |
113 | 145 | } |
114 | 146 |
|
115 | | - if (pluginSettings.requireConfirmation) { |
116 | | - const missingTagsStr = missingTags.join(", "); |
117 | | - const existingTagsStr = existingTags.join(", "); |
118 | | - const msg = pluginSettings.createIfNotExists |
119 | | - ? `Missing Tags that will be created:\n${missingTagsStr}\n\nExisting Tags that will be saved: \n${existingTagsStr}\n\nContinue?` |
120 | | - : `Missing Tags that will be skipped:\n${missingTagsStr}\n\nExisting Tags that will be saved: \n${existingTagsStr}\n\nContinue?`; |
121 | | - |
122 | | - if (!confirm(msg)) { |
123 | | - return; |
| 147 | + // Create missing tags if enabled. Prompt user to confirm if confirmation option is also enabled. |
| 148 | + const missingTagsStr = missingTagNames.join(", "); |
| 149 | + const msg = `Missing Tags that will be created:\n${missingTagsStr}\n\nContinue?`; |
| 150 | + if ( |
| 151 | + pluginSettings.createIfNotExists && |
| 152 | + missingTagNames.length && |
| 153 | + (!pluginSettings.requireConfirmation || confirm(msg)) |
| 154 | + ) { |
| 155 | + for (const missingTagName of missingTagNames) { |
| 156 | + const newTag = await createNewTag(missingTagName); |
| 157 | + if (newTag != null) tagUpdateList.push(newTag); |
124 | 158 | } |
125 | 159 | } |
126 | 160 |
|
127 | | - if (pluginSettings.createIfNotExists && missingTags.length) { |
128 | | - for (const missingTag of missingTags) { |
129 | | - const newTagID = await createNewTag(missingTag); |
130 | | - if (newTagID != null) tagUpdateList.push(newTagID); |
131 | | - } |
132 | | - } |
133 | | - |
134 | | - // Update tags on object with new tag ID list. |
135 | | - await updateObjTags( |
136 | | - tagUpdateList, |
137 | | - `${objType.toLowerCase()}Update`, |
138 | | - `${objType}UpdateInput` |
139 | | - ); |
140 | | - |
141 | | - window.location.reload(); |
| 161 | + // Update TagSelect control with new tag list. |
| 162 | + onSelect(sortTagArray(tagUpdateList)); |
142 | 163 | } |
143 | 164 |
|
144 | 165 | // *** GQL Calls *** |
145 | 166 |
|
146 | | - // Update Object by ID, new tags list, and GQL mutation name. |
147 | | - async function updateObjTags(tags, fnName, inputName) { |
148 | | - const variables = { input: { id: objID, tag_ids: tags } }; |
149 | | - const query = `mutation UpdateObj($input:${inputName}!) { ${fnName}(input: $input) {id} }`; |
150 | | - return await csLib.callGQL({ query, variables }); |
151 | | - } |
152 | | - |
153 | | - // Update Object by ID, new tags list, and GQL mutation name. |
| 167 | + // Create new tag. |
| 168 | + // Return newly created tag object. |
154 | 169 | async function createNewTag(tagName) { |
155 | 170 | const variables = { input: { name: tagName } }; |
156 | | - const query = `mutation CreateTag($input:TagCreateInput!) { tagCreate(input: $input) {id} }`; |
| 171 | + const query = `mutation CreateTag($input:TagCreateInput!) { tagCreate(input: $input) { id, name, sort_name, favorite, description, aliases, image_path, parents {id, name}, stash_ids {endpoint, stash_id, updated_at } } }`; |
157 | 172 | return await csLib |
158 | 173 | .callGQL({ query, variables }) |
159 | | - .then((data) => data.tagCreate.id); |
| 174 | + .then((data) => data.tagCreate); |
160 | 175 | } |
161 | 176 |
|
162 | 177 | // Find Tag by name/alias. |
163 | | - // Return match tag ID. |
| 178 | + // Return matched list of tag objects. |
164 | 179 | async function getTagByName(tagName) { |
165 | 180 | const tagFilter = { |
166 | 181 | name: { value: tagName, modifier: "EQUALS" }, |
167 | 182 | OR: { aliases: { value: tagName, modifier: "EQUALS" } }, |
168 | 183 | }; |
169 | 184 | const findFilter = { per_page: -1, sort: "name" }; |
170 | 185 | const variables = { tag_filter: tagFilter, filter: findFilter }; |
171 | | - const query = `query ($tag_filter: TagFilterType!, $filter: FindFilterType!) { findTags(filter: $filter, tag_filter: $tag_filter) { tags { id } } }`; |
| 186 | + const query = `query ($tag_filter: TagFilterType!, $filter: FindFilterType!) { findTags(filter: $filter, tag_filter: $tag_filter) { tags { id, name, sort_name, favorite, description, aliases, image_path, parents {id, name}, stash_ids {endpoint, stash_id, updated_at } } } }`; |
172 | 187 | return await csLib |
173 | 188 | .callGQL({ query, variables }) |
174 | | - .then((data) => data.findTags.tags.map((item) => item.id)); |
| 189 | + .then((data) => data.findTags.tags); |
175 | 190 | } |
176 | 191 |
|
177 | | - // listener arrays |
178 | | - [ |
179 | | - [ "/scenes/", "[id*='-edit-details']", "Scene" ], |
180 | | - [ "/studios/", "[id='studio-edit']", "Studio" ], |
181 | | - [ "/groups/", "[id='group-edit']", "Group" ], |
182 | | - [ "/performers/", "[id='performer-edit']", "Performer" ], |
183 | | - [ "/galleries/", "[id*='-edit-details']", "Gallery" ], |
184 | | - [ "/images/", "[id*='-edit-details']", "Image" ] |
185 | | - ].forEach(([path, selector, objTypeTriggered]) => { |
186 | | - // Wait for the page to load and the element to be present. |
187 | | - csLib.PathElementListener(path, selector, () => { |
188 | | - setupTagCopyPaste(objTypeTriggered); |
189 | | - }); // PathElementListener is from cs-ui-lib.js |
190 | | - }); |
| 192 | + setupTagCopyPaste(); |
191 | 193 | })(); |
0 commit comments