// ==UserScript==
// @name Saved Posts Helper
// @description Batch-move saved posts between private lists, quick move after saving in Q&A, import/export lists
// @homepage https://github.com/samliew/SO-mod-userscripts
// @author Samuel Liew
// @version 3.2.13
//
// @match https://*.stackoverflow.com/*
// @match https://*.serverfault.com/*
// @match https://*.superuser.com/*
// @match https://*.askubuntu.com/*
// @match https://*.mathoverflow.net/*
// @match https://*.stackexchange.com/*
//
// @exclude https://api.stackexchange.com/*
// @exclude https://data.stackexchange.com/*
// @exclude https://contests.stackoverflow.com/*
// @exclude https://winterbash*.stackexchange.com/*
// @exclude *chat.*
// @exclude *blog.*
//
// @require https://raw.githubusercontent.com/samliew/SO-mod-userscripts/master/lib/se-ajax-common.js
// @require https://raw.githubusercontent.com/samliew/SO-mod-userscripts/master/lib/common.js
// ==/UserScript==
/* globals StackExchange, scriptName, selfId, fkey */
///
'use strict';
const listSidebarNav = document.querySelector('.js-saves-sidebar-nav');
const currListId = listSidebarNav?.querySelector('a.is-selected')?.parentElement.dataset.listId;
const currListName = document.querySelector('.js-saves-list-header')?.innerText.trim();
const isOnQnaPages = location.pathname.startsWith('/questions/');
const isOnSavesPages = location.pathname.startsWith('/users/saves/');
const isOnAllSavesPage = location.pathname.endsWith('/all');
const isOnForLaterPage = !!document.querySelector('[data-is-forlater="True"]') || (!currListId && !isOnAllSavesPage);
let savesList, cAll, cAllSelect, elSavesCount;
if (isOnSavesPages) {
console.log(`Current saves list: ${currListId ? currListId : (isOnForLaterPage ? 'For later' : 'All saves')}`);
}
// Validation
if (!fkey) {
console.error(`${scriptName}: fkey not found!`);
return;
}
if (!(isOnQnaPages || currListId || isOnAllSavesPage || isOnForLaterPage)) {
console.error(`Unable to detect current saved list id.`);
return;
}
/**
* @summary Create saved list
* @param {string} listName new list name
* @returns {number} listId
*/
const createSavedList = async (listName) => {
//console.log('createSavedList', listName);
// Validation
listName = listName.trim();
if (!listName) return false;
const formData = new FormData();
formData.append("fkey", fkey);
formData.append("listName", listName);
const resp = await fetch(`${location.origin}/users/saves/${selfId}/create-list`, {
"method": "POST",
"body": formData,
}).then(resp => resp.json());
// Toast success or error message
StackExchange?.helpers?.hideToasts();
StackExchange?.helpers?.showToast(resp.ToastMessage || resp.ErrorMessage, {
type: resp?.Success ? 'success' : 'danger',
});
return resp.ListId || false;
};
/**
* @summary Save item
* @param {number} pid post id
* @param {number} [listId] list id
* @param {string} [listName] list name
* @param {boolean} [unsaveItem] unsave item
* @returns {string} html
*/
const saveItem = async (pid, listId = '', listName = '', unsaveItem = false) => {
//console.log('saveItem', pid, listName);
const formData = new FormData();
formData.append("fkey", fkey);
if (listId) formData.append("listId", listId);
if (listName) formData.append("listName", listName);
const isUndo = unsaveItem ? '?isUndo=true' : '';
return await fetch(`${location.origin}/posts/${pid}/save${isUndo}`, {
"method": "POST",
"body": formData,
}).then(resp => resp.json());
};
/**
* @summary Move saved item
* @param {number} pid post id
* @param {number} listId list id
* @param {string} [listName] list name
*
* @returns {string} html
*/
const moveSavedItem = async (pid, listId, listName = '') => {
//console.log('moveSavedItem', pid, listId, listName);
const formData = new FormData();
formData.append("fkey", fkey);
formData.append("postId", pid);
formData.append("listId", listId);
if (listName) formData.append("listName", listName);
return await fetch(`${location.origin}/posts/save/manage-save`, {
"method": "POST",
"body": formData,
}).then(resp => resp.json());
};
/**
* @summary Get saves modal
* @param {number} [pid] post id
* @param {boolean} [isMove] move (default create)
* @param {number} [currListId] initial selected list id
* @returns {string} html
*/
const getSavesModal = async (pid = 1, isMove = false, currListId = null) => {
//console.log('getSavesModal', pid, isMove, currListId);
if (!pid) return;
return await fetch(`${location.origin}/posts/${pid}/open-save-modal?isMoveTo=${isMove}&listId=${currListId}&_=${Date.now()}`, {
"method": "GET",
}).then(resp => resp.text());
};
/**
* @summary Get saves private list names (excl. create)
* @param {number} [pid] post id
* @returns {object[]} list of { name, id, count }
*/
const getSavesLists = async (postId = 1) => {
//console.log('getSavesLists', postId);
const modalBody = await getSavesModal(postId);
const el = document.createElement('div');
el.innerHTML = modalBody;
const options = el.querySelectorAll('.js-save-manage-select option');
const values = [...options].map(v => {
// there are newlines and spaces when loading save lists in Q&A pages for some reason
const hasCount = / \((\d+,)*\d+\)[\n\s]*$/.test(v.innerText);
return {
// there are newlines and spaces when loading save lists in Q&A pages for some reason
name: v.innerText.replace(/[\n\s]*\(\d+\)[\n\s]*$/, '').trim(),
id: v.value,
count: hasCount ? v.innerText.match(/(?:\d+,)*\d+/g).pop() : 0
}
}).filter(v => v.value !== 'create');
return values;
};
/**
* @summary Recursive get all items in a saved list
* @param {number} [listId] list id
* @param {string} [sort] sort type
* @param {number} [page] page number
* @returns {object[]} list of { pid, title, url }
*
* If listId is null: get uncategorised (For later) items
* If listId is "all": get all items
*/
const getSavedListItems = async (listId = null, sort = 'Added', page = 1) => {
//console.log('getSavedListItems', listId, sort, page);
// Validate listId param
if (listId !== null && listId !== 'all' && listId <= 0) return [];
const resp = await fetch(`${location.origin}/users/saves/${selfId}${listId === 'all' ? '' : `/${listId || 'all'}`}?sort=${sort}&page=${page}&_=${Date.now()}`, {
"method": "GET",
});
pageHtml = await resp.text();
// Make page dom
const el = document.createElement('div');
el.innerHTML = pageHtml;
// Select items on page
const items = [...el.querySelectorAll('.s-post-summary')].map(v => {
const questionLink = v.querySelector('.s-post-summary--content-title .s-link');
const postLinks = v.querySelectorAll('.s-post-summary--content-title .s-link, .js-post-summary-answer-link');
const postLink = [...postLinks].pop(); // last link is post link (could be saved answer)
return {
//postType: postLinks.length > 1 ? 'answer' : 'question',
pid: getPostId(postLink.href),
url: 'https:' + toShortLink(postLink.href), //.replace(/\?.*$/, ''), // strip ?r=Saves_AllUserSaves from end
title: questionLink.innerText.replace(/;/g, ',').replace(/\s*\[\w+\]\s*$/i, '').trim(),
}
});
// Detect next page
const nextPage = el.querySelector('a.s-pagination--item[rel="next"]');
if (nextPage) {
await delay(1000);
const nextPageItems = await getSavedListItems(listId, sort, page + 1);
return [...items, ...nextPageItems];
}
return items;
};
/**
* @summary Update move dropdown list and sidebar counts
*/
const updateMoveDropdown = async (postId = null, isQuestion = false) => {
if (isOnQnaPages) {
// Get post id from url
postId = location.pathname.match(/\d+/)?.shift() ?? null;
}
else {
// Get a valid post id from anywhere on page
postId = document.querySelector('a[href^="/questions/"]')?.href.match(/\d+/)?.shift() ?? null;
}
//console.log('updateMoveDropdown', postId, isQuestion);
// Get saved lists
const savedLists = await getSavesLists(postId);
// Update move dropdown list
if (cAllSelect) {
if (postId) {
cAllSelect.dataset.postId = postId;
cAllSelect.dataset.isQuestion = isQuestion;
}
cAllSelect.disabled = true;
// Remove all existing options
while (cAllSelect.firstElementChild) cAllSelect.firstElementChild.remove();
if (isOnSavesPages) {
// Add default bulk option value
const opt = makeElem('option', {
'class': 'd-none',
}, 'Move to...');
cAllSelect.appendChild(opt);
}
// Add saved lists to dropdown
savedLists.forEach(v => {
// Build option text
let text = v.name;
if (v.count > 0) text += ` (${v.count})`;
// Make option
const opt = makeElem('option', {
"data-list-name": v.name,
"data-list-count": v.count,
"value": v.id,
}, text);
// Disable current list item (makes no sense to move to current list)
const currListItem = (currListId && currListId === v.id) || (isOnForLaterPage && v.id === 'for-later');
if (isOnSavesPages && currListItem) opt.setAttribute('disabled', 'disabled');
cAllSelect.appendChild(opt);
});
if (isOnSavesPages) {
// Insert group divider before last option (create new list)
const divider = makeElem('option', {
"disabled": "disabled",
}, '---');
cAllSelect.insertBefore(divider, cAllSelect.lastElementChild);
}
else if (isOnQnaPages) {
// Remove last option (create), since it's harder to implement this as the toast message has a short timeout
cAllSelect.lastElementChild?.remove();
}
// Re-enable dropdown
cAllSelect.disabled = false;
}
// Update sidebar count
const sidebarItems = document.querySelectorAll('.js-saves-sidebar-item');
sidebarItems?.forEach(item => {
const data = savedLists.find(v => v.id == item.dataset.listId);
if (data) {
item.dataset.count = data.count;
item.querySelector('a').dataset.count = data.count;
}
});
};
/**
* @summary Add bulk dropdown or Q&A move dropdown event listeners
*/
const handleMoveDropdownEvent = async evt => {
let listId = evt.target.value;
let listName = evt.target.selectedOptions[0].dataset.listName;
const isQuestion = evt.target.dataset.isQuestion === 'true';
// Create new list
if (listId === 'create') {
listName = prompt('Enter new list name');
if (!listName) return;
listId = await createSavedList(listName);
}
// Validation
if (!listId) {
cAllSelect.selectedIndex = 0;
return;
}
// Get selected checkboxes
const selectedCbs = document.querySelectorAll('.saved-item-bulk-checkbox:checked');
if (selectedCbs.length) {
const num = selectedCbs.length;
// Move to selected list
selectedCbs.forEach(async cb => {
const pid = cb.value;
const resp = await moveSavedItem(pid, listId);
cb.checked = false;
});
// In all saves page, update text of ".js-saved-in" to new list name
if (isOnAllSavesPage) {
const updatedListId = /^\d+$/.test(listId) ? listId : ''; // only replace with numerical list ids or empty
selectedCbs.forEach(cb => {
const el = cb.parentElement.querySelector('.js-saved-in');
el.innerText = listName;
el.href = el.href.replace(/\/\d*$/, `/${updatedListId}`);
});
}
// Not in "All saves page"
else if (isOnSavesPages) {
// Remove from display
selectedCbs?.forEach(cb => {
cb.closest('.js-saves-post-summary').remove();
});
// Reduce count in header
if (elSavesCount) elSavesCount.innerText = Number(elSavesCount.innerText) - num;
}
// Toast success message
const listUrl = Number(listId) ? `${listName}` : `For later`;
const numberMoved = isOnQnaPages && num === 1 ? `${isQuestion ? 'Question' : 'Answer'}` : `${num} post${num > 1 ? 's' : ''}`;
StackExchange?.helpers?.hideToasts();
StackExchange?.helpers?.showToast(`${numberMoved} moved to ${listUrl}.`, {
type: 'success',
useRawHtml: true,
transient: true,
transientTimeout: 20e3,
});
}
else {
// Toast info message
StackExchange?.helpers?.hideToasts();
StackExchange?.helpers?.showToast(`No posts were selected, so nothing was moved.`, {
type: 'info',
useRawHtml: false,
transient: true,
transientTimeout: 6e3,
});
}
// Temporarily "clear" and disable dropdown while updating
cAllSelect.disabled = true;
cAllSelect.selectedIndex = 0;
await delay(1000); // wait 1s for database to update
await updateMoveDropdown();
};
/**
* @summary Handle post saved event
*/
const postSavedEvent = async (postId, isQuestion = false) => {
// Add dropdown to toast
cAllSelect = makeElem('select', {
'class': 'saved-item-all-dropdown',
'disabled': 'disabled',
});
const cb = makeElem('input', {
'type': 'checkbox',
'checked': 'checked',
'class': 'saved-item-bulk-checkbox s-checkbox d-none',
'value': postId
});
const cAllSelectWrapper = makeElem('span', {
'class': 's-select-wrapper d-inline-block ml4'
}, null, [
makeElem('span', null, ' Change: '),
cAllSelect,
cb
]);
$('.js-toast .js-toast-body').append(cAllSelectWrapper);
// Add event listener to move dropdown
cAllSelect?.addEventListener('change', handleMoveDropdownEvent);
// Load options
await updateMoveDropdown(postId, isQuestion);
};
/**
* @summary Handle post UNsaved event
* Unfortunately we don't know which list the post was unsaved from so we can only "undo" to the default list (For later)
*/
const postUnsavedEvent = async (postId, isQuestion = false) => {
// When undo button is clicked
const handleUndoClickEvent = async (evt) => {
const postId = evt.target.value;
const resp = await saveItem(postId);
//console.log(`${isQuestion ? 'Question' : 'Answer'} was resaved.`, postId, resp);
// If we know the current list id, move it back there
if (Number(currListId)) {
const resp2 = await moveSavedItem(postId, currListId);
//console.log(`${isQuestion ? 'Question' : 'Answer'} was moved to ${currListId}.`, postId, currListId, resp);
}
// Toast success message
const listName = document.querySelector('.js-saves-list-header')?.childNodes[0].textContent ?? 'For later';
const listUrl = Number(currListId) ? `${listName}` : `For later`;
StackExchange?.helpers?.hideToasts();
StackExchange?.helpers?.showToast(`${isQuestion ? 'Question' : 'Answer'} was resaved to ${listUrl}. Please refresh the page.`, {
type: 'success',
useRawHtml: true,
transient: true,
transientTimeout: 20e3,
});
// If on Q&A page, undo the saves button on the post
$(`#saves-btn-${postId} svg`).toggleClass('d-none');
};
// Add undo button to toast
const undoBtn = makeElem('button', {
'type': 'button',
'class': 's-btn s-btn__xs s-btn__danger s-btn__outlined',
'value': postId
}, 'Undo?');
const undoBtnWrapper = makeElem('span', {
'class': 's-select-wrapper d-inline-block ml4'
}, null, [
undoBtn
]);
$('.js-toast .js-toast-body').append(undoBtnWrapper);
// Add event listener to undo button
undoBtn.addEventListener('click', handleUndoClickEvent);
};
/**
* @summary Add event listeners
*/
const addEventListeners = () => {
// Saved pages only
if (isOnSavesPages) {
// Add event listener to bulk checkbox
const cbs = document.querySelectorAll('.saved-item-bulk-checkbox');
cAll?.addEventListener('click', evt => {
const checked = evt.target.checked;
cbs?.forEach(box => { box.checked = checked });
});
// Add event listener to move dropdown
cAllSelect?.addEventListener('change', handleMoveDropdownEvent);
}
// Q&A pages only
else if (isOnQnaPages) {
// On page update
$(document).ajaxComplete(async (evt, xhr, settings) => {
if (xhr.status !== 200) return; // capture successful requests only
// Post was saved on Q&A page
if (/\/posts\/\d+\/save$/.test(settings.url)) {
const postId = Number(settings.url.match(/\/posts\/(\d+)\/save$/)?.pop());
const isQuestion = xhr.responseJSON?.NextTooltip?.includes('question');
//console.log(`${isQuestion ? 'Question' : 'Answer'} was saved.`, postId, xhr.responseJSON);
setTimeout(() => {
postSavedEvent(postId, isQuestion);
}, 100);
}
});
}
// On all pages,
// On page update
$(document).ajaxComplete(async (evt, xhr, settings) => {
if (xhr.status !== 200) return; // capture successful requests only
// Post was UNsaved
if (/\/posts\/\d+\/save\?isUndo=true$/.test(settings.url)) {
const postId = Number(settings.url.match(/\/posts\/(\d+)\/save\?isUndo=true$/)?.pop());
const isQuestion = xhr.responseJSON?.NextTooltip.includes('question');
//console.log(`${isQuestion ? 'Question' : 'Answer'} was unsaved.`, postId, xhr.responseJSON);
setTimeout(() => {
postUnsavedEvent(postId, isQuestion);
}, 100);
}
});
};
// Append styles
addStylesheet(`
.js-saves-page nav,
.js-saves-page .filter-wrapper {
position: sticky;
top: 50px;
margin-top: -8px;
padding-top: 14px;
padding-bottom: 14px;
margin-bottom: -14px;
background: var(--white);
z-index: 2;
}
.js-saves-page .js-saves-sidebar-item a {
display: flex;
justify-content: space-between;
}
.js-saves-page nav .js-saves-sidebar-item a[data-count]:after {
content: "(" attr(data-count) ")";
display: inline-block;
margin-left: 0.25em;
}
.js-saves-page .filter-wrapper {
margin-bottom: -1px;
--_ps-bb: var(--su1) solid var(--bc-light);
border-bottom: var(--_ps-bb);
}
.js-saves-page .js-saves-count:not([data-saves-count="0"]) {
display: inline-block;
margin: 0 0 0 0.5em;
}
.js-saves-page .js-saves-count:not([data-saves-count="0"]):before {
content: '(';
}
.js-saves-page .js-saves-count:not([data-saves-count="0"]):after {
content: ')';
}
.js-saves-page .saved-item-all-label {
display: flex;
align-items: center;
cursor: auto;
}
.js-saves-page .saved-item-all-checkbox {
margin-left: calc(var(--su8) + 1px);
margin-right: 0.5em;
padding: 0.5rem;
}
.js-saves-lists-posts {
align-items: center;
}
.js-saves-post-list {
border-radius: 0 !important;
}
.js-saves-post-list .js-saves-post-summary {
padding-left: calc(var(--su16) + 1.2rem);
}
.js-saves-post-list .saved-item-bulk-checkbox {
position: absolute;
top: 20px;
left: var(--su8);
padding: 0.5rem;
z-index: 1;
}
.js-saves-post-list .saved-item-bulk-checkbox:hover {
box-shadow: var(--_ch-bs-focus);
}
.js-saves-post-list .saved-item-bulk-checkbox:after {
content: '';
position: absolute;
top: -21px;
left: -10px;
bottom: -76px;
right: -6px;
opacity: 0;
z-index: 0;
}
.js-saves-post-list .js-post-tag-list-wrapper {
margin-bottom: 0;
}
`); // end stylesheet
// On script run
(async function init() {
// On saves pages
if (isOnSavesPages) {
savesList = document.querySelector('.js-saves-post-list');
if (!savesList) return;
// Insert bulk checkbox selector and dropdown in place of saves count
elSavesCount = document.querySelector('.js-saves-count');
const filterWrapper = elSavesCount.parentElement;
cAll = makeElem('input', {
'type': 'checkbox',
'class': 'saved-item-all-checkbox s-checkbox'
});
cAllSelect = makeElem('select', {
'class': 'saved-item-all-dropdown'
});
const cAllSelectWrapper = makeElem('div', {
'class': 's-select ml16'
}, null, [cAllSelect]);
const cAllLabel = makeElem('label', {
'class': 'saved-item-all-label'
}, null, [
cAll,
makeElem('span', null, 'Select all'),
cAllSelectWrapper
]);
filterWrapper.insertBefore(cAllLabel, elSavesCount);
// Alignment
filterWrapper.classList.add('filter-wrapper', 'ai-center');
// Move saves count to list header
const elSavesHeader = document.querySelector('.js-saves-lists-posts');
const elSavesHeaderTitle = elSavesHeader.querySelector('.js-saves-list-header');
elSavesCount.innerText = elSavesCount.dataset.savesCount;
elSavesHeaderTitle.appendChild(elSavesCount);
// UI fix: Remove pe-none from create/edit list modals, otherwise use can click on elements behind them
document.querySelectorAll('aside.pe-none').forEach(modal => modal.classList.remove('pe-none'));
// Create import modal
const importModal = makeElem('aside', {
'class': 's-modal js-save-modal js-list-action-modal',
'id': 'import-list-modal',
'tabindex': "-1",
'role': "dialog",
'aria-hidden': "true",
'data-controller': "s-modal",
'data-s-modal-target': "modal",
'data-modal-action': "Import",
}, null);
importModal.innerHTML = `
`;
elSavesHeader.parentElement.appendChild(importModal);
// Create export modal
const exportModal = makeElem('aside', {
'class': 's-modal js-save-modal js-list-action-modal',
'id': 'export-list-modal',
'tabindex': "-1",
'role': "dialog",
'aria-hidden': "true",
'data-controller': "s-modal",
'data-s-modal-target': "modal",
'data-modal-action': "Export",
}, null);
exportModal.innerHTML = `
`;
elSavesHeader.parentElement.appendChild(exportModal);
// Create import/export buttons
const importListBtn = makeElem('button', {
'type': 'button',
'class': 'flex-item s-btn s-btn__muted s-btn__outlined js-import-list-modal',
'data-action': 's-modal#show',
'data-modal-action': 'Import',
}, 'Import list');
importListBtn.addEventListener('click', () => {
// Reset buttons and links
const importBtn = importModal.querySelector('button.s-btn__primary');
importBtn.previousElementSibling?.classList.add('d-none');
importBtn.classList.remove('d-none');
importBtn.nextElementSibling?.classList.remove('d-none');
// Reset textarea
const textarea = importModal.querySelector('textarea');
textarea.value = '';
textarea.focus();
// Show modal
importModal.setAttribute('aria-hidden', 'false');
});
const exportListBtn = makeElem('button', {
'type': 'button',
'class': 'flex-item s-btn s-btn__muted s-btn__outlined js-export-list-modal d-flex ai-center',
'data-action': 's-modal#show',
'data-modal-action': 'Export',
}, 'Export list');
exportListBtn.addEventListener('click', async () => {
// Get saved list items
StackExchange.helpers.addSpinner(exportListBtn);
const listItems = await getSavedListItems(currListId);
const delimitedList = listItems.map(v => Object.values(v).join(';')).join('\n');
const textarea = exportModal.querySelector('textarea');
textarea.value = delimitedList;
textarea.focus();
textarea.select();
StackExchange.helpers.removeSpinner(exportListBtn);
// Show modal
exportModal.setAttribute('aria-hidden', 'false');
});
const impExpBtnGroup = makeElem('div', {
'class': 'd-flex s-btn-group ml12'
}, null, [importListBtn, exportListBtn]);
// Wrap "Edit list" button in a div
const editListBtn = document.querySelector('.js-saves-lists-posts .js-open-list-modal');
const editListBtnWrapper = makeElem('div', {
'class': 'd-flex'
}, null, [editListBtn, impExpBtnGroup]);
elSavesHeader.appendChild(editListBtnWrapper);
// Add bulk checkboxes to each saved post
savesList.querySelectorAll('.js-saves-post-summary').forEach(item => {
const answer = item.querySelector('.s-post-summary--answer'); // saved post may be an answer
const pid = answer?.dataset.postId || item.dataset.postId;
const c = makeElem('input', {
'type': 'checkbox',
'class': 'saved-item-bulk-checkbox s-checkbox',
'value': pid
});
item.insertBefore(c, item.children[0]);
});
// Add import button handler
const importBtn = importModal.querySelector('button.s-btn__primary');
importBtn.addEventListener('click', async () => {
StackExchange.helpers.removeMessages();
const textarea = importModal.querySelector('textarea');
const toastElement = textarea.parentElement;
// Default: If only one line in textarea, assume semicolon, comma, or space-delimited
let postIds = textarea.value.split(/[,;\s]+/).filter(Number);
// If more than one line, assume one item per line
const rowItems = textarea.value.split(/[\n\r]+/);
if (rowItems.length > 1) {
postIds = rowItems.map(v => v.split(';')[0]).filter(Number);
}
// Unique post IDs
postIds = [...new Set(postIds)];
// If still no post IDs, show error
if (!postIds.length) {
StackExchange.helpers.showErrorMessage(toastElement, 'No post IDs found in textarea.');
return;
}
// Disable import button and show spinner
StackExchange.helpers.addSpinner(importBtn);
StackExchange.helpers.showSuccessMessage(toastElement, 'Importing items...');
importBtn.disabled = true;
// Import items
let successCount = 0, errorCount = 0, alreadySaved = 0;
for (let i = 0; i < postIds.length; i++) {
await delay(500); // delay to avoid rate-limiting
const saveRes = await saveItem(postIds[i], currListId);
// Couldn't save item, because post doesn't exist
if (!saveRes) {
errorCount++;
continue;
}
// Couldn't save item, because it was already saved
if (!saveRes.Success) alreadySaved++;
const moveRes = await moveSavedItem(postIds[i], currListId);
successCount++;
}
// Hide import and show refresh link
importBtn.previousElementSibling.classList.remove('d-none');
importBtn.classList.add('d-none');
importBtn.disabled = false;
importBtn.nextElementSibling.classList.add('d-none');
// Show success message
StackExchange.helpers.removeSpinner(importBtn);
StackExchange.helpers.showSuccessMessage(toastElement, !errorCount ?
`${successCount} unique post${pluralize(successCount)} imported successfully.` :
`${successCount} unique post${pluralize(successCount)} imported successfully (${errorCount} post${pluralize(errorCount)} doesn't exist).`
);
});
// Add export copy button handler
const exportCopyBtn = exportModal.querySelector('button.s-btn__primary');
exportCopyBtn.addEventListener('click', () => {
const textarea = exportModal.querySelector('textarea');
textarea.select();
copyToClipboard(textarea);
StackExchange.helpers.showSuccessMessage(exportCopyBtn.parentElement, 'Copied to clipboard.');
});
}
// On Q&A pages
else if (isOnQnaPages) {
// Nothing needed yet on page load
}
await updateMoveDropdown();
addEventListeners();
})();