Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New addon: sort assets by alphabetical order #7247

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ac3d7f2
start of this
Jazza-231 Mar 2, 2024
c09d2f8
costumes n sounds
Jazza-231 Mar 3, 2024
c13fab4
natural sort
Jazza-231 Mar 3, 2024
51a8302
its in the edit menu not the right click menu
Jazza-231 Mar 3, 2024
f186b35
smallest commit ever
Jazza-231 Mar 3, 2024
fc57fa9
dynam en dis
Jazza-231 Mar 3, 2024
fe480ca
what genius decided sprites would be 1 indexed
Jazza-231 Mar 3, 2024
1e67025
simple fix
Jazza-231 Mar 3, 2024
adb6b76
thanks joe
Jazza-231 Mar 3, 2024
9dbe13e
close edit menu
Jazza-231 Mar 4, 2024
0fe6471
edge case for dynamic enable
Jazza-231 Mar 6, 2024
a338081
misc
Jazza-231 Mar 6, 2024
019cc99
forgor about sprites
Jazza-231 Mar 6, 2024
39c7dce
close edit menu when button clicked - dont care if its disabled or not
Jazza-231 Mar 6, 2024
73552ca
Merge remote-tracking branch 'upstream/master' into alphabetical-order
Jazza-231 Jun 16, 2024
20d3eb7
Clean up, attempt to add restore old order, VERY broken rn
Jazza-231 Jun 16, 2024
0fa7579
More efficient sorting, but unsorting still broken
Jazza-231 Jun 16, 2024
8747bdd
Rewrite code so it directly sorts the arrays and handles restoring order
Joeclinton1 Jun 18, 2024
3eb07ad
rename TargetAndType to targetAndType and remove e var definition
Joeclinton1 Jun 18, 2024
3959d9e
Prettier
Jazza-231 Jun 18, 2024
0cc9b0c
Fix it not reselecting the previously selected costume/sound
Joeclinton1 Jun 18, 2024
2d130cf
Merge branch 'alphabetical-order' of https://github.com/Jazza-231/Scr…
Joeclinton1 Jun 18, 2024
8235cb9
use assetSelect from asset-conflict-dialog to select the asset via re…
Joeclinton1 Jun 18, 2024
9e67ee6
Move the util like functions all to the top for better organisation
Joeclinton1 Jun 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions addons-l10n/en/alphabetical-order.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"alphabetical-order/sprites": "Order sprites alphabetically",
"alphabetical-order/costumes": "Order costumes alphabetically",
"alphabetical-order/sounds": "Order sounds alphabetically"
}
1 change: 1 addition & 0 deletions addons/addons.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@
"place-backpack-code-at-cursor",
"big-save-button",
"asset-conflict-dialog",
"alphabetical-order",

"// NEW ADDONS ABOVE THIS ↑↑",
"// Note: these themes need this exact order to work properly,",
Expand Down
13 changes: 13 additions & 0 deletions addons/alphabetical-order/addon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "Sort assets in alphabetical order",
"description": "Adds an option in the Edit menu to sort assets in alphabetical order.",
"tags": ["codeEditor", "editor"],
"versionAdded": "1.39.0",
"userscripts": [{ "url": "userscript.js", "matches": ["projects"] }],
"credits": [
{ "name": "Jazza", "link": "https://scratch.mit.edu/users/greeny--231" },
{ "name": "Chrome_Cat", "link": "https://scratch.mit.edu/users/Chrome_Cat/" }
],
"dynamicDisable": true,
"dynamicEnable": true
}
201 changes: 201 additions & 0 deletions addons/alphabetical-order/userscript.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { assetSelect} from "../asset-conflict-dialog/utils.js";

