feat: make byte size units translatable (#5969)

Make byte size units translatable
This commit is contained in:
Jerozgen
2026-05-09 12:26:59 +03:00
committed by GitHub
parent e8665f43ca
commit 3052a14d95
21 changed files with 214 additions and 222 deletions

View File

@@ -51,9 +51,10 @@ import {
providePageContext, providePageContext,
providePopupNotificationManager, providePopupNotificationManager,
useDebugLogger, useDebugLogger,
useFormatBytes,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import { formatBytes, renderString } from '@modrinth/utils' import { renderString } from '@modrinth/utils'
import { useQuery, useQueryClient } from '@tanstack/vue-query' import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { getVersion } from '@tauri-apps/api/app' import { getVersion } from '@tauri-apps/api/app'
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/core'
@@ -262,6 +263,8 @@ onUnmounted(async () => {
}) })
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatBytes = useFormatBytes()
const messages = defineMessages({ const messages = defineMessages({
updateInstalledToastTitle: { updateInstalledToastTitle: {
id: 'app.update.complete-toast.title', id: 'app.update.complete-toast.title',

View File

@@ -176,9 +176,10 @@ import {
ButtonStyled, ButtonStyled,
Card, Card,
CopyCode, CopyCode,
useFormatBytes,
useFormatDateTime, useFormatDateTime,
} from '@modrinth/ui' } from '@modrinth/ui'
import { formatBytes, renderString } from '@modrinth/utils' import { renderString } from '@modrinth/utils'
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
@@ -191,6 +192,7 @@ const formatDateTime = useFormatDateTime({
timeStyle: 'short', timeStyle: 'short',
dateStyle: 'long', dateStyle: 'long',
}) })
const formatBytes = useFormatBytes()
const breadcrumbs = useBreadcrumbs() const breadcrumbs = useBreadcrumbs()

View File

@@ -17,74 +17,60 @@
</label> </label>
</template> </template>
<script> <script setup lang="ts">
import { fileIsValid } from '~/helpers/fileUtils.js' import { useFormatBytes } from '@modrinth/ui'
import { fileIsValid } from '@modrinth/utils'
import { ref } from 'vue'
export default { const props = withDefaults(
components: {}, defineProps<{
props: { prompt?: string
prompt: { multiple?: boolean
type: String, accept?: string
default: 'Select file',
},
multiple: {
type: Boolean,
default: false,
},
accept: {
type: String,
default: null,
},
/** /**
* The max file size in bytes * The max file size in bytes
*/ */
maxSize: { maxSize?: number | null
type: Number, showIcon?: boolean
default: null, shouldAlwaysReset?: boolean
}, longStyle?: boolean
showIcon: { disabled?: boolean
type: Boolean, }>(),
default: true, {
}, prompt: 'Select file',
shouldAlwaysReset: { multiple: false,
type: Boolean, showIcon: true,
default: false, shouldAlwaysReset: false,
}, longStyle: false,
longStyle: { disabled: false,
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
}, },
emits: ['change'], )
data() {
return {
files: [],
}
},
methods: {
addFiles(files, shouldNotReset) {
if (!shouldNotReset || this.shouldAlwaysReset) {
this.files = files
}
const validationOptions = { maxSize: this.maxSize, alertOnInvalid: true } const emit = defineEmits<{ change: [files: File[]] }>()
this.files = [...this.files].filter((file) => fileIsValid(file, validationOptions))
if (this.files.length > 0) { const formatBytes = useFormatBytes()
this.$emit('change', this.files)
} const files = ref<File[]>([])
},
handleDrop(e) { function addFiles(incoming: FileList, shouldNotReset = false) {
this.addFiles(e.dataTransfer.files) if (!shouldNotReset || props.shouldAlwaysReset) {
}, files.value = Array.from(incoming)
handleChange(e) { }
this.addFiles(e.target.files) const validationOptions = { maxSize: props.maxSize, alertOnInvalid: true }
}, files.value = files.value.filter((file) => fileIsValid(file, validationOptions, formatBytes))
}, if (files.value.length > 0) {
emit('change', files.value)
}
}
function handleDrop(e: DragEvent) {
addFiles(e.dataTransfer!.files)
}
function handleChange(e: Event) {
const input = e.target as HTMLInputElement
if (!input.files) return
addFiles(input.files)
} }
</script> </script>

View File

@@ -28,6 +28,7 @@ import {
injectNotificationManager, injectNotificationManager,
OverflowMenu, OverflowMenu,
type OverflowMenuOption, type OverflowMenuOption,
useFormatBytes,
useFormatDateTime, useFormatDateTime,
} from '@modrinth/ui' } from '@modrinth/ui'
import { NavTabs } from '@modrinth/ui' import { NavTabs } from '@modrinth/ui'
@@ -56,6 +57,7 @@ const formatDateTimeUtc = useFormatDateTime({
timeZoneName: 'short', timeZoneName: 'short',
timeZone: 'UTC', timeZone: 'UTC',
}) })
const formatBytes = useFormatBytes()
type FlattenedFileReport = Labrinth.TechReview.Internal.FileReport & { type FlattenedFileReport = Labrinth.TechReview.Internal.FileReport & {
id: string id: string
@@ -362,12 +364,6 @@ const formattedDate = computed(() => {
return `${diffDays} days ago` return `${diffDays} days ago`
}) })
function formatFileSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KiB`
return `${(bytes / (1024 * 1024)).toFixed(2)} MiB`
}
function viewFileFlags(file: FlattenedFileReport) { function viewFileFlags(file: FlattenedFileReport) {
selectedFileId.value = file.id selectedFileId.value = file.id
currentTab.value = 'File' currentTab.value = 'File'
@@ -851,7 +847,7 @@ const reviewSummaryPreview = computed(() => {
const fileVerdict = fileUnsafe > 0 ? 'Unsafe' : 'Safe' const fileVerdict = fileUnsafe > 0 ? 'Unsafe' : 'Safe'
markdown += `### ${fileData.fileName}\n` markdown += `### ${fileData.fileName}\n`
markdown += `> ${formatFileSize(fileData.fileSize)}${fileData.decisions.length} issues • Max severity: ${fileData.maxSeverity} • **Verdict:** ${fileVerdict}\n\n` markdown += `> ${formatBytes(fileData.fileSize)}${fileData.decisions.length} issues • Max severity: ${fileData.maxSeverity} • **Verdict:** ${fileVerdict}\n\n`
markdown += `<details>\n<summary>Issues (${fileSafe} safe, ${fileUnsafe} unsafe)</summary>\n\n` markdown += `<details>\n<summary>Issues (${fileSafe} safe, ${fileUnsafe} unsafe)</summary>\n\n`
markdown += `| Class | Issue Type | Severity | Decision |\n` markdown += `| Class | Issue Type | Severity | Decision |\n`
markdown += `|-------|------------|----------|----------|\n` markdown += `|-------|------------|----------|----------|\n`
@@ -1150,7 +1146,7 @@ async function handleSubmitReview(verdict: 'safe' | 'unsafe') {
</span> </span>
<div class="rounded-full border border-solid border-surface-5 bg-surface-3 px-2.5 py-1"> <div class="rounded-full border border-solid border-surface-5 bg-surface-3 px-2.5 py-1">
<span class="text-sm font-medium text-secondary">{{ <span class="text-sm font-medium text-secondary">{{
formatFileSize(file.file_size) formatBytes(file.file_size)
}}</span> }}</span>
</div> </div>
<div <div

View File

@@ -1,32 +0,0 @@
import { formatBytes } from '@modrinth/utils'
export const fileIsValid = (file, validationOptions) => {
const { maxSize, alertOnInvalid } = validationOptions
if (maxSize !== null && maxSize !== undefined && file.size > maxSize) {
if (alertOnInvalid) {
alert(`File ${file.name} is too big! Must be less than ${formatBytes(maxSize)}`)
}
return false
}
return true
}
export const acceptFileFromProjectType = (projectType) => {
switch (projectType) {
case 'mod':
return '.jar,.zip,.litemod,application/java-archive,application/x-java-archive,application/zip'
case 'plugin':
return '.jar,.zip,application/java-archive,application/x-java-archive,application/zip'
case 'resourcepack':
return '.zip,application/zip'
case 'shader':
return '.zip,application/zip'
case 'datapack':
return '.zip,application/zip'
case 'modpack':
return '.mrpack,application/x-modrinth-modpack+zip,application/zip'
default:
return '*'
}
}

View File

@@ -146,7 +146,11 @@
const input = e.target const input = e.target
if (input.files?.length) { if (input.files?.length) {
if ( if (
fileIsValid(input.files[0], { maxSize: 524288000, alertOnInvalid: true }) fileIsValid(
input.files[0],
{ maxSize: 524288000, alertOnInvalid: true },
formatBytes,
)
) )
showBannerPreview(Array.from(input.files)) showBannerPreview(Array.from(input.files))
} }
@@ -379,6 +383,7 @@ import {
StyledInput, StyledInput,
Toggle, Toggle,
UnsavedChangesPopup, UnsavedChangesPopup,
useFormatBytes,
usePageLeaveSafety, usePageLeaveSafety,
} from '@modrinth/ui' } from '@modrinth/ui'
import { fileIsValid, formatProjectStatus, formatProjectType } from '@modrinth/utils' import { fileIsValid, formatProjectStatus, formatProjectType } from '@modrinth/utils'
@@ -405,6 +410,8 @@ const flags = useFeatureFlags()
const tags = useGeneratedState() const tags = useGeneratedState()
const router = useNativeRouter() const router = useNativeRouter()
const formatBytes = useFormatBytes()
const name = ref(project.value.title) const name = ref(project.value.title)
const slug = ref(project.value.slug) const slug = ref(project.value.slug)
const summary = ref(project.value.description) const summary = ref(project.value.description)

View File

@@ -442,9 +442,10 @@ import {
MultiSelect, MultiSelect,
PROJECT_DEP_MARKER_QUERY, PROJECT_DEP_MARKER_QUERY,
StyledInput, StyledInput,
useFormatBytes,
useFormatDateTime, useFormatDateTime,
} from '@modrinth/ui' } from '@modrinth/ui'
import { formatBytes, renderHighlightedString } from '@modrinth/utils' import { renderHighlightedString } from '@modrinth/utils'
import Breadcrumbs from '~/components/ui/Breadcrumbs.vue' import Breadcrumbs from '~/components/ui/Breadcrumbs.vue'
import CreateProjectVersionModal from '~/components/ui/create-project-version/CreateProjectVersionModal.vue' import CreateProjectVersionModal from '~/components/ui/create-project-version/CreateProjectVersionModal.vue'
@@ -473,6 +474,7 @@ const formatDateTime = useFormatDateTime({
dateStyle: 'long', dateStyle: 'long',
}) })
const formatDate = useFormatDateTime({ dateStyle: 'medium' }) const formatDate = useFormatDateTime({ dateStyle: 'medium' })
const formatBytes = useFormatBytes()
// Helper for accessing nuxt app $formatVersion // Helper for accessing nuxt app $formatVersion
const formatVersionDisplay = (versions: string[]) => (data as any).$formatVersion(versions) const formatVersionDisplay = (versions: string[]) => (data as any).$formatVersion(versions)

