diff --git a/apps/app-frontend/package.json b/apps/app-frontend/package.json
index 9df74bb686..18ff367db0 100644
--- a/apps/app-frontend/package.json
+++ b/apps/app-frontend/package.json
@@ -44,7 +44,8 @@
"vue": "^3.5.13",
"vue-i18n": "^10.0.0",
"vue-router": "^4.6.0",
- "vue-virtual-scroller": "v2.0.0-beta.8"
+ "vue-virtual-scroller": "v2.0.0-beta.8",
+ "vuedraggable": "^4.1.0"
},
"devDependencies": {
"@eslint/compat": "^1.1.1",
diff --git a/apps/app-frontend/src/components/ui/skin/VirtualSkinSectionList.vue b/apps/app-frontend/src/components/ui/skin/VirtualSkinSectionList.vue
index 43430cfa0a..fc6054ef3f 100644
--- a/apps/app-frontend/src/components/ui/skin/VirtualSkinSectionList.vue
+++ b/apps/app-frontend/src/components/ui/skin/VirtualSkinSectionList.vue
@@ -1,5 +1,12 @@
@@ -316,8 +346,24 @@ defineExpose({ getAddSkinButtonElement })
]"
:style="{ transform: `translateY(${top}px)` }"
>
+
+
+
+
+
+
-
-
-
-
-
- {{ formatMessage(messages.addSkinButton) }}
- {{ formatMessage(messages.dragAndDropSubtitle) }}
-
+
+
+
+
+
+ {{ formatMessage(messages.addSkinButton) }}
+ {{ formatMessage(messages.dragAndDropSubtitle) }}
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/app-frontend/src/helpers/skins.ts b/apps/app-frontend/src/helpers/skins.ts
index 9a05df55de..e0d17fe0e2 100644
--- a/apps/app-frontend/src/helpers/skins.ts
+++ b/apps/app-frontend/src/helpers/skins.ts
@@ -142,6 +142,12 @@ export async function remove_custom_skin(skin: Skin): Promise {
})
}
+export async function set_custom_skin_order(textureKeys: string[]): Promise {
+ await invoke('plugin:minecraft-skins|set_custom_skin_order', {
+ textureKeys,
+ })
+}
+
export async function save_custom_skin(
skin: Skin,
textureBlob: Uint8Array,
diff --git a/apps/app-frontend/src/pages/Skins.vue b/apps/app-frontend/src/pages/Skins.vue
index 58bb228f50..f48d068a51 100644
--- a/apps/app-frontend/src/pages/Skins.vue
+++ b/apps/app-frontend/src/pages/Skins.vue
@@ -46,6 +46,7 @@ import {
get_normalized_skin_texture,
normalize_skin_texture,
remove_custom_skin,
+ set_custom_skin_order,
} from '@/helpers/skins.ts'
import { hasPride26Badge } from '@/helpers/user-campaigns.ts'
import { handleSevereError } from '@/store/error'
@@ -269,6 +270,7 @@ let isUnmounted = false
const isDraggingSkinFile = ref(false)
const isAddSkinButtonDragActive = ref(false)
+const isSavedSkinReorderMode = ref(false)
const deleteSkinModal = ref()
const skinToDelete = ref(null)
@@ -480,6 +482,44 @@ function updateLocalSkin(savedSkin: Skin, applied: boolean, previousSkin?: Skin)
generateSkinPreviews(skins.value, capes.value)
}
+function reorderLocalSavedSkins(textureKeys: string[]) {
+ const requestedKeys = new Set(textureKeys)
+ const savedSkinList = skins.value.filter((skin) => skin.source !== 'default')
+ const savedSkinByKey = new Map(savedSkinList.map((skin) => [skin.texture_key, skin]))
+ const orderedSavedSkins = [
+ ...textureKeys.map((key) => savedSkinByKey.get(key)).filter((skin): skin is Skin => !!skin),
+ ...savedSkinList.filter((skin) => !requestedKeys.has(skin.texture_key)),
+ ]
+ let savedSkinIndex = 0
+
+ skins.value = skins.value.flatMap((skin) => {
+ if (skin.source === 'default') {
+ return [skin]
+ }
+
+ const nextSkin = orderedSavedSkins[savedSkinIndex++]
+ return nextSkin ? [nextSkin] : []
+ })
+}
+
+async function onSavedSkinsReordered(textureKeys: string[]) {
+ const previousSkins = skins.value
+
+ reorderLocalSavedSkins(textureKeys)
+
+ try {
+ await set_custom_skin_order(textureKeys)
+ } catch (error) {
+ skins.value = previousSkins
+ handleError(error as Error)
+ await loadSkins()
+ }
+}
+
+function toggleSavedSkinReorderMode() {
+ isSavedSkinReorderMode.value = !isSavedSkinReorderMode.value
+}
+
function schedulePendingSkinRefresh() {
if (pendingSkinRefreshTimeout !== null) {
window.clearTimeout(pendingSkinRefreshTimeout)
@@ -853,7 +893,7 @@ await loadSkins()