Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 13 additions & 4 deletions apps/app-frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ import {
import { useError } from '@/store/error.js'
import { useInstall } from '@/store/install.js'
import { useLoading, useTheming } from '@/store/state'
import AnimatedIcon from './components/ui/AnimatedIcon.vue'

import { create_profile_and_install_from_file } from './helpers/pack'
import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
Expand Down Expand Up @@ -804,10 +805,10 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
:is-primary="() => route.path.startsWith('/browse') && !route.query.i"
:is-subpage="(route) => route.path.startsWith('/project') && !route.query.i"
>
<CompassIcon />
<AnimatedIcon :icon="CompassIcon" />
</NavButton>
<NavButton v-tooltip.right="'Skins (Beta)'" to="/skins">
<ChangeSkinIcon />
<AnimatedIcon :icon="ChangeSkinIcon" id="change-skin-icon" />
</NavButton>
<NavButton
v-tooltip.right="'Library'"
Expand All @@ -819,7 +820,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
route.query.i)
"
>
<LibraryIcon />
<AnimatedIcon :icon="LibraryIcon" id="library-icon" />
</NavButton>
<div class="h-px w-6 mx-auto my-2 bg-surface-5"></div>
<suspense>
Expand Down Expand Up @@ -877,7 +878,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
v-tooltip.right="formatMessage(commonMessages.settingsLabel)"
:to="() => $refs.settingsModal.show()"
>
<SettingsIcon />
<AnimatedIcon :icon="SettingsIcon" />
</NavButton>
<OverflowMenu
v-if="credentials"
Expand Down Expand Up @@ -1351,6 +1352,14 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
opacity: 0;
}

#change-skin-icon {
width: 3rem;
}

#library-icon {
height: 2rem;
}

@media (prefers-reduced-motion: no-preference) {
.toast-enter-active,
.nav-button-animated-enter-active {
Expand Down
96 changes: 96 additions & 0 deletions apps/app-frontend/src/components/ui/AnimatedIcon.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<template>
<component :is="icon" :class="{ animating: isAnimating }" ref="iconRef" />
</template>

<script setup>
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'

const props = defineProps({
icon: { type: Object, required: true },
})

const isAnimating = ref(false)
const iconRef = ref(null)
let target = null
let AnimationLength = 0
let hover = false
let isInfinite = false

onMounted(async () => {
await nextTick()
const el = iconRef.value?.$el || iconRef.value
if (!el) {
console.warn('AnimatedIcon: Unable to find DOM element.')
return
}

// calculate animation duration
el.classList.add('animating')
el.offsetHeight
AnimationLength = calculateLongestAnimation(el)
el.classList.remove('animating')

// use parent if it exists, otherwise fallback to component itself
target = el.parentElement || el
target.addEventListener('mouseenter', playAnimation)
target.addEventListener('mouseleave', stopHover)
})

onBeforeUnmount(() => {
if (!target) return
target.removeEventListener('mouseenter', playAnimation)
target.removeEventListener('mouseleave', stopHover)
})

const playAnimation = () => {
hover = true
if (isAnimating.value) return

isAnimating.value = true

setTimeout(() => {
isAnimating.value = false
if (isInfinite && hover) {
playAnimation()
}
}, AnimationLength)
}

function stopHover() {
hover = false
}

function calculateLongestAnimation(el) {
if (!el) return 0
let maxDuration = 0

const iconElements = [el, ...el.querySelectorAll('*')]
iconElements.forEach((child) => {
const style = getComputedStyle(child)
const durations = style.animationDuration.split(',').map((s) => parseTimeMs(s))
const delays = style.animationDelay.split(',').map((s) => parseTimeMs(s))
const iterations = style.animationIterationCount.split(',').map((s) => {
if (s === 'infinite') {
isInfinite = true
return 1
}
return parseFloat(s) || 1
})

durations.forEach((duration, i) => {
const delay = delays[i] || 0
const iter = iterations[i] || 1
const total = duration * iter + delay
if (total > maxDuration) maxDuration = total
})
})

return maxDuration
}

function parseTimeMs(s) {
const num = parseFloat(s)
if (!num) return 0
return num * (s.endsWith('ms') ? 1 : 1000)
}
</script>
13 changes: 8 additions & 5 deletions apps/app-frontend/src/components/ui/SearchCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<div class="icon w-[96px] h-[96px] relative">
<Avatar :src="project.icon_url" size="96px" class="search-icon origin-top transition-all" />
</div>
<div class="flex flex-col gap-2 overflow-hidden">
<div class="flex flex-col gap-2 flex-1 overflow-hidden">
<div class="gap-2 overflow-hidden no-wrap text-ellipsis">
<span class="text-lg font-extrabold text-contrast m-0 leading-none">
{{ project.title }}
Expand All @@ -24,7 +24,10 @@
<div class="m-0 line-clamp-2">
{{ project.description }}
</div>
<div v-if="categories.length > 0" class="mt-auto flex items-center gap-1 no-wrap">
<div
v-if="categories.length > 0"
class="mt-auto flex items-center gap-1 no-wrap [mask-image:linear-gradient(to_right,black_95%,transparent)]"
>
<TagsIcon class="h-4 w-4 shrink-0" />
<div
v-if="project.project_type === 'mod' || project.project_type === 'modpack'"
Expand Down Expand Up @@ -65,13 +68,13 @@
<div
v-for="tag in categories"
:key="tag"
class="text-sm font-semibold text-secondary flex gap-1 px-[0.375rem] py-0.5 bg-button-bg rounded-full"
class="text-sm font-semibold text-secondary flex gap-1 px-[0.4rem] py-0.5 bg-button-bg rounded-full"
>
{{ formatCategory(tag.name) }}
</div>
</div>
</div>
<div class="flex flex-col gap-2 items-end shrink-0 ml-auto">
<div class="flex flex-col gap-2 items-end shrink-0">
<div class="flex items-center gap-2">
<DownloadIcon class="shrink-0" />
<span>
Expand All @@ -87,7 +90,7 @@
</span>
</div>
<div class="mt-auto relative">
<div class="absolute bottom-0 right-0 w-fit">
<div class="bottom-0 right-0 w-fit">
<ButtonStyled color="brand" type="outlined">
<button
:disabled="installed || installing"
Expand Down
3 changes: 2 additions & 1 deletion apps/app-frontend/src/pages/Skins.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type { Ref } from 'vue'
import { computed, inject, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'

import type AccountsCard from '@/components/ui/AccountsCard.vue'
import AnimatedIcon from '@/components/ui/AnimatedIcon.vue'
import EditSkinModal from '@/components/ui/skin/EditSkinModal.vue'
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue'
Expand Down Expand Up @@ -353,7 +354,7 @@ await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()])
)
"
>
<UpdatedIcon />
<AnimatedIcon :icon="UpdatedIcon" />
Change cape
</button>
</ButtonStyled>
Expand Down
3 changes: 3 additions & 0 deletions apps/app-frontend/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export default defineConfig({
name: 'preset-default',
params: {
overrides: {
cleanupIds: false,
inlineStyles: false,
minifyStyles: false,
removeViewBox: false,
},
},
Expand Down
70 changes: 66 additions & 4 deletions packages/assets/icons/change-skin.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 32 additions & 1 deletion packages/assets/icons/compass.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
55 changes: 54 additions & 1 deletion packages/assets/icons/library.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading