diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 760c8d47e..caed5e872 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -3,6 +3,7 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue' import { RouterView, useRoute, useRouter } from 'vue-router' import { ArrowBigUpDashIcon, + ChangeSkinIcon, CompassIcon, DownloadIcon, HomeIcon, @@ -399,6 +400,9 @@ function handleAuxClick(e) { > + + + -import { ref } from 'vue' +import { ref, useTemplateRef } from 'vue' import { NewModal as Modal } from '@modrinth/ui' import { show_ads_window, hide_ads_window } from '@/helpers/ads.js' import { useTheming } from '@/store/theme.ts' @@ -26,16 +26,16 @@ const props = defineProps({ default: true, }, }) -const modal = ref(null) +const modal = useTemplateRef('modal') defineExpose({ - show: () => { + show: (e: MouseEvent) => { hide_ads_window() - modal.value.show() + modal.value?.show(e) }, hide: () => { onModalHide() - modal.value.hide() + modal.value?.hide() }, }) diff --git a/apps/app-frontend/src/components/ui/skin/CapeButton.vue b/apps/app-frontend/src/components/ui/skin/CapeButton.vue new file mode 100644 index 000000000..b05b4e3b7 --- /dev/null +++ b/apps/app-frontend/src/components/ui/skin/CapeButton.vue @@ -0,0 +1,85 @@ + + + + + + + + + + + + diff --git a/apps/app-frontend/src/components/ui/skin/EditSkinModal.vue b/apps/app-frontend/src/components/ui/skin/EditSkinModal.vue new file mode 100644 index 000000000..6c65d2285 --- /dev/null +++ b/apps/app-frontend/src/components/ui/skin/EditSkinModal.vue @@ -0,0 +1,33 @@ + + + + + Edit skin + + + + + + + + diff --git a/apps/app-frontend/src/components/ui/skin/SelectCapeModal.vue b/apps/app-frontend/src/components/ui/skin/SelectCapeModal.vue new file mode 100644 index 000000000..ae750773c --- /dev/null +++ b/apps/app-frontend/src/components/ui/skin/SelectCapeModal.vue @@ -0,0 +1,77 @@ + + + + + Selecting a cape + + + + + + + + + + + + + + + + + + Select + + + + + + Cancel + + + + + diff --git a/apps/app-frontend/src/components/ui/skin/SkinButton.vue b/apps/app-frontend/src/components/ui/skin/SkinButton.vue new file mode 100644 index 000000000..13c162272 --- /dev/null +++ b/apps/app-frontend/src/components/ui/skin/SkinButton.vue @@ -0,0 +1,181 @@ + + + + + + + + + + + + emit('edit', e)" + > + + {{ formatMessage(commonMessages.editButton) }} + + + + + + + diff --git a/apps/app-frontend/src/helpers/skins.ts b/apps/app-frontend/src/helpers/skins.ts new file mode 100644 index 000000000..ded98c531 --- /dev/null +++ b/apps/app-frontend/src/helpers/skins.ts @@ -0,0 +1,64 @@ +import { invoke } from '@tauri-apps/api/core' + +export interface Cape { + id: string + name: string + texture: string + is_default: boolean + is_equipped: boolean +} + +export type SkinModel = 'Classic' | 'Slim' | 'Unknown' +export type SkinSource = 'Default' | 'CustomExternal' | 'Custom' + +export interface Skin { + texture_key: string + name?: string + variant: SkinModel + cape_id?: string + texture: string + source: SkinSource + is_equipped: boolean +} + +export async function get_available_capes(): Promise { + return await invoke('plugin:minecraft-skins|get_available_capes', {}) +} + +export async function get_available_skins(): Promise { + return await invoke('plugin:minecraft-skins|get_available_skins', {}) +} + +export async function add_and_equip_custom_skin( + texture_blob: Uint8Array, + variant: SkinModel, + cape_override?: Cape, +): Promise { + await invoke('plugin:minecraft-skins|add_and_equip_custom_skin', { + texture_blob, + variant, + cape_override, + }) +} + +export async function set_default_cape(cape?: Cape): Promise { + await invoke('plugin:minecraft-skins|set_default_cape', { + cape, + }) +} + +export async function equip_skin(skin: Skin): Promise { + await invoke('plugin:minecraft-skins|equip_skin', { + skin, + }) +} + +export async function remove_custom_skin(skin: Skin): Promise { + await invoke('plugin:minecraft-skins|remove_custom_skin', { + skin, + }) +} + +export async function unequip_skin(): Promise { + await invoke('plugin:minecraft-skins|unequip_skin') +} diff --git a/apps/app-frontend/src/pages/Index.vue b/apps/app-frontend/src/pages/Index.vue index e5c1e0689..14776c2bb 100644 --- a/apps/app-frontend/src/pages/Index.vue +++ b/apps/app-frontend/src/pages/Index.vue @@ -10,6 +10,7 @@ import dayjs from 'dayjs' import { get_search_results } from '@/helpers/cache.js' import type { SearchResult } from '@modrinth/utils' import RecentWorldsList from '@/components/ui/world/RecentWorldsList.vue' +import type { GameInstance } from '@/helpers/types' const route = useRoute() const breadcrumbs = useBreadcrumbs() @@ -82,7 +83,7 @@ async function refreshFeaturedProjects() { await fetchInstances() await refreshFeaturedProjects() -const unlistenProfile = await profile_listener(async (e) => { +const unlistenProfile = await profile_listener(async (e: { event: string; profile_path_id: string }) => { await fetchInstances() if (e.event === 'added' || e.event === 'created' || e.event === 'removed') { @@ -97,8 +98,8 @@ onUnmounted(() => { - Welcome back! - Welcome to Modrinth App! + Welcome back! + Welcome to Modrinth App! +import { UpdatedIcon, PlusIcon } from '@modrinth/assets' +import { ButtonStyled } from '@modrinth/ui' +import { ref, computed, useTemplateRef } from 'vue' +import SkinButton from '@/components/ui/skin/SkinButton.vue' +import EditSkinModal from '@/components/ui/skin/EditSkinModal.vue' +import type { Cape, Skin, SkinModel } from '@/helpers/skins.ts' +import { get_available_skins, get_available_capes } from '@/helpers/skins.ts' +import { handleError } from '@/store/notifications' +import CapeButton from '@/components/ui/skin/CapeButton.vue' +import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue' + +const editSkinModal = useTemplateRef('editSkinModal') +const selectCapeModal = useTemplateRef('selectCapeModal') + +const selectedSkin = ref('ProspectorDev') +const previewSkin = computed(() => `https://vzge.me/full/350/${selectedSkin.value}.png?no=ears`) + +const savedSkins = computed(() => skins.value.filter((skin) => skin.source !== 'Default')) +const defaultSkins = computed(() => + skins.value + .filter( + (skin) => + skin.source === 'Default' && (!skin.name || skin.variant === defaultModels[skin.name]), + ) + .sort((a, b) => { + if (!a.name || !defaultModelSorting.includes(a.name)) { + return 1 + } else if (!b.name || !defaultModelSorting.includes(b.name)) { + return -1 + } + + return defaultModelSorting.indexOf(a.name) - defaultModelSorting.indexOf(b.name) + }), +) +const currentCape = ref() + +const defaultModelSorting = ['Steve', 'Alex'] + +const defaultModels: Record = { + Steve: 'Classic', + Alex: 'Slim', + Zuri: 'Classic', + Sunny: 'Classic', + Noor: 'Slim', + Makena: 'Slim', + Kai: 'Classic', + Efe: 'Slim', + Ari: 'Classic', +} + +const skins = ref([]) +const capes = ref([]) + +await loadCapes() +await loadSkins() + +async function loadCapes() { + await get_available_capes() + .then((c) => { + capes.value = c + currentCape.value = capes.value.find((cape) => cape.is_equipped) + console.log(c) + }) + .catch((err) => handleError(err)) +} + +async function loadSkins() { + await get_available_skins() + .then((s) => { + skins.value = s + console.log(s) + }) + .catch((err) => handleError(err)) +} + + + + + + + + Skins + + + selectCapeModal?.show(e, selectedSkin, currentCape)"> + + Change cape + + + + + + + + + + + Saved skins + + + + Add a skin + + + + + + Default skins + + + + + + + + diff --git a/apps/app-frontend/src/pages/index.js b/apps/app-frontend/src/pages/index.js index 82b0b3ec2..2e0361cd5 100644 --- a/apps/app-frontend/src/pages/index.js +++ b/apps/app-frontend/src/pages/index.js @@ -1,5 +1,6 @@ import Index from './Index.vue' import Browse from './Browse.vue' import Worlds from './Worlds.vue' +import Skins from './Skins.vue' -export { Index, Browse, Worlds } +export { Index, Browse, Worlds, Skins } diff --git a/apps/app-frontend/src/routes.js b/apps/app-frontend/src/routes.js index 6d5e4e372..67172e68d 100644 --- a/apps/app-frontend/src/routes.js +++ b/apps/app-frontend/src/routes.js @@ -34,6 +34,14 @@ export default new createRouter({ breadcrumb: [{ name: 'Discover content' }], }, }, + { + path: '/skins', + name: 'Skins', + component: Pages.Skins, + meta: { + breadcrumb: [{ name: 'Skins' }], + }, + }, { path: '/library', name: 'Library', diff --git a/packages/assets/icons/change-skin.svg b/packages/assets/icons/change-skin.svg new file mode 100644 index 000000000..762605150 --- /dev/null +++ b/packages/assets/icons/change-skin.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/assets/index.ts b/packages/assets/index.ts index df7df4247..b79b62ed5 100644 --- a/packages/assets/index.ts +++ b/packages/assets/index.ts @@ -57,6 +57,7 @@ import _BoxImportIcon from './icons/box-import.svg?component' import _BracesIcon from './icons/braces.svg?component' import _CalendarIcon from './icons/calendar.svg?component' import _CardIcon from './icons/card.svg?component' +import _ChangeSkinIcon from './icons/change-skin.svg?component' import _ChartIcon from './icons/chart.svg?component' import _CheckIcon from './icons/check.svg?component' import _CheckCheckIcon from './icons/check-check.svg?component' @@ -276,6 +277,7 @@ export const BoxIcon = _BoxIcon export const BoxImportIcon = _BoxImportIcon export const BracesIcon = _BracesIcon export const CalendarIcon = _CalendarIcon +export const ChangeSkinIcon = _ChangeSkinIcon export const ChartIcon = _ChartIcon export const CheckIcon = _CheckIcon export const CheckCheckIcon = _CheckCheckIcon diff --git a/packages/ui/src/components/base/ScrollablePanel.vue b/packages/ui/src/components/base/ScrollablePanel.vue index 35766efd4..f84ba3cee 100644 --- a/packages/ui/src/components/base/ScrollablePanel.vue +++ b/packages/ui/src/components/base/ScrollablePanel.vue @@ -55,6 +55,7 @@ onUnmounted(() => { } }) function updateFade(scrollTop, offsetHeight, scrollHeight) { + console.log(scrollTop, offsetHeight, scrollHeight) scrollableAtBottom.value = Math.ceil(scrollTop + offsetHeight) >= scrollHeight scrollableAtTop.value = scrollTop <= 0 } @@ -64,6 +65,18 @@ function onScroll({ target: { scrollTop, offsetHeight, scrollHeight } }) {