View File

@@ -81,11 +81,19 @@
<script setup lang="ts"> <script setup lang="ts">
import { FileIcon, SpinnerIcon, UploadIcon } from '@modrinth/assets' import { FileIcon, SpinnerIcon, UploadIcon } from '@modrinth/assets'
import { Admonition, Avatar, CopyCode, injectNotificationManager } from '@modrinth/ui' import {
import { formatBytes, type Project, type Version } from '@modrinth/utils' Admonition,
Avatar,
CopyCode,
injectNotificationManager,
useFormatBytes,
} from '@modrinth/ui'
import type { Project, Version } from '@modrinth/utils'
const { addNotification } = injectNotificationManager() const { addNotification } = injectNotificationManager()
const formatBytes = useFormatBytes()
const fileInput = ref<HTMLInputElement>() const fileInput = ref<HTMLInputElement>()
const selectedFile = ref<File | null>(null) const selectedFile = ref<File | null>(null)
const fileHashes = ref<{ const fileHashes = ref<{

View File

@@ -30,9 +30,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { formatBytes } from '@modrinth/utils'
import { computed, onUnmounted, ref, watch } from 'vue' import { computed, onUnmounted, ref, watch } from 'vue'
import { useFormatBytes } from '#ui/composables'
interface Props { interface Props {
maxValue: number maxValue: number
currentValue: number currentValue: number
@@ -59,6 +60,8 @@ const props = withDefaults(defineProps<Props>(), {
], ],
}) })
const formatBytes = useFormatBytes()
const currentPhrase = ref('') const currentPhrase = ref('')
const usedPhrases = ref(new Set<number>()) const usedPhrases = ref(new Set<number>())
let phraseInterval: NodeJS.Timeout | null = null let phraseInterval: NodeJS.Timeout | null = null

