You've already forked AstralRinth
forked from didirus/AstralRinth
Reworked app update flow (#3960)
* Make theseus capable of logging messages from the `log` crate * Move update checking entirely into JS and open a modal if an update is available * Fix formatjs on Windows and run formatjs * Add in the buttons and body * Fix lint * Show update size in modal * Fix update not being rechecked if the update modal was directly dismissed * Slight UI tweaks * Fix lint * Implement skipping the update * Implement the Update Now button * Implement updating at next exit * Turn download progress into an error bar on failure * Restore 5 minute update check instead of 30 seconds * Fix PendingUpdateData being seen as a unit struct * Fix lint * Make CI also lint updater code * feat: create AppearingProgressBar component * feat: polish update available modal * feat: add error handling * Open changelog with tauri-plugin-opener * Run intl:extract * Update completion toasts (#3978) * Use single LAUNCHER_USER_AGENT constant for all user agents * Fix build on Mac * Request the update size with HEAD instead of GET * UI tweaks * lint * Fix lint * fix: hide modal header & add "Hide update reminder" button w/ tooltip * Run intl:extract * fix: lint issues * fix: merge issues * notifications.js no longer exists * Add metered network checking * Add a timeout to macOS is_network_metered * Fix tauri.conf.json * vibe debugging * Set a dispatch queue * Have a popup that asks you if you'd like to disable automatic file downloads if you're on a metered network * Move UpdateModal to modal package * Fix lint * Add a toggle for automatic downloads * Fix type Co-authored-by: Alejandro González <7822554+AlexTMjugador@users.noreply.github.com> Signed-off-by: Josiah Glosson <soujournme@gmail.com> * Redo updating UI and experience * lint * fix unlistener issue * remove unneeded translation keys * Fix expose issue * temp disable cranelift, tweak some messages * change version back * Clean up App.vue * move toast to top right * update reload icon * Fixed the bug!!!!!!!!!!!! * improve messages * intl:extract * Add liquid glass icon file * not you! * use dependency injection * lint on apple icon * Fix imports, move download size to button * change update check back to 5 mins * lint + move to providers * intl:extract --------- Signed-off-by: Cal H. <hendersoncal117@gmail.com> Signed-off-by: Josiah Glosson <soujournme@gmail.com> Co-authored-by: Calum <calum@modrinth.com> Co-authored-by: Prospector <prospectordev@gmail.com> Co-authored-by: Cal H. <hendersoncal117@gmail.com> Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com> Co-authored-by: Alejandro González <7822554+AlexTMjugador@users.noreply.github.com>
This commit is contained in:
140
packages/ui/src/components/base/AppearingProgressBar.vue
Normal file
140
packages/ui/src/components/base/AppearingProgressBar.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<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-md font-semibold">{{ 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>
|
||||
121
packages/ui/src/components/base/JoinedButtons.vue
Normal file
121
packages/ui/src/components/base/JoinedButtons.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div class="joined-buttons">
|
||||
<ButtonStyled :color="color">
|
||||
<button :disabled="disabled" @click="handlePrimaryAction">
|
||||
<component :is="primaryAction.icon" v-if="primaryAction.icon" aria-hidden="true" />
|
||||
{{ primaryAction.label }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="dropdownActions.length > 0" :color="color">
|
||||
<OverflowMenu class="btn-dropdown-animation" :options="dropdownOptions" :disabled="disabled">
|
||||
<DropdownIcon />
|
||||
<template v-for="action in dropdownActions" :key="action.id" #[action.id]>
|
||||
<component :is="action.icon" v-if="action.icon" aria-hidden="true" />
|
||||
{{ action.label }}
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DropdownIcon } from '@modrinth/assets'
|
||||
import type { Component } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { ButtonStyled, OverflowMenu } from '../index'
|
||||
|
||||
// TODO: This should be moved to a shared types file.
|
||||
type Colors = 'standard' | 'brand' | 'red' | 'orange' | 'green' | 'blue' | 'purple'
|
||||
|
||||
export interface JoinedButtonAction {
|
||||
id: string
|
||||
label: string
|
||||
icon?: Component
|
||||
action: () => void
|
||||
color?: Colors
|
||||
hoverFilled?: boolean
|
||||
}
|
||||
|
||||
interface Props {
|
||||
actions: JoinedButtonAction[]
|
||||
color?: Colors
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
color: 'standard',
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
const primaryAction = computed(() => props.actions[0])
|
||||
|
||||
const dropdownActions = computed(() => props.actions.slice(1))
|
||||
|
||||
const colorMap: Record<
|
||||
Colors,
|
||||
| 'red'
|
||||
| 'orange'
|
||||
| 'green'
|
||||
| 'blue'
|
||||
| 'purple'
|
||||
| 'highlight'
|
||||
| 'primary'
|
||||
| 'danger'
|
||||
| 'secondary'
|
||||
| undefined
|
||||
> = {
|
||||
standard: 'secondary',
|
||||
brand: 'primary',
|
||||
red: 'red',
|
||||
orange: 'orange',
|
||||
green: 'green',
|
||||
blue: 'blue',
|
||||
purple: 'purple',
|
||||
}
|
||||
|
||||
const dropdownOptions = computed(() =>
|
||||
dropdownActions.value.map((action) => ({
|
||||
id: action.id,
|
||||
color: action.color ? colorMap[action.color] : undefined,
|
||||
action: action.action,
|
||||
hoverFilled: action.hoverFilled ?? true,
|
||||
})),
|
||||
)
|
||||
|
||||
function handlePrimaryAction() {
|
||||
if (primaryAction.value && !props.disabled) {
|
||||
primaryAction.value.action()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.joined-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.joined-buttons > :deep(.btn) {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.joined-buttons > :deep(.btn:first-child) {
|
||||
border-top-left-radius: var(--radius-md);
|
||||
border-bottom-left-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.joined-buttons > :deep(.btn:last-child) {
|
||||
border-top-right-radius: var(--radius-md);
|
||||
border-bottom-right-radius: var(--radius-md);
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
.joined-buttons > :deep(.btn:not(:last-child)) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.btn-dropdown-animation {
|
||||
padding: 0.5rem !important;
|
||||
}
|
||||
</style>
|
||||
58
packages/ui/src/components/base/ProgressSpinner.vue
Normal file
58
packages/ui/src/components/base/ProgressSpinner.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
progress: number
|
||||
max?: number
|
||||
}>(),
|
||||
{
|
||||
max: 1,
|
||||
},
|
||||
)
|
||||
|
||||
const percent = computed(() => props.progress / props.max)
|
||||
</script>
|
||||
<template>
|
||||
<span class="relative flex items-center justify-center">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="absolute"
|
||||
>
|
||||
<circle opacity="0.25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||
</svg>
|
||||
<svg
|
||||
:style="{ '--_progress': `${percent * 100}%` }"
|
||||
width="24"
|
||||
height="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="absolute progress-circle"
|
||||
>
|
||||
<circle opacity="0.75" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||
</svg>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@property --_progress {
|
||||
syntax: '<percentage>';
|
||||
inherits: false;
|
||||
initial-value: 0%;
|
||||
}
|
||||
|
||||
.progress-circle {
|
||||
transition: --_progress 0.125s ease-in-out;
|
||||
mask-image: conic-gradient(
|
||||
black 0%,
|
||||
black var(--_progress),
|
||||
transparent calc(var(--_progress) + 1%),
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
</style>
|
||||
@@ -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'
|
||||
@@ -23,6 +24,8 @@ export type { FilterBarOption } from './base/FilterBar.vue'
|
||||
export { default as FilterBar } from './base/FilterBar.vue'
|
||||
export { default as HeadingLink } from './base/HeadingLink.vue'
|
||||
export { default as IconSelect } from './base/IconSelect.vue'
|
||||
export type { JoinedButtonAction } from './base/JoinedButtons.vue'
|
||||
export { default as JoinedButtons } from './base/JoinedButtons.vue'
|
||||
export { default as LoadingIndicator } from './base/LoadingIndicator.vue'
|
||||
export { default as ManySelect } from './base/ManySelect.vue'
|
||||
export { default as MarkdownEditor } from './base/MarkdownEditor.vue'
|
||||
@@ -34,6 +37,7 @@ export { default as Pagination } from './base/Pagination.vue'
|
||||
export { default as PopoutMenu } from './base/PopoutMenu.vue'
|
||||
export { default as PreviewSelectButton } from './base/PreviewSelectButton.vue'
|
||||
export { default as ProgressBar } from './base/ProgressBar.vue'
|
||||
export { default as ProgressSpinner } from './base/ProgressSpinner.vue'
|
||||
export { default as ProjectCard } from './base/ProjectCard.vue'
|
||||
export { default as RadialHeader } from './base/RadialHeader.vue'
|
||||
export { default as RadioButtons } from './base/RadioButtons.vue'
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<div class="modal-container experimental-styles-within" :class="{ shown: visible }">
|
||||
<div class="modal-body flex flex-col bg-bg-raised rounded-2xl">
|
||||
<div
|
||||
v-if="!hideHeader"
|
||||
data-tauri-drag-region
|
||||
class="grid grid-cols-[auto_min-content] items-center gap-12 p-6 border-solid border-0 border-b-[1px] border-divider max-w-full"
|
||||
>
|
||||
@@ -61,6 +62,7 @@ const props = withDefaults(
|
||||
closeOnClickOutside?: boolean
|
||||
warnOnClose?: boolean
|
||||
header?: string
|
||||
hideHeader?: boolean
|
||||
onHide?: () => void
|
||||
onShow?: () => void
|
||||
}>(),
|
||||
@@ -72,6 +74,7 @@ const props = withDefaults(
|
||||
closeOnEsc: true,
|
||||
warnOnClose: false,
|
||||
header: undefined,
|
||||
hideHeader: false,
|
||||
onHide: () => {},
|
||||
onShow: () => {},
|
||||
},
|
||||
@@ -135,7 +138,7 @@ function updateMousePosition(event: { clientX: number; clientY: number }) {
|
||||
}
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (props.closeOnEsc && event.key === 'Escape') {
|
||||
if (props.closeOnEsc && event.key === 'Escape' && props.closable) {
|
||||
hide()
|
||||
mouseX.value = window.innerWidth / 2
|
||||
mouseY.value = window.innerHeight / 2
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
"button.cancel": {
|
||||
"defaultMessage": "Cancel"
|
||||
},
|
||||
"button.close": {
|
||||
"defaultMessage": "Close"
|
||||
},
|
||||
"button.continue": {
|
||||
"defaultMessage": "Continue"
|
||||
},
|
||||
|
||||
@@ -25,6 +25,10 @@ export const commonMessages = defineMessages({
|
||||
id: 'button.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
closeButton: {
|
||||
id: 'button.close',
|
||||
defaultMessage: 'Close',
|
||||
},
|
||||
changesSavedLabel: {
|
||||
id: 'label.changes-saved',
|
||||
defaultMessage: 'Changes saved',
|
||||
|
||||
Reference in New Issue
Block a user