feat: ws client & new backups frontend (#4813)

* feat: ws client

* feat: v1 backups endpoints

* feat: migrate backups page to api-client and new DI ctx

* feat: switch to ws client via api-client

* fix: disgust

* fix: stats

* fix: console

* feat: v0 backups api

* feat: migrate backups.vue to page system w/ components to ui pkgs

* feat: polish backups frontend

* feat: pending refactor for ws handling of backups

* fix: vue shit

* fix: cancel logic fix

* fix: qa issues

* fix: alignment issues for backups page

* fix: bar positioning

* feat: finish QA

* fix: icons

* fix: lint & i18n

* fix: clear comment

* lint

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
Calum H.
2025-12-04 02:32:03 +00:00
committed by GitHub
parent e3444a3456
commit 8eff939039
43 changed files with 2466 additions and 1177 deletions

View File

@@ -43,6 +43,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
})
const accentedButton = computed(() =>
@@ -69,6 +73,7 @@ const classes = computed(() => {
'btn-hover-filled-only': props.hoverFilledOnly,
'btn-outline': props.outline,
'color-accent-contrast': accentedButton,
disabled: props.disabled,
}
})
</script>
@@ -78,10 +83,14 @@ const classes = computed(() => {
v-if="link && link.startsWith('/')"
class="btn"
:class="classes"
:to="link"
:to="disabled ? '' : link"
:target="external ? '_blank' : '_self'"
@click="
(event) => {
if (disabled) {
event.preventDefault()
return
}
if (action) {
action(event)
}
@@ -96,10 +105,14 @@ const classes = computed(() => {
v-else-if="link"
class="btn"
:class="classes"
:href="link"
:href="disabled ? undefined : link"
:target="external ? '_blank' : '_self'"
@click="
(event) => {
if (disabled) {
event.preventDefault()
return
}
if (action) {
action(event)
}
@@ -110,7 +123,7 @@ const classes = computed(() => {
<ExternalIcon v-if="external && !iconOnly" class="external-icon" />
<UnknownIcon v-if="!$slots.default" />
</a>
<button v-else class="btn" :class="classes" @click="action">
<button v-else class="btn" :class="classes" :disabled="disabled" @click="action">
<slot />
<UnknownIcon v-if="!$slots.default" />
</button>

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import SpinnerIcon from '@modrinth/assets/icons/spinner.svg'
import { computed } from 'vue'
const props = withDefaults(
@@ -7,11 +8,21 @@ const props = withDefaults(
max?: number
color?: 'brand' | 'green' | 'red' | 'orange' | 'blue' | 'purple' | 'gray'
waiting?: boolean
fullWidth?: boolean
striped?: boolean
gradientBorder?: boolean
label?: string
labelClass?: string
showProgress?: boolean
}>(),
{
max: 1,
color: 'brand',
waiting: false,
fullWidth: false,
striped: false,
gradientBorder: true,
showProgress: false,
},
)
@@ -49,15 +60,28 @@ const colors = {
const percent = computed(() => props.progress / props.max)
</script>
<template>
<div
class="flex w-full max-w-[15rem] h-1 rounded-full overflow-hidden"
:class="colors[props.color].bg"
>
<div
class="rounded-full progress-bar"
:class="[colors[props.color].fg, { 'progress-bar--waiting': waiting }]"
:style="!waiting ? { width: `${percent * 100}%` } : {}"
></div>
<div class="flex w-full flex-col gap-2" :class="fullWidth ? '' : 'max-w-[15rem]'">
<div v-if="label || showProgress" class="flex items-center justify-between">
<span v-if="label" :class="labelClass">{{ label }}</span>
<div v-if="showProgress" class="flex items-center gap-1 text-sm text-secondary">
<span>{{ Math.round(percent * 100) }}%</span>
<slot name="progress-icon">
<SpinnerIcon class="size-5 animate-spin" />
</slot>
</div>
</div>
<div class="flex h-2 w-full overflow-hidden rounded-full" :class="[colors[props.color].bg]">
<div
class="rounded-full progress-bar"
:class="[
colors[props.color].fg,
{ 'progress-bar--waiting': waiting },
{ 'progress-bar--gradient-border': gradientBorder },
striped ? `progress-bar--striped--${color}` : '',
]"
:style="!waiting ? { width: `${percent * 100}%` } : {}"
></div>
</div>
</div>
</template>
<style scoped lang="scss">
@@ -83,4 +107,76 @@ const percent = computed(() => props.progress / props.max)
width: 20%;
}
}
.progress-bar--gradient-border {
position: relative;
&::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.3), transparent);
border-radius: inherit;
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: xor;
padding: 2px;
pointer-events: none;
}
}
%progress-bar--striped-common {
background-attachment: scroll;
background-position: 0 0;
background-size: 9.38px 9.38px;
}
@mixin striped-background($color-variable) {
background-image: linear-gradient(
135deg,
$color-variable 11.54%,
transparent 11.54%,
transparent 50%,
$color-variable 50%,
$color-variable 61.54%,
transparent 61.54%,
transparent 100%
);
}
.progress-bar--striped--brand {
@include striped-background(var(--color-brand));
@extend %progress-bar--striped-common;
}
.progress-bar--striped--green {
@include striped-background(var(--color-green));
@extend %progress-bar--striped-common;
}
.progress-bar--striped--red {
@include striped-background(var(--color-red));
@extend %progress-bar--striped-common;
}
.progress-bar--striped--orange {
@include striped-background(var(--color-orange));
@extend %progress-bar--striped-common;
}
.progress-bar--striped--blue {
@include striped-background(var(--color-blue));
@extend %progress-bar--striped-common;
}
.progress-bar--striped--purple {
@include striped-background(var(--color-purple));
@extend %progress-bar--striped-common;
}
.progress-bar--striped--gray {
@include striped-background(var(--color-divider-dark));
@extend %progress-bar--striped-common;
}
</style>