feat: create AppearingProgressBar component

This commit is contained in:
Calum 2025-07-10 17:50:24 +01:00
parent 286ab6d4a0
commit 59ab09a275
3 changed files with 146 additions and 108 deletions

View File

@ -1,36 +1,7 @@
<template>
<NewModal ref="mrpackModal" header="Uploading mrpack" :closable="!isLoading" @show="onShow">
<div class="flex flex-col gap-4 md:w-[600px]">
<Transition
enter-active-class="transition-all duration-300 ease-out"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-20"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 max-h-20"
leave-to-class="opacity-0 max-h-0"
>
<div v-if="isLoading" class="w-full">
<div class="mb-2 flex justify-between text-sm">
<Transition name="phrase-fade" mode="out-in">
<span :key="currentPhrase" class="text-lg font-medium text-contrast">{{
currentPhrase
}}</span>
</Transition>
<div class="flex flex-col items-end">
<span class="text-secondary">{{ Math.round(uploadProgress) }}%</span>
<span class="text-xs text-secondary"
>{{ formatBytes(uploadedBytes) }} / {{ formatBytes(totalBytes) }}</span
>
</div>
</div>
<div class="h-2 w-full rounded-full bg-divider">
<div
class="h-2 animate-pulse rounded-full bg-brand transition-all duration-300 ease-out"
:style="{ width: `${uploadProgress}%` }"
></div>
</div>
</div>
</Transition>
<AppearingProgressBar :max-value="totalBytes" :current-value="uploadedBytes" />
<Transition
enter-active-class="transition-all duration-300 ease-out"
@ -144,7 +115,7 @@
</template>
<script setup lang="ts">
import { BackupWarning, ButtonStyled, NewModal } from "@modrinth/ui";
import { BackupWarning, ButtonStyled, NewModal, AppearingProgressBar } from "@modrinth/ui";
import {
UploadIcon,
RightArrowIcon,
@ -187,50 +158,9 @@ const hardReset = ref(false);
const isLoading = ref(false);
const loadingServerCheck = ref(false);
const mrpackFile = ref<File | null>(null);
const uploadProgress = ref(0);
const uploadedBytes = ref(0);
const totalBytes = ref(0);
const uploadPhrases = [
"Removing Herobrine...",
"Feeding parrots...",
"Teaching villagers new trades...",
"Convincing creepers to be friendly...",
"Polishing diamonds...",
"Training wolves to fetch...",
"Building pixel art...",
"Explaining redstone to beginners...",
"Collecting all the cats...",
"Negotiating with endermen...",
"Planting suspicious stew ingredients...",
"Calibrating TNT blast radius...",
"Teaching chickens to fly...",
"Sorting inventory alphabetically...",
"Convincing iron golems to smile...",
];
const currentPhrase = ref("Uploading...");
let phraseInterval: NodeJS.Timeout | null = null;
const usedPhrases = ref(new Set<number>());
const getNextPhrase = () => {
if (usedPhrases.value.size >= uploadPhrases.length) {
const currentPhraseIndex = uploadPhrases.indexOf(currentPhrase.value);
usedPhrases.value.clear();
if (currentPhraseIndex !== -1) {
usedPhrases.value.add(currentPhraseIndex);
}
}
const availableIndices = uploadPhrases
.map((_, index) => index)
.filter((index) => !usedPhrases.value.has(index));
const randomIndex = availableIndices[Math.floor(Math.random() * availableIndices.length)];
usedPhrases.value.add(randomIndex);
return uploadPhrases[randomIndex];
};
const isDangerous = computed(() => hardReset.value);
const canInstall = computed(() => !mrpackFile.value || isLoading.value || loadingServerCheck.value);
@ -259,31 +189,17 @@ const handleReinstall = async () => {
}
isLoading.value = true;
uploadProgress.value = 0;
uploadProgress.value = 0;
uploadedBytes.value = 0;
totalBytes.value = mrpackFile.value.size;
currentPhrase.value = getNextPhrase();
phraseInterval = setInterval(() => {
currentPhrase.value = getNextPhrase();
}, 4500);
const { onProgress, promise } = props.server.general.reinstallFromMrpack(
mrpackFile.value,
hardReset.value,
);
onProgress(({ loaded, total, progress }) => {
uploadProgress.value = progress;
onProgress(({ loaded, total }) => {
uploadedBytes.value = loaded;
totalBytes.value = total;
if (phraseInterval && progress >= 100) {
clearInterval(phraseInterval);
phraseInterval = null;
currentPhrase.value = "Installing modpack...";
}
});
try {
@ -316,10 +232,6 @@ const handleReinstall = async () => {
}
} finally {
isLoading.value = false;
if (phraseInterval) {
clearInterval(phraseInterval);
phraseInterval = null;
}
}
};
const onShow = () => {
@ -328,15 +240,8 @@ const onShow = () => {
loadingServerCheck.value = false;
isLoading.value = false;
mrpackFile.value = null;
uploadProgress.value = 0;
uploadedBytes.value = 0;
totalBytes.value = 0;
currentPhrase.value = "Uploading...";
usedPhrases.value.clear();
if (phraseInterval) {
clearInterval(phraseInterval);
phraseInterval = null;
}
};
const show = () => mrpackModal.value?.show();
@ -349,14 +254,4 @@ defineExpose({ show, hide });
.stylized-toggle:checked::after {
background: var(--color-accent-contrast) !important;
}
.phrase-fade-enter-active,
.phrase-fade-leave-active {
transition: opacity 0.3s ease;
}
.phrase-fade-enter-from,
.phrase-fade-leave-to {
opacity: 0;
}
</style>

View File

@ -0,0 +1,142 @@
<template>
<Transition
enter-active-class="transition-all duration-300 ease-out"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-20"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 max-h-20"
leave-to-class="opacity-0 max-h-0"
>
<div v-if="isVisible" class="w-full">
<div class="mb-2 flex justify-between text-sm">
<Transition name="phrase-fade" mode="out-in">
<span :key="currentPhrase" class="text-lg font-medium text-contrast">{{
currentPhrase
}}</span>
</Transition>
<div class="flex flex-col items-end">
<span class="text-secondary">{{ Math.round(progress) }}%</span>
<span class="text-xs text-secondary"
>{{ formatBytes(currentValue) }} / {{ formatBytes(maxValue) }}</span
>
</div>
</div>
<div class="h-2 w-full rounded-full bg-divider">
<div
class="h-2 animate-pulse bg-brand rounded-full transition-all duration-300 ease-out"
:style="{ width: `${progress}%` }"
></div>
</div>
</div>
</Transition>
</template>
<script setup lang="ts">
import { formatBytes } from '@modrinth/utils'
import { computed, onUnmounted, ref, watch } from 'vue'
interface Props {
maxValue: number
currentValue: number
tips?: string[]
}
const props = withDefaults(defineProps<Props>(), {
tips: () => [
'Removing Herobrine...',
'Feeding parrots...',
'Teaching villagers new trades...',
'Convincing creepers to be friendly...',
'Polishing diamonds...',
'Training wolves to fetch...',
'Building pixel art...',
'Explaining redstone to beginners...',
'Collecting all the cats...',
'Negotiating with endermen...',
'Planting suspicious stew ingredients...',
'Calibrating TNT blast radius...',
'Teaching chickens to fly...',
'Sorting inventory alphabetically...',
'Convincing iron golems to smile...',
],
})
const currentPhrase = ref('')
const usedPhrases = ref(new Set<number>())
let phraseInterval: NodeJS.Timeout | null = null
const progress = computed(() => {
if (props.maxValue === 0) return 0
return Math.min((props.currentValue / props.maxValue) * 100, 100)
})
const isVisible = computed(() => props.maxValue > 0 && props.currentValue >= 0)
function getNextPhrase() {
if (usedPhrases.value.size >= props.tips.length) {
const currentPhraseIndex = props.tips.indexOf(currentPhrase.value)
usedPhrases.value.clear()
if (currentPhraseIndex !== -1) {
usedPhrases.value.add(currentPhraseIndex)
}
}
const availableIndices = props.tips
.map((_, index) => index)
.filter((index) => !usedPhrases.value.has(index))
const randomIndex = availableIndices[Math.floor(Math.random() * availableIndices.length)]
usedPhrases.value.add(randomIndex)
return props.tips[randomIndex]
}
function startPhraseRotation() {
if (phraseInterval) {
clearInterval(phraseInterval)
}
currentPhrase.value = getNextPhrase()
phraseInterval = setInterval(() => {
currentPhrase.value = getNextPhrase()
}, 4500)
}
function stopPhraseRotation() {
if (phraseInterval) {
clearInterval(phraseInterval)
phraseInterval = null
}
}
watch(isVisible, (newVisible) => {
if (newVisible) {
startPhraseRotation()
} else {
stopPhraseRotation()
usedPhrases.value.clear()
}
})
watch(progress, (newProgress) => {
if (newProgress >= 100) {
stopPhraseRotation()
currentPhrase.value = 'Installing modpack...'
}
})
onUnmounted(() => {
stopPhraseRotation()
})
</script>
<style scoped>
.phrase-fade-enter-active,
.phrase-fade-leave-active {
transition: opacity 0.3s ease;
}
.phrase-fade-enter-from,
.phrase-fade-leave-to {
opacity: 0;
}
</style>

View File

@ -1,6 +1,7 @@
// Base content
export { default as Accordion } from './base/Accordion.vue'
export { default as Admonition } from './base/Admonition.vue'
export { default as AppearingProgressBar } from './base/AppearingProgressBar.vue'
export { default as AutoLink } from './base/AutoLink.vue'
export { default as Avatar } from './base/Avatar.vue'
export { default as Badge } from './base/Badge.vue'