Skip to content

Commit 3c1cb64

Browse files
[tagCopyPaste] Initial Commit. (#570)
1 parent 5a5997d commit 3c1cb64

File tree

4 files changed

+257
-0
lines changed

4 files changed

+257
-0
lines changed

plugins/tagCopyPaste/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Tag Copy/Paste
2+
3+
This plugin adds Copy and Paste buttons next to the Tags input field that allows for easier bulk adding and copying of tags, with the goal of making it easy to copy Tags between objects, bulk load manually created tag lists, or load tag lists copied from AI tagger output.
4+
5+
The Copy button will create a comma delimited list of all currently entered tags and put this on your clipboard.
6+
7+
The Paste button will check your current clipboard for a comma delimited string and add these as Tags, optionally creating any missing tags. Pasted tags will be checked against both primary Tag names and all aliases, comparisons are not case sensitive and allow "_" to be interpreted as a space.
8+
9+
**Note**: This plugin will prompt you to grant access to the clipboard. This must be granted in order for this plugin to work.
10+
11+
## Config Options:
12+
- **Create If Not Exists**: If enabled, new tags will be created when pasted list contains entries that do not already exist. DEFAULT: Disabled
13+
- **Require Confirmation**: If enabled, user needs to confirm paste before changes are saved. DEFAULT: Disabled
14+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
button.imageGalleryNav-copyButton,
2+
button.imageGalleryNav-pasteButton {
3+
float: right;
4+
height: 21px;
5+
line-height: 20px;
6+
padding: 0 10px;
7+
margin-right: 15px;
8+
}
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
(async () => {
2+
let pluginSettings = {};
3+
const defaultPluginSettings = {
4+
createIfNotExists: false,
5+
requireConfirmation: false,
6+
};
7+
8+
// On image page, get data about gallery (image's position within gallery, next/prev image IDs),
9+
// add arrow buttons to page, and register arrow keypress handlers,
10+
async function setupTagCopyPaste(objType) {
11+
// Get plugin settings.
12+
const configSettings = await csLib.getConfiguration("tagCopyPaste", {}); // getConfiguration is from cs-ui-lib.js
13+
pluginSettings = {
14+
...defaultPluginSettings,
15+
...configSettings,
16+
};
17+
18+
var objID = window.location.pathname.split("/")[2];
19+
20+
// Add UI elements.
21+
if (objID !== "new") {
22+
insertCopyPasteButtons(objID, objType);
23+
}
24+
}
25+
26+
function insertCopyPasteButtons(objID, objType) {
27+
var copyButton = document.createElement("button");
28+
copyButton.className = "imageGalleryNav-copyButton btn btn-secondary";
29+
copyButton.innerText = "Copy";
30+
copyButton.addEventListener("click", (e) => {
31+
e.preventDefault();
32+
handleCopyClick();
33+
});
34+
35+
var pasteButton = document.createElement("button");
36+
pasteButton.className = "imageGalleryNav-pasteButton btn btn-secondary";
37+
pasteButton.innerText = "Paste";
38+
pasteButton.addEventListener("click", (e) => {
39+
e.preventDefault();
40+
handlePasteClick(objID, objType);
41+
});
42+
43+
if (document.querySelector("button.imageGalleryNav-pasteButton") == null) {
44+
document.querySelector("label[for='tag_ids']").append(pasteButton);
45+
}
46+
if (document.querySelector("button.imageGalleryNav-copyButton") == null) {
47+
document.querySelector("label[for='tag_ids']").append(copyButton);
48+
}
49+
}
50+
51+
// Handle copy click. Return delimited list of current tags.
52+
async function handleCopyClick() {
53+
// Get tags from input box.
54+
var tagList = [];
55+
document
56+
.querySelectorAll(
57+
"label[for='tag_ids'] + div .react-select__multi-value__label"
58+
)
59+
.forEach((item) => {
60+
tagList.push(item.innerText);
61+
});
62+
63+
// Join tags as comma delimited list and write to clipboard.
64+
await navigator.clipboard.writeText(tagList.join(","));
65+
}
66+
67+
// Handle paste click.
68+
async function handlePasteClick(objID, objType) {
69+
var inputTagList = [];
70+
71+
// Parse tag list from comma delimited string.
72+
var tagInput = await navigator.clipboard.readText();
73+
tagInput.split(",").forEach((item) => {
74+
if (!inputTagList.includes(item)) {
75+
inputTagList.push(item);
76+
}
77+
});
78+
79+
// Get tags from input box and also add to tag list.
80+
document
81+
.querySelectorAll(
82+
"label[for='tag_ids'] + div .react-select__multi-value__label"
83+
)
84+
.forEach((item) => {
85+
if (!inputTagList.includes(item)) {
86+
inputTagList.push(item.innerText);
87+
}
88+
});
89+
90+
inputTagList.sort();
91+
92+
var missingTags = [];
93+
var existingTags = [];
94+
var tagUpdateList = [];
95+
96+
// Search for tag ID for each tag. If exists, add to tag ID list. If not exists, create new tag and add to tag ID list.
97+
for (let i = 0; i < inputTagList.length; i++) {
98+
var inputTag = inputTagList[i];
99+
var tagID = await getTagByName(inputTag);
100+
if (tagID != null && tagID.length) {
101+
existingTags.push(inputTag);
102+
tagUpdateList.push(tagID[0]);
103+
} else {
104+
missingTags.push(inputTag);
105+
}
106+
}
107+
108+
if (pluginSettings.requireConfirmation) {
109+
var msg = "";
110+
if (pluginSettings.createIfNotExists) {
111+
msg = `Missing Tags that will be created:\n${missingTags.join(
112+
", "
113+
)}\n\nExisting Tags that will be saved: \n${existingTags.join(
114+
", "
115+
)}\n\nContinue?`;
116+
} else {
117+
msg = `Missing Tags that will be skipped:\n${missingTags.join(
118+
", "
119+
)}\n\nExisting Tags that will be saved: \n${existingTags.join(
120+
", "
121+
)}\n\nContinue?`;
122+
}
123+
124+
var userConfirmed = confirm(msg);
125+
if (!userConfirmed) {
126+
return;
127+
}
128+
}
129+
130+
if (pluginSettings.createIfNotExists && missingTags.length) {
131+
for (let i = 0; i < missingTags.length; i++) {
132+
var newTagName = missingTags[i];
133+
var newTagID = await createNewTag(newTagName);
134+
if (newTagID != null) {
135+
tagUpdateList.push(newTagID);
136+
}
137+
}
138+
}
139+
140+
// Update tags on object with new tag ID list.
141+
await updateObjTags(
142+
objID,
143+
tagUpdateList,
144+
objType.toLowerCase() + "Update",
145+
objType + "UpdateInput"
146+
);
147+
148+
window.location.reload();
149+
}
150+
151+
// *** Utility Functions ***
152+
153+
// *** GQL Calls ***
154+
155+
// Update Object by ID, new tags list, and GQL mutation name.
156+
async function updateObjTags(objID, tags, fnName, inputName) {
157+
const variables = { input: { id: objID, tag_ids: tags } };
158+
const query = `mutation UpdateObj($input:${inputName}!) { ${fnName}(input: $input) {id} }`;
159+
return await csLib.callGQL({ query, variables });
160+
}
161+
162+
// Update Object by ID, new tags list, and GQL mutation name.
163+
async function createNewTag(tagName) {
164+
const variables = { input: { name: tagName } };
165+
const query = `mutation CreateTag($input:TagCreateInput!) { tagCreate(input: $input) {id} }`;
166+
return await csLib
167+
.callGQL({ query, variables })
168+
.then((data) => data.tagCreate.id);
169+
}
170+
171+
// Find Tag by name/alias.
172+
// Return match tag ID.
173+
async function getTagByName(tagName) {
174+
const tagFilter = {
175+
name: { value: tagName, modifier: "EQUALS" },
176+
OR: { aliases: { value: tagName, modifier: "EQUALS" } },
177+
};
178+
const findFilter = { per_page: -1, sort: "name" };
179+
const variables = { tag_filter: tagFilter, filter: findFilter };
180+
const query = `query ($tag_filter: TagFilterType!, $filter: FindFilterType!) { findTags(filter: $filter, tag_filter: $tag_filter) { tags { id } } }`;
181+
return await csLib
182+
.callGQL({ query, variables })
183+
.then((data) => data.findTags.tags.map((item) => item.id));
184+
}
185+
186+
// Wait for scenes page.
187+
csLib.PathElementListener("/scenes/", "[id*='-edit-details']", () => {
188+
setupTagCopyPaste("Scene");
189+
}); // PathElementListener is from cs-ui-lib.js
190+
191+
// Wait for studios page.
192+
csLib.PathElementListener("/studios/", "[id='studio-edit']", () => {
193+
setupTagCopyPaste("Studio");
194+
}); // PathElementListener is from cs-ui-lib.js
195+
196+
// Wait for groups page.
197+
csLib.PathElementListener("/groups/", "[id='group-edit']", () => {
198+
setupTagCopyPaste("Group");
199+
}); // PathElementListener is from cs-ui-lib.js
200+
201+
// Wait for performers page.
202+
csLib.PathElementListener("/performers/", "[id='performer-edit']", () => {
203+
setupTagCopyPaste("Performer");
204+
}); // PathElementListener is from cs-ui-lib.js
205+
206+
// Wait for galleries page.
207+
csLib.PathElementListener("/galleries/", "[id*='-edit-details']", () => {
208+
setupTagCopyPaste("Gallery");
209+
}); // PathElementListener is from cs-ui-lib.js
210+
211+
// Wait for images page.
212+
csLib.PathElementListener("/images/", "[id*='-edit-details']", () => {
213+
setupTagCopyPaste("Image");
214+
}); // PathElementListener is from cs-ui-lib.js
215+
})();
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: tagCopyPaste
2+
# requires: CommunityScriptsUILibrary
3+
description: Adds Copy/Paste buttons to Tags field.
4+
version: 0.1
5+
settings:
6+
createIfNotExists:
7+
displayName: Create If Not Exists
8+
description: If enabled, new tags will be created when pasted list contains entries that do not already exist.
9+
type: BOOLEAN
10+
requireConfirmation:
11+
displayName: Require Confirmation
12+
description: If enabled, user needs to confirm paste before changes are saved.
13+
type: BOOLEAN
14+
ui:
15+
requires:
16+
- CommunityScriptsUILibrary
17+
javascript:
18+
- tagCopyPaste.js
19+
css:
20+
- tagCopyPaste.css

0 commit comments

Comments
 (0)