export default async function ({ addon, msg, console }) {
const vm = addon.tab.traps.vm;

function getAssetName(asset) {
return asset.hasOwnProperty("sprite") ? asset.sprite.name : asset.name;
}

function compareAssetsByName(a, b) {
return String(getAssetName(a)).localeCompare(String(getAssetName(b)), undefined, {
numeric: true,
sensitivity: "base",
});
}

function isSortedAscending(arr, compare) {
return arr.every((v, i) => i === 0 || compare(arr[i - 1], v) <= 0);
}

function getTargetAssetsByType(target, type) {
return type === "costumes" ? target.sprite.costumes : type === "sounds" ? target.sprite.sounds : vm.runtime.targets;
}

function setTargetAssetsByType(target, type, assets) {
return type === "costumes"
? (target.sprite.costumes = assets)
: type === "sounds"
? (target.sprite.sounds = assets)
: (vm.runtime.targets = assets);
}

function getSelectedAssetName(){
return document.querySelector(
"[class*=sprite-selector-item_is-selected] [class*=sprite-selector-item_sprite-name]"
).innerText;
}

function selectAssetByName(assets, assetName, assetType){
if (assetType !== 'sprites') assetSelect(
addon,
assets.findIndex((e) => getAssetName(e) === assetName),
assetType === 'costumes' ? 'costume' : 'sound'
)
}

function wrapReplaceNameWithNameToIdUpdate(originalFunc, type){
return function(...args) {
// we only perform an update if the target and type match up with the target and type from when the user did the reorder operation
if (vm.editingTarget !== targetAndType[0] || type !== targetAndType[1]) return originalFunc.apply(this, args);

const [idxOrId, newName] = args;
const asset =
type === "sprites" ? vm.runtime.getTargetById(idxOrId) : getTargetAssetsByType(vm.editingTarget, type)[idxOrId];
const oldName = getAssetName(asset);

if (nameToOriginalIdx.has(oldName)) {
const value = nameToOriginalIdx.get(oldName);
nameToOriginalIdx.set(newName, value);
nameToOriginalIdx.delete(oldName);
}

return originalFunc.apply(this, args);
};
}

function restoreAssetsOrder(assetType) {
// This will still work even if assets are renamed or new assets are added
// however if assets are deleted it will break. So we assume that asset deletion, deactivates the restore function
const selectedAssetName = getSelectedAssetName();
let assetArray = getTargetAssetsByType(vm.editingTarget, assetType);
let newAssets = new Array (nameToOriginalIdx.size);
const leftovers = [];
assetArray.forEach((asset) => {
const newIdx = nameToOriginalIdx.get(getAssetName(asset));
if (newIdx === undefined) {
leftovers.push(asset);
} else {
newAssets[newIdx] = asset;
}
});
newAssets = newAssets.concat(leftovers);
setTargetAssetsByType(vm.editingTarget, assetType, newAssets);

// reselect the asset that was selected before the sort
selectAssetByName(newAssets, selectedAssetName, assetType);

// update to make sure what we see on screen correctly matches the array
vm.emitTargetsUpdate()
}

function sortAssetsAlphabetically() {
const assetType = menuItem.getAttribute("assetType");
const selectedAssetName = getSelectedAssetName();
const assets = getTargetAssetsByType(vm.editingTarget, assetType);

// get the id to original index map for use when restoring the order
nameToOriginalIdx = new Map();
assets.forEach((asset, idx) => nameToOriginalIdx.set(getAssetName(asset), idx));
targetAndType = [vm.editingTarget, assetType];

// sort assets alphabetically
if (assetType === "sprites") {
// in the case ordering sprites we must ensure that the stage remains untouched
assets.splice(1, assets.length - 1, ...assets.slice(1).sort(compareAssetsByName));
} else {
assets.sort(compareAssetsByName);
}

// reselect the asset that was selected before the sort
selectAssetByName(assets, selectedAssetName, assetType);

// update to make sure what we see on screen correctly matches the array
vm.emitTargetsUpdate()

// dispatch the redux event to close the menu and create the restore order button
addon.tab.redux.dispatch({ type: "scratch-gui/menus/CLOSE_MENU", menu: "editMenu" });

addon.tab.redux.dispatch({
type: "scratch-gui/restore-deletion/RESTORE_UPDATE",
state: {
restoreFun: () => restoreAssetsOrder(assetType),
deletedItem: assetType,
isRestoreOrder: true,
},
});

restoreOrderFunctionIsActive = true;
}

async function handleEditMenuOpened() {
editMenu = await addon.tab.waitForElement("[class*=menu_right]", { markAsSeen: true });
if (!addon.self.disabled) {
// if the restorOrder function is active then we hijack the restore button to use as it's action button
if (restoreOrderFunctionIsActive) {
const restoreButton = await addon.tab.waitForElement(
'[class*="menu-bar_menu-bar-item_"]:nth-child(4) [class*="menu_menu-item_"]:first-child > span'
);
restoreButton.innerText = "Restore previous order";
}

// handle adding the sortAlphabetical button to the edit menu
const tabIndex = addon.tab.redux.state.scratchGui.editorTab.activeTabIndex;
const assetType = ["sprites", "costumes", "sounds"][tabIndex];
const assetArray = getTargetAssetsByType(vm.editingTarget, assetType);
const slicedAssetArray = assetType === "sprites" ? assetArray.slice(1) : assetArray;
const isSorted = isSortedAscending(slicedAssetArray, compareAssetsByName);
menuItem.classList.toggle(addon.tab.scratchClass("menu-bar_disabled"), isSorted);

if (addon.tab.redux.state.scratchGui.menus.editMenu) {
menuItemLabel.textContent = msg(assetType);
menuItem.setAttribute("assetType", assetType);
editMenu.appendChild(menuItem);
}
}
}

function handleStateChangedEvents(action) {
const isEditMenuOpenedUpdate =
action.detail.action.type === "scratch-gui/menus/OPEN_MENU" && action.detail.action.menu === "editMenu";
const isRestoreUpdate =
action.detail.action && action.detail.action.type === "scratch-gui/restore-deletion/RESTORE_UPDATE";

if (isEditMenuOpenedUpdate) handleEditMenuOpened();
if (isRestoreUpdate && !action.detail.action.hasOwnProperty("isRestoreOrder")) restoreOrderFunctionIsActive = false;
}

// initialize module level variables to handle async changes
let editMenu;
let restoreOrderFunctionIsActive = false;
let nameToOriginalIdx = new Map();
let targetAndType = [null, null]; // used so we can know when we should update the nameToOriginalIdx map

// Create the menu item in the 'Edit' menu that when clicked triggers the alphabetical sort
const menuItem = document.createElement("li");
const menuItemLabel = document.createElement("span");
menuItem.classList = addon.tab.scratchClass("menu_menu-item", "menu_hoverable");
menuItem.appendChild(menuItemLabel);
menuItem.onclick = () => {
if (menuItem.classList.contains(addon.tab.scratchClass("menu-bar_disabled"))) return;
sortAssetsAlphabetically();
};

// handle the menu already being opened, and the addon being disabled/enabled
addon.self.addEventListener("disabled", () => (menuItem.style.display = "none"));
addon.self.addEventListener("reenabled", () => {
menuItem.style.display = "block";
if (addon.tab.redux.state.scratchGui.menus.editMenu) {
editMenu.appendChild(menuItem);
}
});

// add an event listener for state changes, that will handle the edit menu opening and setting our restore function to inactive
addon.tab.redux.initialize();
addon.tab.redux.addEventListener("statechanged", handleStateChangedEvents);

// pollute asset renaming functions to make sure `originalNameToId` stays accurate despite name changes
vm.renameCostume = wrapReplaceNameWithNameToIdUpdate(vm.renameCostume, "costumes");
vm.renameSound = wrapReplaceNameWithNameToIdUpdate(vm.renameSound, "sounds");
vm.renameSprite = wrapReplaceNameWithNameToIdUpdate(vm.renameSprite, "sprites");
}
Loading