View File

@@ -50,6 +50,7 @@ import { FolderUpIcon } from '@modrinth/assets'
import { fileIsValid } from '@modrinth/utils' import { fileIsValid } from '@modrinth/utils'
import { ref } from 'vue' import { ref } from 'vue'
import { useFormatBytes } from '../../composables'
import { injectNotificationManager } from '../../providers' import { injectNotificationManager } from '../../providers'
const { addNotification } = injectNotificationManager() const { addNotification } = injectNotificationManager()
@@ -78,6 +79,8 @@ const props = withDefaults(
}, },
) )
const formatBytes = useFormatBytes()
const files = ref<File[]>([]) const files = ref<File[]>([])
function matchesAccept(file: File, accept?: string): boolean { function matchesAccept(file: File, accept?: string): boolean {
@@ -129,7 +132,7 @@ function addFiles(incoming: FileList, shouldNotReset = false) {
alertOnInvalid: true, alertOnInvalid: true,
} }
files.value = files.value.filter((file) => fileIsValid(file, validationOptions)) files.value = files.value.filter((file) => fileIsValid(file, validationOptions, formatBytes))
if (files.value.length > 0) { if (files.value.length > 0) {
emit('change', files.value) emit('change', files.value)

View File

@@ -12,73 +12,62 @@
</label> </label>
</template> </template>
<script> <script setup lang="ts">
import { fileIsValid } from '@modrinth/utils' import { fileIsValid } from '@modrinth/utils'
import { defineComponent } from 'vue' import { ref } from 'vue'
export default defineComponent({ import { useFormatBytes } from '../../composables'
props: {
prompt: { const props = withDefaults(
type: String, defineProps<{
default: 'Select file', prompt?: string
}, multiple?: boolean
multiple: { accept?: string
type: Boolean,
default: false,
},
accept: {
type: String,
default: null,
},
/** /**
* The max file size in bytes * The max file size in bytes
*/ */
maxSize: { maxSize?: number | null
type: Number, showIcon?: boolean
default: null, shouldAlwaysReset?: boolean
}, longStyle?: boolean
showIcon: { disabled?: boolean
type: Boolean, }>(),
default: true, {
}, prompt: 'Select file',
shouldAlwaysReset: { multiple: false,
type: Boolean, showIcon: true,
default: false, shouldAlwaysReset: false,
}, longStyle: false,
longStyle: { disabled: false,
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
}, },
emits: ['change'], )
data() {
return { const emit = defineEmits<{ change: [files: File[]] }>()
files: [],
} const formatBytes = useFormatBytes()
},
methods: { const files = ref<File[]>([])
addFiles(files, shouldNotReset) {
if (!shouldNotReset || this.shouldAlwaysReset) { function addFiles(incoming: FileList, shouldNotReset = false) {
this.files = files if (!shouldNotReset || props.shouldAlwaysReset) {
} files.value = Array.from(incoming)
const validationOptions = { maxSize: this.maxSize, alertOnInvalid: true } }
this.files = [...this.files].filter((file) => fileIsValid(file, validationOptions)) const validationOptions = { maxSize: props.maxSize, alertOnInvalid: true }
if (this.files.length > 0) { files.value = files.value.filter((file) => fileIsValid(file, validationOptions, formatBytes))
this.$emit('change', this.files) if (files.value.length > 0) {
} emit('change', files.value)
}, }
handleDrop(e) { }
this.addFiles(e.dataTransfer.files)
}, function handleDrop(e: DragEvent) {
handleChange(e) { addFiles(e.dataTransfer!.files)
this.addFiles(e.target.files) }
},
}, function handleChange(e: Event) {
}) const input = e.target as HTMLInputElement
if (!input.files) return
addFiles(input.files)
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -268,16 +268,12 @@ import {
Pagination, Pagination,
TagItem, TagItem,
useCompactNumber, useCompactNumber,
useFormatBytes,
useFormatDateTime, useFormatDateTime,
VersionChannelIndicator, VersionChannelIndicator,
VersionFilterControl, VersionFilterControl,
} from '@modrinth/ui' } from '@modrinth/ui'
import { import { formatVersionsForDisplay, type GameVersionTag, type Version } from '@modrinth/utils'
formatBytes,
formatVersionsForDisplay,
type GameVersionTag,
type Version,
} from '@modrinth/utils'
import { Menu } from 'floating-vue' import { Menu } from 'floating-vue'
import { computed, type Ref, ref } from 'vue' import { computed, type Ref, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
@@ -293,6 +289,7 @@ const formatDateTime = useFormatDateTime({
timeStyle: 'short', timeStyle: 'short',
dateStyle: 'long', dateStyle: 'long',
}) })
const formatBytes = useFormatBytes()
type VersionWithDisplayUrlEnding = Version & { type VersionWithDisplayUrlEnding = Version & {
displayUrlEnding: string displayUrlEnding: string

View File

@@ -15,7 +15,7 @@
<span> <span>
{{ {{
formatMessage(messages.extracted, { formatMessage(messages.extracted, {
size: 'bytes_processed' in op ? formatBytes(op.bytes_processed ?? 0) : '0 B', size: formatBytes(op.bytes_processed ?? 0),
}) })
}} }}
</span> </span>
@@ -35,11 +35,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { PackageOpenIcon } from '@modrinth/assets' import { PackageOpenIcon } from '@modrinth/assets'
import { formatBytes } from '@modrinth/utils'
import { computed } from 'vue' import { computed } from 'vue'
import Admonition from '#ui/components/base/Admonition.vue' import Admonition from '#ui/components/base/Admonition.vue'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue' import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import { useFormatBytes } from '#ui/composables'
import { defineMessages, useVIntl } from '#ui/composables/i18n' import { defineMessages, useVIntl } from '#ui/composables/i18n'
import type { FileOperation } from '#ui/layouts/shared/files-tab/types' import type { FileOperation } from '#ui/layouts/shared/files-tab/types'
import { injectModrinthServerContext } from '#ui/providers' import { injectModrinthServerContext } from '#ui/providers'
@@ -53,6 +53,7 @@ const props = defineProps<{
}>() }>()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatBytes = useFormatBytes()
const ctx = injectModrinthServerContext() const ctx = injectModrinthServerContext()
const messages = defineMessages({ const messages = defineMessages({

View File

@@ -25,13 +25,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { UploadIcon } from '@modrinth/assets' import { UploadIcon } from '@modrinth/assets'
import { formatBytes } from '@modrinth/utils'
import { computed } from 'vue' import { computed } from 'vue'
import Admonition from '#ui/components/base/Admonition.vue' import Admonition from '#ui/components/base/Admonition.vue'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue' import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import { useFormatBytes } from '#ui/composables'
import { injectModrinthServerContext } from '#ui/providers' import { injectModrinthServerContext } from '#ui/providers'
const formatBytes = useFormatBytes()
const ctx = injectModrinthServerContext() const ctx = injectModrinthServerContext()
const state = computed(() => ctx.uploadState.value) const state = computed(() => ctx.uploadState.value)

View File

@@ -0,0 +1,39 @@
import { defineMessage, useVIntl } from '#ui/composables/i18n.ts'
const messages = [
defineMessage({
id: 'format.bytes.0',
defaultMessage: '{count, plural, one {# byte} other {# bytes}}',
}),
defineMessage({
id: 'format.bytes.1',
defaultMessage: '{count, number} KiB',
}),
defineMessage({
id: 'format.bytes.2',
defaultMessage: '{count, number} MiB',
}),
defineMessage({
id: 'format.bytes.3',
defaultMessage: '{count, number} GiB',
}),
defineMessage({
id: 'format.bytes.4',
defaultMessage: '{count, number} TiB',
}),
]
export function useFormatBytes() {
const { formatMessage } = useVIntl()
function format(bytes: number, decimals = 2): string {
if (bytes === 0) return formatMessage(messages[0], { count: 0 })
const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), messages.length - 1)
return formatMessage(messages[exponent], {
count: (bytes / Math.pow(1024, exponent)).toFixed(decimals),
})
}
return format
}

View File

@@ -1,5 +1,6 @@
export * from './debug-logger' export * from './debug-logger'
export * from './dynamic-font-size' export * from './dynamic-font-size'
export * from './format-bytes'
export * from './format-date-time' export * from './format-date-time'
export * from './format-money' export * from './format-money'
export * from './format-number' export * from './format-number'

View File

@@ -104,6 +104,7 @@ import { computed, ref } from 'vue'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue' import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import Checkbox from '#ui/components/base/Checkbox.vue' import Checkbox from '#ui/components/base/Checkbox.vue'
import TeleportOverflowMenu from '#ui/components/base/TeleportOverflowMenu.vue' import TeleportOverflowMenu from '#ui/components/base/TeleportOverflowMenu.vue'
import { useFormatBytes } from '#ui/composables'
import { useFormatDateTime } from '#ui/composables/format-date-time' import { useFormatDateTime } from '#ui/composables/format-date-time'
import { defineMessages, useVIntl } from '#ui/composables/i18n' import { defineMessages, useVIntl } from '#ui/composables/i18n'
import { injectNotificationManager } from '#ui/providers/web-notifications' import { injectNotificationManager } from '#ui/providers/web-notifications'
@@ -164,8 +165,6 @@ const isDropTarget = computed(
) )
const isDragSource = computed(() => fileDragActive.value && fileDragData.value?.path === props.path) const isDragSource = computed(() => fileDragActive.value && fileDragData.value?.path === props.path)
const units = Object.freeze(['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'])
const formatDateTime = useFormatDateTime({ const formatDateTime = useFormatDateTime({
year: '2-digit', year: '2-digit',
month: '2-digit', month: '2-digit',
@@ -173,6 +172,7 @@ const formatDateTime = useFormatDateTime({
hour: 'numeric', hour: 'numeric',
minute: 'numeric', minute: 'numeric',
}) })
const formatBytes = useFormatBytes()
const containerClasses = computed(() => { const containerClasses = computed(() => {
const dropTarget = isDropTarget.value const dropTarget = isDropTarget.value
@@ -307,12 +307,7 @@ const formattedSize = computed(() => {
} }
if (props.size === undefined) return '' if (props.size === undefined) return ''
const bytes = props.size return formatBytes(props.size)
if (bytes === 0) return '0 B'
const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
const size = (bytes / Math.pow(1024, exponent)).toFixed(2)
return `${size} ${units[exponent]}`
}) })
function openContextMenu(event: MouseEvent) { function openContextMenu(event: MouseEvent) {

View File

@@ -115,12 +115,14 @@ import { CheckCircleIcon, FolderOpenIcon, SpinnerIcon, XCircleIcon } from '@modr
import { computed, nextTick, ref, watch } from 'vue' import { computed, nextTick, ref, watch } from 'vue'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue' import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import { useFormatBytes } from '#ui/composables'
import { defineMessages, useVIntl } from '#ui/composables/i18n' import { defineMessages, useVIntl } from '#ui/composables/i18n'
import { injectModrinthClient } from '#ui/providers/api-client' import { injectModrinthClient } from '#ui/providers/api-client'
import { injectNotificationManager } from '#ui/providers/web-notifications' import { injectNotificationManager } from '#ui/providers/web-notifications'
import { commonMessages } from '#ui/utils/common-messages' import { commonMessages } from '#ui/utils/common-messages'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatBytes = useFormatBytes()
const { addNotification } = injectNotificationManager() const { addNotification } = injectNotificationManager()
const client = injectModrinthClient() const client = injectModrinthClient()
@@ -240,13 +242,6 @@ watch(
{ deep: true }, { deep: true },
) )
const formatFileSize = (bytes: number): string => {
if (bytes < 1024) return bytes + ' B'
if (bytes < 1024 ** 2) return (bytes / 1024).toFixed(1) + ' KB'
if (bytes < 1024 ** 3) return (bytes / 1024 ** 2).toFixed(1) + ' MB'
return (bytes / 1024 ** 3).toFixed(1) + ' GB'
}
const cancelUpload = (item: UploadItem) => { const cancelUpload = (item: UploadItem) => {
if (item.uploader && item.status === 'uploading') { if (item.uploader && item.status === 'uploading') {
item.uploader.cancel() item.uploader.cancel()
@@ -269,7 +264,7 @@ const uploadFile = async (file: File) => {
file, file,
progress: 0, progress: 0,
status: 'pending', status: 'pending',
size: formatFileSize(file.size), size: formatBytes(file.size, 1),
} }
uploadQueue.value.push(uploadItem) uploadQueue.value.push(uploadItem)

View File

@@ -65,6 +65,7 @@ import { useStorage } from '@vueuse/core'
import { computed, defineAsyncComponent, onMounted, ref, shallowRef, watch } from 'vue' import { computed, defineAsyncComponent, onMounted, ref, shallowRef, watch } from 'vue'
import { RouterLink } from 'vue-router' import { RouterLink } from 'vue-router'
import { useFormatBytes } from '#ui/composables'
import { injectModrinthServerContext, injectPageContext } from '#ui/providers' import { injectModrinthServerContext, injectPageContext } from '#ui/providers'
const VueApexCharts = defineAsyncComponent(() => import('vue3-apexcharts')) const VueApexCharts = defineAsyncComponent(() => import('vue3-apexcharts'))
@@ -92,6 +93,8 @@ const props = withDefaults(
}, },
) )
const formatBytes = useFormatBytes()
const chartsReady = ref(new Set<number>()) const chartsReady = ref(new Set<number>())
const userPreferences = useStorage(`pyro-server-${serverId || 'unknown'}-preferences`, { const userPreferences = useStorage(`pyro-server-${serverId || 'unknown'}-preferences`, {
ramAsNumber: false, ramAsNumber: false,
@@ -178,21 +181,10 @@ const ramChartOptions = computed(() => buildChartOptions(ramWarning.value, 1, ra
const cpuSeries = computed(() => [{ name: 'CPU', data: cpuData.value }]) const cpuSeries = computed(() => [{ name: 'CPU', data: cpuData.value }])
const ramSeries = computed(() => [{ name: 'Memory', data: ramData.value }]) const ramSeries = computed(() => [{ name: 'Memory', data: ramData.value }])
const formatBytes = (bytes: number) => {
const units = ['B', 'KB', 'MB', 'GB']
let value = bytes
let unit = 0
while (value >= 1024 && unit < units.length - 1) {
value /= 1024
unit++
}
return `${Math.round(value * 10) / 10} ${units[unit]}`
}
const metrics = computed(() => { const metrics = computed(() => {
const storageMetric = { const storageMetric = {
title: 'Storage', title: 'Storage',
value: props.loading ? '0 B' : formatBytes(stats.value.storage_usage_bytes ?? 0), value: formatBytes(props.loading ? 0 : (stats.value.storage_usage_bytes ?? 0), 1),
secondary: null as string | null, secondary: null as string | null,
icon: FolderOpenIcon, icon: FolderOpenIcon,
showGraph: false, showGraph: false,
@@ -241,10 +233,10 @@ const metrics = computed(() => {
{ {
title: 'Memory', title: 'Memory',
value: showRamAsBytes.value value: showRamAsBytes.value
? formatBytes(stats.value.ram_usage_bytes ?? 0) ? formatBytes(stats.value.ram_usage_bytes ?? 0, 1)
: `${ramPercent.value.toFixed(2)}%`, : `${ramPercent.value.toFixed(2)}%`,
secondary: showRamAsBytes.value secondary: showRamAsBytes.value
? `/ ${formatBytes(stats.value.ram_total_bytes ?? 0)}` ? `/ ${formatBytes(stats.value.ram_total_bytes ?? 0, 1)}`
: (null as string | null), : (null as string | null),
icon: DatabaseIcon, icon: DatabaseIcon,
showGraph: true, showGraph: true,

View File

@@ -1217,6 +1217,21 @@
"form.placeholder.state": { "form.placeholder.state": {
"defaultMessage": "Enter state/province" "defaultMessage": "Enter state/province"
}, },
"format.bytes.0": {
"defaultMessage": "{count, plural, one {# byte} other {# bytes}}"
},
"format.bytes.1": {
"defaultMessage": "{count, number} KiB"
},
"format.bytes.2": {
"defaultMessage": "{count, number} MiB"
},
"format.bytes.3": {
"defaultMessage": "{count, number} GiB"
},
"format.bytes.4": {
"defaultMessage": "{count, number} TiB"
},
"header.category.category": { "header.category.category": {
"defaultMessage": "Category" "defaultMessage": "Category"
}, },

View File

@@ -94,18 +94,6 @@ export const sortedCategories = (tags, formatCategoryName, locale) => {
}) })
} }
export const formatBytes = (bytes, decimals = 2) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ['Bytes', 'KiB', 'MiB', 'GiB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
}
export const capitalizeString = (name) => { export const capitalizeString = (name) => {
return name ? name.charAt(0).toUpperCase() + name.slice(1) : name return name ? name.charAt(0).toUpperCase() + name.slice(1) : name
} }
@@ -240,7 +228,7 @@ export function cycleValue<T extends string>(value: T, values: T[]): T {
return values[index % values.length] return values[index % values.length]
} }
export const fileIsValid = (file, validationOptions) => { export const fileIsValid = (file, validationOptions, formatBytes) => {
const { maxSize, alertOnInvalid } = validationOptions const { maxSize, alertOnInvalid } = validationOptions
if (maxSize !== null && maxSize !== undefined && file.size > maxSize) { if (maxSize !== null && maxSize !== undefined && file.size > maxSize) {
if (alertOnInvalid) { if (alertOnInvalid) {