Compare commits

..

1 Commits

Author SHA1 Message Date
Josiah Glosson
52451f85b5 Attempt to fix cache issue 2025-03-12 11:10:33 -05:00
426 changed files with 5904 additions and 11604 deletions

View File

@@ -16,7 +16,7 @@ body:
id: version
attributes:
label: What version of the Modrinth App are you using?
description: Find this in ⚙️ Settings (bottom right) -> After Modrinth App (bottom left)
description: Find this in ⚙️ Settings (bottom right) -> About -> App version.
validations:
required: true
- type: dropdown

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -7,13 +7,11 @@ on:
paths:
- .github/workflows/daedalus-docker.yml
- 'apps/daedalus_client/**'
- 'packages/daedalus/**'
pull_request:
types: [opened, synchronize]
paths:
- .github/workflows/daedalus-docker.yml
- 'apps/daedalus_client/**'
- 'packages/daedalus/**'
merge_group:
types: [checks_requested]

1022
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,6 @@
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { RouterView, useRoute, useRouter } from 'vue-router'
import {
UserIcon,
ArrowBigUpDashIcon,
CompassIcon,
DownloadIcon,
@@ -234,9 +233,6 @@ async function fetchCredentials() {
credentials.value = creds
}
const profileMenu = ref()
const isProfileMenuOpen = computed(() => profileMenu.value?.isOpen)
async function signIn() {
await login().catch(handleError)
await fetchCredentials()
@@ -414,34 +410,26 @@ function handleAuxClick(e) {
<NavButton v-tooltip.right="'Settings'" :to="() => $refs.settingsModal.show()">
<SettingsIcon />
</NavButton>
<OverflowMenu
v-if="credentials"
ref="profileMenu"
placement="right-end"
class="w-12 h-12 border-none cursor-pointer rounded-full flex items-center justify-center text-2xl transition-all button-animation"
:class="isProfileMenuOpen ? 'bg-button-bg' : 'bg-transparent hover:bg-button-bg'"
:options="[
{
id: 'profile',
action: () => router.push(`/user/${credentials.user.id}`),
},
{
id: 'sign-out',
action: () => logOut(),
color: 'danger',
},
]"
direction="left"
>
<Avatar
:src="credentials.user.avatar_url"
:alt="credentials.user.username"
size="32px"
circle
/>
<template #profile> <UserIcon /> Profile </template>
<template #sign-out> <LogOutIcon /> Sign out </template>
</OverflowMenu>
<ButtonStyled v-if="credentials" type="transparent" circular>
<OverflowMenu
:options="[
{
id: 'sign-out',
action: () => logOut(),
color: 'danger',
},
]"
direction="left"
>
<Avatar
:src="credentials.user.avatar_url"
:alt="credentials.user.username"
size="32px"
circle
/>
<template #sign-out> <LogOutIcon /> Sign out </template>
</OverflowMenu>
</ButtonStyled>
<NavButton v-else v-tooltip.right="'Sign in'" :to="() => signIn()">
<LogInIcon />
<template #label>Sign in</template>
@@ -485,7 +473,7 @@ function handleAuxClick(e) {
<RunningAppBar />
</Suspense>
</div>
<section v-if="!nativeDecorations" class="window-controls" data-tauri-drag-region-exclude>
<section v-if="!nativeDecorations" class="window-controls">
<Button class="titlebar-button" icon-only @click="() => getCurrentWindow().minimize()">
<MinimizeIcon />
</Button>
@@ -533,16 +521,6 @@ function handleAuxClick(e) {
width: 'calc(100% - var(--right-bar-width))',
}"
></div>
<div
v-if="criticalErrorMessage"
class="m-6 mb-0 flex flex-col border-red bg-bg-red rounded-2xl border-2 border-solid p-4 gap-1 font-semibold text-contrast"
>
<h1 class="m-0 text-lg font-extrabold">{{ criticalErrorMessage.header }}</h1>
<div
class="markdown-body text-primary"
v-html="renderString(criticalErrorMessage.body ?? '')"
></div>
</div>
<RouterView v-slot="{ Component }">
<template v-if="Component">
<Suspense @pending="loading.startLoading()" @resolve="loading.stopLoading()">
@@ -614,6 +592,12 @@ function handleAuxClick(e) {
<PromotionWrapper />
</template>
</div>
<div class="view">
<div v-if="criticalErrorMessage" class="critical-error-banner" data-tauri-drag-region>
<h1>{{ criticalErrorMessage.header }}</h1>
<div class="markdown-body" v-html="renderString(criticalErrorMessage.body ?? '')"></div>
</div>
</div>
</div>
<URLConfirmModal ref="urlModal" />
<Notifications ref="notificationsWrapper" sidebar />
@@ -710,23 +694,12 @@ function handleAuxClick(e) {
.app-grid-navbar {
grid-area: nav;
// Fixes SVG scaling issues
filter: brightness(1.00001);
}
.app-grid-statusbar {
grid-area: status;
}
[data-tauri-drag-region] {
-webkit-app-region: drag;
}
[data-tauri-drag-region-exclude] {
-webkit-app-region: no-drag;
}
.app-contents {
position: absolute;
z-index: 1;
@@ -796,7 +769,6 @@ function handleAuxClick(e) {
height: 100%;
overflow: auto;
overflow-x: hidden;
scrollbar-gutter: stable;
}
.app-contents::before {

View File

@@ -181,26 +181,24 @@ const maxInstancesPerRow = ref(1)
const maxProjectsPerRow = ref(1)
const calculateCardsPerRow = () => {
if (rows.value && rows.value[0]) {
// Calculate how many cards fit in one row
const containerWidth = rows.value[0].clientWidth
// Convert container width from pixels to rem
const containerWidthInRem =
containerWidth / parseFloat(getComputedStyle(document.documentElement).fontSize)
// Calculate how many cards fit in one row
const containerWidth = rows.value[0].clientWidth
// Convert container width from pixels to rem
const containerWidthInRem =
containerWidth / parseFloat(getComputedStyle(document.documentElement).fontSize)
maxInstancesPerCompactRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75)
maxInstancesPerRow.value = Math.floor((containerWidthInRem + 0.75) / 20.75)
maxProjectsPerRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75)
maxInstancesPerCompactRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75)
maxInstancesPerRow.value = Math.floor((containerWidthInRem + 0.75) / 20.75)
maxProjectsPerRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75)
if (maxInstancesPerRow.value < 5) {
maxInstancesPerRow.value *= 2
}
if (maxInstancesPerCompactRow.value < 5) {
maxInstancesPerCompactRow.value *= 2
}
if (maxProjectsPerRow.value < 3) {
maxProjectsPerRow.value *= 2
}
if (maxInstancesPerRow.value < 5) {
maxInstancesPerRow.value *= 2
}
if (maxInstancesPerCompactRow.value < 5) {
maxInstancesPerCompactRow.value *= 2
}
if (maxProjectsPerRow.value < 3) {
maxProjectsPerRow.value *= 2
}
}
@@ -209,17 +207,13 @@ const resizeObserver = ref(null)
onMounted(() => {
calculateCardsPerRow()
resizeObserver.value = new ResizeObserver(calculateCardsPerRow)
if (rowContainer.value) {
resizeObserver.value.observe(rowContainer.value)
}
resizeObserver.value.observe(rowContainer.value)
window.addEventListener('resize', calculateCardsPerRow)
})
onUnmounted(() => {
window.removeEventListener('resize', calculateCardsPerRow)
if (rowContainer.value) {
resizeObserver.value.unobserve(rowContainer.value)
}
resizeObserver.value.unobserve(rowContainer.value)
})
</script>

View File

@@ -151,7 +151,7 @@ const exportPack = async () => {
</div>
</div>
<div v-if="showingFiles" class="table-content">
<div v-for="[path, children] in folders" :key="path.name" class="table-row">
<div v-for="[path, children] of folders" :key="path.name" class="table-row">
<div class="table-cell file-entry">
<div class="file-primary">
<Checkbox

View File

@@ -2,14 +2,14 @@
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import {
SpinnerIcon,
GameIcon,
TimerIcon,
StopCircleIcon,
PlayIcon,
DownloadIcon,
GameIcon,
PlayIcon,
SpinnerIcon,
StopCircleIcon,
TimerIcon,
} from '@modrinth/assets'
import { ButtonStyled, Avatar, SmartClickable } from '@modrinth/ui'
import { Avatar, ButtonStyled } from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core'
import { finish_install, kill, run } from '@/helpers/profile'
import { get_by_profile_path } from '@/helpers/process'
@@ -134,26 +134,22 @@ onUnmounted(() => unlisten())
</script>
<template>
<SmartClickable class="card-shadow bg-bg-raised rounded-xl" @mouseenter="checkProcess">
<template #clickable>
<router-link
class="no-click-animation"
:to="`/instance/${encodeURIComponent(instance.path)}/`"
/>
</template>
<div v-if="compact" class="grid grid-cols-[auto_1fr_auto] p-3 pl-4 gap-2">
<template v-if="compact">
<div
class="card-shadow grid grid-cols-[auto_1fr_auto] bg-bg-raised rounded-xl p-3 pl-4 gap-2 cursor-pointer hover:brightness-90 transition-all"
@click="seeInstance"
@mouseenter="checkProcess"
>
<Avatar
size="48px"
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
:tint-by="instance.path"
alt="Mod card"
/>
<div
class="h-full flex items-center font-bold text-contrast leading-normal smart-clickable:underline-on-hover"
>
<div class="h-full flex items-center font-bold text-contrast leading-normal">
<span class="line-clamp-2">{{ instance.name }}</span>
</div>
<div class="flex items-center smart-clickable:allow-pointer-events">
<div class="flex items-center">
<ButtonStyled v-if="playing" color="red" circular @mousehover="checkProcess">
<button v-tooltip="'Stop'" @click="(e) => stop(e, 'InstanceCard')">
<StopCircleIcon />
@@ -180,7 +176,13 @@ onUnmounted(() => unlisten())
<span class="text-sm"> Played {{ dayjs(instance.last_played).fromNow() }} </span>
</div>
</div>
<div v-else class="p-4 rounded-xl flex gap-3 group" @mouseenter="checkProcess">
</template>
<div v-else>
<div
class="button-base bg-bg-raised p-4 rounded-xl flex gap-3 group"
@click="seeInstance"
@mouseenter="checkProcess"
>
<div class="relative flex items-center justify-center">
<Avatar
size="48px"
@@ -229,9 +231,7 @@ onUnmounted(() => unlisten())
</div>
</div>
<div class="flex flex-col gap-1">
<p
class="m-0 text-md font-bold text-contrast leading-tight line-clamp-1 smart-clickable:underline-on-hover"
>
<p class="m-0 text-md font-bold text-contrast leading-tight line-clamp-1">
{{ instance.name }}
</p>
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold mt-auto">
@@ -242,5 +242,5 @@ onUnmounted(() => unlisten())
</div>
</div>
</div>
</SmartClickable>
</div>
</template>

View File

@@ -3,18 +3,23 @@ import { convertFileSrc } from '@tauri-apps/api/core'
import { formatCategory } from '@modrinth/utils'
import { GameIcon, LeftArrowIcon } from '@modrinth/assets'
import { Avatar, ButtonStyled } from '@modrinth/ui'
import type { GameInstance } from '@/helpers/types'
type Instance = {
game_version: string
loader: string
path: string
install_stage: string
icon_path?: string
name: string
}
defineProps<{
instance?: GameInstance
instance: Instance
}>()
</script>
<template>
<div
v-if="instance"
class="flex justify-between items-center border-0 border-b border-solid border-divider pb-4"
>
<div class="flex justify-between items-center border-0 border-b border-solid border-divider pb-4">
<router-link
:to="`/instance/${encodeURIComponent(instance.path)}`"
tabindex="-1"
@@ -44,3 +49,5 @@ defineProps<{
</ButtonStyled>
</div>
</template>
<style scoped lang="scss"></style>

View File

@@ -1,13 +1,16 @@
<script setup>
import { Avatar, SmartClickable, TagItem } from '@modrinth/ui'
import { Avatar, TagItem } from '@modrinth/ui'
import { DownloadIcon, HeartIcon, TagIcon } from '@modrinth/assets'
import { formatNumber, formatCategory } from '@modrinth/utils'
import { computed } from 'vue'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { useRouter } from 'vue-router'
dayjs.extend(relativeTime)
const router = useRouter()
const props = defineProps({
project: {
type: Object,
@@ -37,15 +40,29 @@ const toColor = computed(() => {
const r = (color >>> 16) & 0xff
return 'rgba(' + [r, g, b, 1].join(',') + ')'
})
const toTransparent = computed(() => {
let color = props.project.color
color >>>= 0
const b = color & 0xff
const g = (color >>> 8) & 0xff
const r = (color >>> 16) & 0xff
return (
'linear-gradient(rgba(' +
[r, g, b, 0.03].join(',') +
'), 65%, rgba(' +
[r, g, b, 0.3].join(',') +
'))'
)
})
</script>
<template>
<SmartClickable
<div
class="card-shadow bg-bg-raised rounded-xl overflow-clip cursor-pointer hover:brightness-90 transition-all"
@click="router.push(`/project/${project.slug}`)"
>
<template #clickable>
<router-link class="no-click-animation" :to="`/project/${project.slug}`" />
</template>
<div
class="w-full aspect-[2/1] bg-cover bg-center bg-no-repeat"
:style="{
@@ -56,13 +73,21 @@ const toColor = computed(() => {
'https://launcher-files.modrinth.com/assets/maze-bg.png'
})`,
}"
></div>
>
<div
class="badges-wrapper"
:class="{
'no-image': !project.featured_gallery && !project.gallery[0],
}"
:style="{
background: !project.featured_gallery && !project.gallery[0] ? toTransparent : null,
}"
></div>
</div>
<div class="flex flex-col justify-center gap-2 px-4 py-3">
<div class="flex gap-2 items-center">
<Avatar size="48px" :src="project.icon_url" />
<div
class="h-full flex items-center font-bold text-contrast leading-normal smart-clickable:underline-on-hover"
>
<div class="h-full flex items-center font-bold text-contrast leading-normal">
<span class="line-clamp-2">{{ project.title }}</span>
</div>
</div>
@@ -90,7 +115,7 @@ const toColor = computed(() => {
</div>
</div>
</div>
</SmartClickable>
</div>
</template>
<style scoped lang="scss"></style>

View File

@@ -1,194 +0,0 @@
<script setup lang="ts">
import {
VersionIcon,
ImageIcon,
BookmarkIcon,
DownloadIcon,
HeartIcon,
MoreVerticalIcon,
ExternalIcon,
LinkIcon,
ReportIcon,
SpinnerIcon,
CheckIcon,
} from '@modrinth/assets'
import { ButtonStyled, commonMessages, OverflowMenu } from '@modrinth/ui'
import type { GameInstance } from '@/helpers/types'
import { computed, ref, type Ref } from 'vue'
import { install as installVersion } from '@/store/install'
import { useVIntl, defineMessages } from '@vintl/vintl'
import { isSearchResult, type Project, type SearchResult } from '@modrinth/utils'
import type { InstanceContentMap } from '@/composables/instance-context.ts'
const { formatMessage } = useVIntl()
const props = defineProps<{
project: Project | SearchResult
instance?: GameInstance
instanceContent?: InstanceContentMap
}>()
const installing = ref(false)
const installed: Ref<boolean> = ref(false)
function checkInstallStatus() {
if (props.instanceContent) {
installed.value = Object.values(props.instanceContent).some(
(content) => content.metadata?.project_id === projectId.value,
)
}
}
async function install(toInstance: boolean) {
if (toInstance) {
installing.value = true
}
await installVersion(
projectId.value,
null,
props.instance && toInstance ? props.instance.path : null,
'SearchCard',
() => {
if (toInstance) {
installing.value = false
installed.value = true
}
},
)
}
const modpack = computed(() => props.project.project_type === 'modpack')
const projectWebUrl = computed(
() => `https://modrinth.com/${props.project.project_type}/${props.project.slug}`,
)
const tooltip = defineMessages({
installing: {
id: 'project.card.actions.installing.tooltip',
defaultMessage: 'This project is being installed',
},
installed: {
id: 'project.card.actions.installed.tooltip',
defaultMessage: 'This project is already installed',
},
})
const messages = defineMessages({
viewVersions: {
id: 'project.card.actions.view-versions',
defaultMessage: 'View versions',
},
viewGallery: {
id: 'project.card.actions.view-gallery',
defaultMessage: 'View gallery',
},
})
const projectId = computed(() =>
isSearchResult(props.project) ? props.project.project_id : props.project.id,
)
const copyText = (text: string) => {
navigator.clipboard.writeText(text)
}
checkInstallStatus()
</script>
<template>
<ButtonStyled color="brand">
<button
v-tooltip="
installing
? formatMessage(tooltip.installing)
: installed
? formatMessage(tooltip.installed)
: null
"
:disabled="installing || installed"
@click="() => install(true)"
>
<SpinnerIcon v-if="installing" />
<CheckIcon v-else-if="installed" />
<DownloadIcon v-else />
{{
formatMessage(
installing
? commonMessages.installingButton
: installed
? commonMessages.installedButton
: commonMessages.installButton,
)
}}
</button>
</ButtonStyled>
<!-- TODO: Add in later -->
<ButtonStyled v-if="false" circular>
<button v-tooltip="'Follow'">
<HeartIcon />
</button>
</ButtonStyled>
<ButtonStyled v-if="false" circular>
<button v-tooltip="'Save'">
<BookmarkIcon />
</button>
</ButtonStyled>
<ButtonStyled circular type="transparent">
<OverflowMenu
:options="[
{
id: 'install-elsewhere',
color: 'primary',
action: () => install(false),
shown: !!instance && !modpack,
},
{
divider: true,
shown: !!instance && !modpack,
},
{
id: 'versions',
link: `/project/${projectId}/versions`,
},
{
id: 'gallery',
link: `/project/${projectId}/gallery`,
shown: (project.gallery?.length ?? 0) > 0,
},
{
id: 'open-link',
link: projectWebUrl,
},
{
id: 'copy-link',
action: () => copyText(projectWebUrl),
},
{
divider: true,
},
{
id: 'report',
color: 'red',
hoverFilled: true,
action: () => {},
},
]"
>
<MoreVerticalIcon />
<template #install-elsewhere>
<DownloadIcon /> {{ formatMessage(commonMessages.installToButton) }}
</template>
<template #versions> <VersionIcon /> {{ formatMessage(messages.viewVersions) }} </template>
<template #gallery> <ImageIcon /> {{ formatMessage(messages.viewGallery) }} </template>
<template #open-link>
<ExternalIcon /> {{ formatMessage(commonMessages.openInBrowserButton) }}
</template>
<template #copy-link>
<LinkIcon /> {{ formatMessage(commonMessages.copyLinkButton) }}
</template>
<template #report> <ReportIcon /> {{ formatMessage(commonMessages.reportButton) }} </template>
</OverflowMenu>
</ButtonStyled>
</template>

View File

@@ -1,72 +1,157 @@
<template>
<NewProjectCard
:project="project"
:link="
asLink(
{
path: `/project/${projectId}`,
<div
class="card-shadow p-4 bg-bg-raised rounded-xl flex gap-3 group cursor-pointer hover:brightness-90 transition-all"
@click="
() => {
emit('open')
$router.push({
path: `/project/${project.project_id ?? project.id}`,
query: { i: props.instance ? props.instance.path : undefined },
},
() => emit('open'),
)
"
:experimental-colors="themeStore.featureFlags.project_card_background"
:creator-link="
creator
? asLink(
{
path: `/user/${creator}`,
query: { i: props.instance ? props.instance.path : undefined },
},
() => emit('open'),
)
: undefined
})
}
"
>
<template #actions>
<ButtonStyled color="brand">
<button
:disabled="installed || installing"
class="shrink-0 no-wrap"
@click.stop="install()"
<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="gap-2 overflow-hidden no-wrap text-ellipsis">
<span class="text-lg font-extrabold text-contrast m-0 leading-none">
{{ project.title }}
</span>
<span v-if="project.author" class="text-secondary"> by {{ project.author }}</span>
</div>
<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">
<TagsIcon class="h-4 w-4 shrink-0" />
<div
v-if="project.project_type === 'mod' || project.project_type === 'modpack'"
class="text-sm font-semibold text-secondary flex gap-1 px-[0.375rem] py-0.5 bg-button-bg rounded-full"
>
<template v-if="!installed">
<DownloadIcon />
<template v-if="project.client_side === 'optional' && project.server_side === 'optional'">
Client or server
</template>
<CheckIcon v-else />
{{ installing ? 'Installing' : installed ? 'Installed' : 'Install' }}
</button>
</ButtonStyled>
</template>
</NewProjectCard>
<template
v-else-if="
(project.client_side === 'optional' || project.client_side === 'required') &&
(project.server_side === 'optional' || project.server_side === 'unsupported')
"
>
Client
</template>
<template
v-else-if="
(project.server_side === 'optional' || project.server_side === 'required') &&
(project.client_side === 'optional' || project.client_side === 'unsupported')
"
>
Server
</template>
<template
v-else-if="
project.client_side === 'unsupported' && project.server_side === 'unsupported'
"
>
Unsupported
</template>
<template
v-else-if="project.client_side === 'required' && project.server_side === 'required'"
>
Client and server
</template>
</div>
<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"
>
{{ formatCategory(tag.name) }}
</div>
</div>
</div>
<div class="flex flex-col gap-2 items-end shrink-0 ml-auto">
<div class="flex items-center gap-2">
<DownloadIcon class="shrink-0" />
<span>
{{ formatNumber(project.downloads) }}
<span class="text-secondary">downloads</span>
</span>
</div>
<div class="flex items-center gap-2">
<HeartIcon class="shrink-0" />
<span>
{{ formatNumber(project.follows ?? project.followers) }}
<span class="text-secondary">followers</span>
</span>
</div>
<div class="mt-auto relative">
<div class="absolute bottom-0 right-0 w-fit">
<ButtonStyled color="brand" type="outlined">
<button
:disabled="installed || installing"
class="shrink-0 no-wrap"
@click.stop="install()"
>
<template v-if="!installed">
<DownloadIcon v-if="modpack || instance" />
<PlusIcon v-else />
</template>
<CheckIcon v-else />
{{
installing
? 'Installing'
: installed
? 'Installed'
: modpack || instance
? 'Install'
: 'Add to an instance'
}}
</button>
</ButtonStyled>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { DownloadIcon, CheckIcon } from '@modrinth/assets'
import { ButtonStyled, NewProjectCard, asLink } from '@modrinth/ui'
import type { Project, SearchResult } from '@modrinth/utils'
import { isSearchResult } from '@modrinth/utils'
<script setup>
import { TagsIcon, DownloadIcon, HeartIcon, PlusIcon, CheckIcon } from '@modrinth/assets'
import { ButtonStyled, Avatar } from '@modrinth/ui'
import { formatNumber, formatCategory } from '@modrinth/utils'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { ref, computed } from 'vue'
import { install as installVersion } from '@/store/install.js'
import { useTheming } from '@/store/state.js'
import type { GameInstance } from '@/helpers/types'
dayjs.extend(relativeTime)
const themeStore = useTheming()
const props = withDefaults(
defineProps<{
project: Project | SearchResult
instance?: GameInstance
installed?: boolean
}>(),
{
instance: undefined,
installed: false,
const props = defineProps({
backgroundImage: {
type: String,
default: null,
},
)
project: {
type: Object,
required: true,
},
categories: {
type: Array,
required: true,
},
instance: {
type: Object,
default: null,
},
featured: {
type: Boolean,
default: false,
},
installed: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['open', 'install'])
@@ -75,19 +160,16 @@ const installing = ref(false)
async function install() {
installing.value = true
await installVersion(
projectId.value,
props.project.project_id ?? props.project.id,
null,
props.instance ? props.instance.path : null,
'SearchCard',
() => {
installing.value = false
emit('install', projectId.value)
emit('install', props.project.project_id ?? props.project.id)
},
)
}
const projectId = computed(() =>
isSearchResult(props.project) ? props.project.project_id : props.project.id,
)
const creator = computed(() => (isSearchResult(props.project) ? props.project.author : undefined))
const modpack = computed(() => props.project.project_type === 'modpack')
</script>

View File

@@ -116,6 +116,10 @@ function devModeCount() {
themeStore.devMode = !themeStore.devMode
settings.value.developer_mode = !!themeStore.devMode
devModeCounter.value = 0
if (!themeStore.devMode && tabs[modal.value.selectedTab].developerOnly) {
modal.value.setTab(0)
}
}
}
</script>

View File

@@ -1,22 +1,19 @@
<script setup lang="ts">
import { ButtonStyled, Toggle } from '@modrinth/ui'
import { Toggle } from '@modrinth/ui'
import { useTheming } from '@/store/state'
import { ref, type Ref, watch } from 'vue'
import { ref, watch } from 'vue'
import { get, set } from '@/helpers/settings'
import { DEFAULT_FEATURE_FLAGS } from '@/store/theme'
type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS
const themeStore = useTheming()
const settings = ref(await get())
const options: Ref<FeatureFlag[]> = ref(Object.keys(DEFAULT_FEATURE_FLAGS) as FeatureFlag[])
const options = ref(['project_background', 'page_path'])
function getStoreValue(key: FeatureFlag) {
function getStoreValue(key: string) {
return themeStore.featureFlags[key] ?? false
}
function setStoreValue(key: FeatureFlag, value: boolean) {
function setStoreValue(key: string, value: boolean) {
themeStore.featureFlags[key] = value
settings.value.feature_flags[key] = value
}
@@ -30,39 +27,17 @@ watch(
)
</script>
<template>
<h2 class="m-0 text-lg font-extrabold text-contrast">Feature flags</h2>
<p class="mt-1 mb-0 leading-tight text-secondary">
These are developer tools that are not intended to be used by end users except for debugging
purposes.
</p>
<p class="my-3 font-bold">Do not report bugs or issues if you have any feature flags enabled.</p>
<div
v-for="option in options"
:key="option"
class="mt-2 px-4 py-3 flex items-center justify-between bg-bg rounded-2xl"
>
<div v-for="option in options" :key="option" class="mt-4 flex items-center justify-between">
<div>
<h2 class="m-0 text-base font-bold text-primary capitalize">
{{ option.replace(new RegExp('_', 'g'), ' ') }}
<h2 class="m-0 text-lg font-extrabold text-contrast capitalize">
{{ option }}
</h2>
<p class="m-0 text-sm text-secondary">Default: {{ DEFAULT_FEATURE_FLAGS[option] }}</p>
</div>
<div class="flex items-center gap-1">
<ButtonStyled type="transparent">
<button
class="text-sm"
:disabled="getStoreValue(option) === DEFAULT_FEATURE_FLAGS[option]"
@click="() => setStoreValue(option, !themeStore.featureFlags[option])"
>
Reset to default
</button>
</ButtonStyled>
<Toggle
id="advanced-rendering"
:model-value="getStoreValue(option)"
@update:model-value="() => setStoreValue(option, !themeStore.featureFlags[option])"
/>
</div>
<Toggle
id="advanced-rendering"
:model-value="getStoreValue(option)"
@update:model-value="() => setStoreValue(option, !themeStore.featureFlags[option])"
/>
</div>
</template>

View File

@@ -1,41 +0,0 @@
import { useRoute } from 'vue-router'
import { ref, computed, type Ref, watch } from 'vue'
import { handleError } from '@/store/notifications'
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile'
import type { GameInstance, InstanceContent } from '@/helpers/types'
export type InstanceContentMap = Record<string, InstanceContent>
export async function useInstanceContext() {
const route = useRoute()
const instance: Ref<GameInstance | undefined> = ref()
const instanceContent: Ref<InstanceContentMap | undefined> = ref()
await loadInstance()
watch(route, () => {
loadInstance()
})
async function loadInstance() {
;[instance.value, instanceContent.value] = await Promise.all([
route.query.i ? getInstance(route.query.i).catch(handleError) : Promise.resolve(),
route.query.i ? getInstanceProjects(route.query.i).catch(handleError) : Promise.resolve(),
])
}
const instanceQueryAppendage = computed(() => {
if (instance.value) {
return `?i=${instance.value.path}`
} else {
return ''
}
})
return {
instance,
instanceContent,
instanceQueryAppendage,
}
}

View File

@@ -32,18 +32,6 @@ type GameInstance = {
hooks: Hooks
}
type InstanceContent = {
hash: string
file_name: string
size: number
metadata?: {
project_id: ModrinthId
version_id: ModrinthId
}
update_version_id: string
project_type: 'mod' | 'resourcepack' | 'datapack' | 'shaderpack'
}
type InstallStage =
| 'installed'
| 'minecraft_installing'

View File

@@ -308,18 +308,6 @@
"instance.settings.title": {
"message": "Settings"
},
"project.card.actions.installed.tooltip": {
"message": "This project is already installed"
},
"project.card.actions.installing.tooltip": {
"message": "This project is being installed"
},
"project.card.actions.view-gallery": {
"message": "View gallery"
},
"project.card.actions.view-versions": {
"message": "View versions"
},
"search.filter.locked.instance": {
"message": "Provided by the instance"
},

View File

@@ -2,14 +2,7 @@
import { computed, nextTick, ref, shallowRef, watch } from 'vue'
import type { Ref } from 'vue'
import { SearchIcon, XIcon, ClipboardCopyIcon, GlobeIcon, ExternalIcon } from '@modrinth/assets'
import type {
CategoryTag,
GameVersionTag,
PlatformTag,
ProjectType,
SortType,
Tags,
} from '@modrinth/ui'
import type { Category, GameVersion, Platform, ProjectType, SortType, Tags } from '@modrinth/ui'
import {
SearchFilterControl,
SearchSidebarFilter,
@@ -26,14 +19,14 @@ import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
import type { LocationQuery } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
import SearchCard from '@/components/ui/SearchCard.vue'
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile.js'
import { get_search_results } from '@/helpers/cache.js'
import NavTabs from '@/components/ui/NavTabs.vue'
import type Instance from '@/components/ui/Instance.vue'
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
import { defineMessages, useVIntl } from '@vintl/vintl'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import { openUrl } from '@tauri-apps/plugin-opener'
import { useInstanceContext } from '@/composables/instance-context.ts'
import type { SearchResult } from '@modrinth/utils'
const { formatMessage } = useVIntl()
@@ -45,45 +38,62 @@ const projectTypes = computed(() => {
})
const [categories, loaders, availableGameVersions] = await Promise.all([
get_categories()
.catch(handleError)
.then((x: CategoryTag[]) => ref(x)),
get_loaders()
.catch(handleError)
.then((x: PlatformTag[]) => ref(x)),
get_game_versions()
.catch(handleError)
.then((x: GameVersionTag[]) => ref(x)),
get_categories().catch(handleError).then(ref),
get_loaders().catch(handleError).then(ref),
get_game_versions().catch(handleError).then(ref),
])
const tags: Ref<Tags> = computed(() => ({
gameVersions: availableGameVersions.value as GameVersionTag[],
loaders: loaders.value as PlatformTag[],
categories: categories.value as CategoryTag[],
gameVersions: availableGameVersions.value as GameVersion[],
loaders: loaders.value as Platform[],
categories: categories.value as Category[],
}))
const instanceHideInstalled = ref(false)
const newlyInstalled: Ref<string[]> = ref([])
type Instance = {
game_version: string
loader: string
path: string
install_stage: string
icon_path?: string
name: string
}
const { instance, instanceContent } = await useInstanceContext()
type InstanceProject = {
metadata: {
project_id: string
}
}
const instance: Ref<Instance | null> = ref(null)
const instanceProjects: Ref<InstanceProject[] | null> = ref(null)
const instanceHideInstalled = ref(false)
const newlyInstalled = ref([])
const PERSISTENT_QUERY_PARAMS = ['i', 'ai']
await checkHideInstalledQuery()
await updateInstanceContext()
watch(instance, () => {
checkHideInstalledQuery()
watch(route, () => {
updateInstanceContext()
})
async function checkHideInstalledQuery() {
async function updateInstanceContext() {
if (route.query.i) {
;[instance.value, instanceProjects.value] = await Promise.all([
getInstance(route.query.i).catch(handleError),
getInstanceProjects(route.query.i).catch(handleError),
])
newlyInstalled.value = []
}
if (route.query.ai && !(projectTypes.value.length === 1 && projectTypes.value[0] === 'modpack')) {
instanceHideInstalled.value = route.query.ai === 'true'
}
// if (instance.value && instance.value.path !== route.query.i && route.path.startsWith('/browse')) {
// instance.value = undefined
// instanceHideInstalled.value = false
// }
if (instance.value && instance.value.path !== route.query.i && route.path.startsWith('/browse')) {
instance.value = null
instanceHideInstalled.value = false
}
}
const instanceFilters = computed(() => {
@@ -109,10 +119,10 @@ const instanceFilters = computed(() => {
})
}
if (instanceHideInstalled.value && instanceContent.value) {
const installedMods: string[] = Object.values(instanceContent.value)
if (instanceHideInstalled.value && instanceProjects.value) {
const installedMods = Object.values(instanceProjects.value)
.filter((x) => x.metadata)
.map((x) => x.metadata!.project_id)
.map((x) => x.metadata.project_id)
installedMods.push(...newlyInstalled.value)
@@ -163,27 +173,23 @@ breadcrumbs.setContext({ name: 'Discover content', link: route.path, query: rout
const loading = ref(true)
const projectType: Ref<ProjectType | undefined> = ref(
typeof route.params.projectType === 'string'
? (route.params.projectType as ProjectType)
: undefined,
)
const projectType = ref(route.params.projectType)
watch(projectType, () => {
loading.value = true
})
type ExtendedSearchResult = SearchResult & {
installed?: boolean
type SearchResult = {
project_id: string
}
type SearchResults = {
total_hits: number
limit: number
hits: ExtendedSearchResult[]
hits: SearchResult[]
}
const results: Ref<SearchResults | undefined> = shallowRef()
const results: Ref<SearchResults | null> = shallowRef(null)
const pageCount = computed(() =>
results.value ? Math.ceil(results.value.total_hits / results.value.limit) : 1,
)
@@ -194,7 +200,7 @@ watch(requestParams, () => {
})
async function refreshSearch() {
let rawResults = (await get_search_results(requestParams.value)) as { result: SearchResults }
let rawResults = await get_search_results(requestParams.value)
if (!rawResults) {
rawResults = {
result: {
@@ -205,15 +211,13 @@ async function refreshSearch() {
}
}
if (instance.value) {
rawResults.result.hits.map((x) => ({
...x,
installed:
newlyInstalled.value.includes(x.project_id) ||
(instanceContent.value &&
Object.values(instanceContent.value).some(
(content) => content.metadata && content.metadata.project_id === x.project_id,
)),
}))
for (const val of rawResults.result.hits) {
val.installed =
newlyInstalled.value.includes(val.project_id) ||
Object.values(instanceProjects.value).some(
(x) => x.metadata && x.metadata.project_id === val.project_id,
)
}
}
results.value = rawResults.result
@@ -267,9 +271,9 @@ watch(
() => route.params.projectType,
async (newType) => {
// Check if the newType is not the same as the current value
if (!newType || newType === projectType.value || typeof newType !== 'string') return
if (!newType || newType === projectType.value) return
projectType.value = newType as ProjectType
projectType.value = newType
currentSortType.value = { display: 'Relevance', name: 'relevance' }
query.value = ''
@@ -283,7 +287,7 @@ const selectableProjectTypes = computed(() => {
if (instance.value) {
if (
availableGameVersions.value.findIndex((x) => x.version === instance.value?.game_version) <=
availableGameVersions.value.findIndex((x) => x.version === instance.value.game_version) <=
availableGameVersions.value.findIndex((x) => x.version === '1.13')
) {
dataPacks = true
@@ -349,10 +353,9 @@ const messages = defineMessages({
},
})
const options: Ref<InstanceType<typeof ContextMenu> | null> = ref(null)
const handleRightClick = (event: MouseEvent, result: ExtendedSearchResult) => {
options.value?.showMenu(event, result, [
const options = ref(null)
const handleRightClick = (event, result) => {
options.value.showMenu(event, result, [
{
name: 'open_link',
},
@@ -361,7 +364,7 @@ const handleRightClick = (event: MouseEvent, result: ExtendedSearchResult) => {
},
])
}
const handleOptionsClick = (args: { item: ExtendedSearchResult; option: string }) => {
const handleOptionsClick = (args) => {
switch (args.option) {
case 'open_link':
openUrl(`https://modrinth.com/${args.item.project_type}/${args.item.slug}`)
@@ -474,26 +477,33 @@ await refreshSearch()
<section v-if="loading" class="offline">
<LoadingIndicator />
</section>
<section v-else-if="offline && (!results || results.total_hits === 0)" class="offline">
<section v-else-if="offline && results.total_hits === 0" class="offline">
You are currently offline. Connect to the internet to browse Modrinth!
</section>
<section
v-else-if="results"
class="project-list display-mode--list instance-results"
role="list"
>
<section v-else class="project-list display-mode--list instance-results" role="list">
<SearchCard
v-for="result in results.hits"
:key="result?.project_id"
:project="result"
:instance="instance"
:categories="[
...categories.filter(
(cat) =>
result?.display_categories.includes(cat.name) && cat.project_type === projectType,
),
...loaders.filter(
(loader) =>
result?.display_categories.includes(loader.name) &&
loader.supported_project_types?.includes(projectType),
),
]"
:installed="result.installed || newlyInstalled.includes(result.project_id)"
@install="
(id) => {
newlyInstalled.push(id)
}
"
@contextmenu.prevent.stop="(event: MouseEvent) => handleRightClick(event, result)"
@contextmenu.prevent.stop="(event) => handleRightClick(event, result)"
/>
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>

View File

@@ -1,136 +0,0 @@
<template>
<Teleport to="#sidebar-teleport-target">
<CollectionSidebarDescription
v-if="collection"
:collection="collection"
class="project-sidebar-section"
/>
<CollectionSidebarCurator
v-if="curator"
:user="curator"
:link="`/user/${curator.id}`"
class="project-sidebar-section"
/>
<CollectionSidebarDetails
v-if="collection"
:collection="collection"
class="project-sidebar-section"
/>
</Teleport>
<div v-if="collection" class="p-6 flex flex-col gap-4">
<InstanceIndicator :instance="instance" />
<CollectionHeader :collection="collection">
<template #actions>
<ButtonStyled v-if="themeStore.devMode" circular type="transparent" size="large">
<OverflowMenu
:options="[{ id: 'copy-id', action: () => copyId(), shown: themeStore.devMode }]"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #copy-id>
<ClipboardCopyIcon aria-hidden="true" />
{{ formatMessage(commonMessages.copyIdButton) }}
</template>
</OverflowMenu>
</ButtonStyled>
</template>
</CollectionHeader>
<div v-if="projects">
<ProjectsList
:projects="projects"
:project-link="(project) => `/project/${project.id}${instanceQueryAppendage}`"
:experimental-colors="themeStore.featureFlags.project_card_background"
>
<template #project-actions="{ project }">
<ProjectCardActions
:instance="instance"
:instance-content="instanceContent"
:project="project"
/>
</template>
</ProjectsList>
</div>
</div>
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router'
import { ref, type Ref, watch } from 'vue'
import { handleError } from '@/store/notifications.js'
import {
ProjectsList,
ButtonStyled,
commonMessages,
OverflowMenu,
CollectionHeader,
CollectionSidebarCurator,
CollectionSidebarDescription,
CollectionSidebarDetails,
} from '@modrinth/ui'
import { ClipboardCopyIcon, MoreVerticalIcon } from '@modrinth/assets'
import { useVIntl } from '@vintl/vintl'
import { useFetch } from '@/helpers/fetch'
import type { User, Project, Collection } from '@modrinth/utils'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { useTheming } from '@/store/theme'
import { useInstanceContext } from '@/composables/instance-context'
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
import ProjectCardActions from '@/components/ui/ProjectCardActions.vue'
const breadcrumbs = useBreadcrumbs()
const route = useRoute()
const { formatMessage } = useVIntl()
const collection: Ref<Collection | null> = ref(null)
const curator: Ref<User | null> = ref(null)
const projects: Ref<Project[]> = ref([])
async function fetchCollection() {
collection.value = await useFetch(
`https://api.modrinth.com/v3/collection/${route.params.id}`,
).catch(handleError)
if (!collection.value) {
return
}
;[projects.value, curator.value] = await Promise.all([
useFetch(
`https://api.modrinth.com/v2/projects?ids=${encodeURIComponent(JSON.stringify(collection.value.projects))}`,
),
useFetch(`https://api.modrinth.com/v2/user/${collection.value.user}`).catch(handleError),
])
breadcrumbs.setContext({ name: 'Collection', link: `/collection/${collection.value.name}` })
breadcrumbs.setName('Collection', collection.value.name)
}
await fetchCollection()
const { instance, instanceContent, instanceQueryAppendage } = await useInstanceContext()
watch(
() => route.params.id,
async () => {
if (route.params.id && route.path.startsWith('/collection')) {
await fetchCollection()
}
},
)
const themeStore = useTheming()
async function copyId() {
if (collection.value) {
await navigator.clipboard.writeText(String(collection.value.id))
}
}
</script>
<style scoped lang="scss">
.project-sidebar-section {
@apply p-4 flex flex-col gap-2 border-0 border-[--brand-gradient-border] border-solid;
}
.project-sidebar-section:not(:last-child) {
@apply border-b-[1px];
}
</style>

View File

@@ -1,3 +0,0 @@
import Index from './Index.vue'
export { Index }

View File

@@ -17,11 +17,11 @@
<div
class="flex items-center gap-2 font-semibold transform capitalize border-0 border-solid border-divider pr-4 md:border-r"
>
<GameIcon class="h-5 w-5 text-secondary" />
<GameIcon class="h-6 w-6 text-secondary" />
{{ instance.loader }} {{ instance.game_version }}
</div>
<div class="flex items-center gap-2 font-semibold">
<TimerIcon class="h-5 w-5 text-secondary" />
<TimerIcon class="h-6 w-6 text-secondary" />
<template v-if="timePlayed > 0">
{{ timePlayedHumanized }}
</template>

View File

@@ -66,13 +66,10 @@
if (x.author) {
item.creator = {
name: x.author.name,
type: x.author.type,
id: x.author.slug,
link: {
path: `/${x.author.type}/${x.author.slug}`,
query: { i: props.instance.path },
},
name: x.author,
type: 'user',
id: x.author,
link: 'https://modrinth.com/user/' + x.author,
linkProps: { target: '_blank' },
}
}
@@ -332,28 +329,6 @@ const props = defineProps({
},
})
type ProjectListEntryAuthor = {
name: string
slug: string
type: 'user' | 'organization'
}
type ProjectListEntry = {
path: string
name: string
slug?: string
author: ProjectListEntryAuthor | null
version: string | null
file_name: string
icon: string | null
disabled: boolean
updateVersion?: string
outdated: boolean
updated: dayjs.Dayjs
project_type: string
id?: string
}
const isPackLocked = computed(() => {
return props.instance.linked_data && props.instance.linked_data.locked
})
@@ -363,7 +338,7 @@ const canUpdatePack = computed(() => {
})
const exportModal = ref(null)
const projects = ref<ProjectListEntry[]>([])
const projects = ref([])
const selectedFiles = ref([])
const selectedProjects = computed(() =>
projects.value.filter((x) => selectedFiles.value.includes(x.file_name)),
@@ -372,7 +347,7 @@ const selectedProjects = computed(() =>
const selectionMap = ref(new Map())
const initProjects = async (cacheBehaviour?) => {
const newProjects: ProjectListEntry[] = []
const newProjects = []
const profileProjects = await get_projects(props.instance.path, cacheBehaviour)
const fetchProjects = []
@@ -409,29 +384,21 @@ const initProjects = async (cacheBehaviour?) => {
const team = modrinthTeams.find((x) => x[0].team_id === project.team)
let author: ProjectListEntryAuthor | null
let owner
if (org) {
author = {
name: org.name,
slug: org.slug,
type: 'organization',
}
owner = org.name
} else if (team) {
const teamMember = team.find((x) => x.is_owner)
author = {
name: teamMember.user.username,
slug: teamMember.user.username,
type: 'user',
}
owner = team.find((x) => x.is_owner).user.username
} else {
author = null
owner = null
}
newProjects.push({
path,
name: project.title,
slug: project.slug,
author,
author: owner,
version: version.version_number,
file_name: file.file_name,
icon: project.icon_url,
@@ -450,7 +417,7 @@ const initProjects = async (cacheBehaviour?) => {
newProjects.push({
path,
name: file.file_name.replace('.disabled', ''),
author: null,
author: '',
version: null,
file_name: file.file_name,
icon: null,

View File

@@ -10,7 +10,7 @@ defineProps({
</script>
<template>
<GridDisplay
v-if="instances && instances.length > 0"
v-if="instances.length > 0"
label="Instances"
:instances="instances.filter((i) => !i.linked_data)"
/>

View File

@@ -10,7 +10,7 @@ defineProps({
</script>
<template>
<GridDisplay
v-if="instances && instances.length > 0"
v-if="instances.length > 0"
label="Instances"
:instances="instances.filter((i) => i.linked_data)"
/>

View File

@@ -9,5 +9,5 @@ defineProps({
})
</script>
<template>
<GridDisplay v-if="instances && instances.length > 0" label="Instances" :instances="instances" />
<GridDisplay v-if="instances.length > 0" label="Instances" :instances="instances" />
</template>

View File

@@ -1,176 +0,0 @@
<template>
<Teleport to="#sidebar-teleport-target">
<OrganizationSidebarMembers
v-if="organization"
:members="organization.members"
:user-link="(user) => `/user/${user.id}`"
class="project-sidebar-section"
/>
</Teleport>
<div v-if="organization" class="flex flex-col gap-4 p-6">
<InstanceIndicator :instance="instance" />
<OrganizationHeader
:organization="organization"
:download-count="sumDownloads"
:project-count="projects.length"
>
<template #actions>
<ButtonStyled v-if="themeStore.devMode" circular type="transparent" size="large">
<OverflowMenu
:options="[{ id: 'copy-id', action: () => copyId(), shown: themeStore.devMode }]"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #copy-id>
<ClipboardCopyIcon aria-hidden="true" />
{{ formatMessage(commonMessages.copyIdButton) }}
</template>
</OverflowMenu>
</ButtonStyled>
</template>
</OrganizationHeader>
<div v-if="projects">
<ProjectsList
:projects="projects"
:project-link="(project) => `/project/${project.id}${instanceQueryAppendage}`"
:experimental-colors="themeStore.featureFlags.project_card_background"
>
<template #project-actions="{ project }">
<ProjectCardActions
:project="project"
:instance="instance"
:instance-content="instanceContent"
/>
</template>
</ProjectsList>
</div>
</div>
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router'
import { ref, type Ref, watch, computed } from 'vue'
import { handleError } from '@/store/notifications.js'
import {
ProjectsList,
ButtonStyled,
commonMessages,
OverflowMenu,
OrganizationHeader,
OrganizationSidebarMembers,
} from '@modrinth/ui'
import { ClipboardCopyIcon, MoreVerticalIcon } from '@modrinth/assets'
import { useVIntl } from '@vintl/vintl'
import { useFetch } from '@/helpers/fetch'
import type { Project, Organization, ProjectV3, Environment } from '@modrinth/utils'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { useTheming } from '@/store/theme'
import { useInstanceContext } from '@/composables/instance-context.ts'
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
import ProjectCardActions from '@/components/ui/ProjectCardActions.vue'
const breadcrumbs = useBreadcrumbs()
const route = useRoute()
const { formatMessage } = useVIntl()
const organization: Ref<Organization | null> = ref(null)
const projects: Ref<Project[]> = ref([])
const { instance, instanceContent, instanceQueryAppendage } = await useInstanceContext()
async function fetchOrganization() {
organization.value = await useFetch(
`https://api.modrinth.com/v3/organization/${route.params.id}`,
).catch(handleError)
projects.value = (
await useFetch(`https://api.modrinth.com/v3/organization/${route.params.id}/projects`).catch(
handleError,
)
).map((projectV3: ProjectV3) => {
let type = projectV3.project_types[0]
if (type === 'plugin' || type === 'datapack') {
type = 'mod'
}
let clientSide: Environment = 'unknown'
let serverSide: Environment = 'unknown'
const singleplayer = projectV3.singleplayer && projectV3.singleplayer[0]
const clientAndServer = projectV3.client_and_server && projectV3.client_and_server[0]
const clientOnly = projectV3.client_only && projectV3.client_only[0]
const serverOnly = projectV3.server_only && projectV3.server_only[0]
// quick and dirty hack to show envs as legacy
if (singleplayer && clientAndServer && !clientOnly && !serverOnly) {
clientSide = 'required'
serverSide = 'required'
} else if (singleplayer && clientAndServer && clientOnly && !serverOnly) {
clientSide = 'required'
serverSide = 'unsupported'
} else if (singleplayer && clientAndServer && !clientOnly && serverOnly) {
clientSide = 'unsupported'
serverSide = 'required'
} else if (singleplayer && clientAndServer && clientOnly && serverOnly) {
clientSide = 'optional'
serverSide = 'optional'
}
const projectV2: Project = {
...projectV3,
title: projectV3.name,
description: projectV3.summary,
body: projectV3.description,
project_type: type,
team: projectV3.team_id,
donation_urls: [],
client_side: clientSide,
server_side: serverSide,
}
return projectV2
})
if (!organization.value) {
return
}
breadcrumbs.setName('Organization', organization.value.name)
}
await fetchOrganization()
watch(
() => route.params.id,
async () => {
if (route.params.id && route.path.startsWith('/organization')) {
await fetchOrganization()
}
},
)
const themeStore = useTheming()
async function copyId() {
if (organization.value) {
await navigator.clipboard.writeText(String(organization.value.id))
}
}
const sumDownloads = computed(() => {
let sum = 0
for (const project of projects.value) {
sum += project.downloads
}
return sum
})
</script>
<style scoped lang="scss">
.project-sidebar-section {
@apply p-4 flex flex-col gap-2 border-0 border-[--brand-gradient-border] border-solid;
}
.project-sidebar-section:not(:last-child) {
@apply border-b-[1px];
}
</style>

View File

@@ -1,3 +0,0 @@
import Index from './Index.vue'
export { Index }

View File

@@ -31,10 +31,10 @@
: 'https://cdn.modrinth.com/placeholder-banner.svg'
"
:alt="expandedGalleryItem.title ? expandedGalleryItem.title : 'gallery-image'"
@click.stop="() => {}"
@click.stop=""
/>
<div class="floating" @click.stop="() => {}">
<div class="floating" @click.stop="">
<div class="text">
<h2 v-if="expandedGalleryItem.title">
{{ expandedGalleryItem.title }}
@@ -99,7 +99,7 @@ import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
const props = defineProps({
project: {
type: Object,
default: () => ({}),
default: () => {},
},
})

View File

@@ -8,10 +8,11 @@
/>
<ProjectSidebarLinks link-target="_blank" :project="data" class="project-sidebar-section" />
<ProjectSidebarCreators
:organization="organization"
:organization="null"
:members="members"
:org-link="(org) => `/organization/${org.id}${instance ? '?i=' + instance.path : ''}`"
:user-link="(user) => `/user/${user.id}${instance ? '?i=' + instance.path : ''}`"
:org-link="(slug) => `https://modrinth.com/organization/${slug}`"
:user-link="(username) => `https://modrinth.com/user/${username}`"
link-target="_blank"
class="project-sidebar-section"
/>
<ProjectSidebarDetails
@@ -22,7 +23,7 @@
/>
</Teleport>
<div class="flex flex-col gap-4 p-6">
<InstanceIndicator :instance="instance" />
<InstanceIndicator v-if="instance" :instance="instance" />
<template v-if="data">
<Teleport
v-if="themeStore.featureFlags.project_background"
@@ -91,11 +92,6 @@
label: 'Description',
href: `/project/${$route.params.id}`,
},
{
label: 'Gallery',
href: `/project/${$route.params.id}/gallery`,
shown: data.gallery.length > 0,
},
{
label: 'Versions',
href: {
@@ -104,6 +100,11 @@
},
subpages: ['version'],
},
{
label: 'Gallery',
href: `/project/${$route.params.id}/gallery`,
shown: data.gallery.length > 0,
},
]"
/>
<RouterView
@@ -165,7 +166,6 @@ import NavTabs from '@/components/ui/NavTabs.vue'
import { useTheming } from '@/store/state.js'
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
import { openUrl } from '@tauri-apps/plugin-opener'
import { useFetch } from '@/helpers/fetch.js'
dayjs.extend(relativeTime)
@@ -177,7 +177,6 @@ const installing = ref(false)
const data = shallowRef(null)
const versions = shallowRef([])
const members = shallowRef([])
const organization = shallowRef(null)
const categories = shallowRef([])
const instance = ref(null)
const instanceProjects = ref(null)
@@ -203,12 +202,6 @@ async function fetchProjectData() {
route.query.i ? getInstanceProjects(route.query.i).catch(handleError) : Promise.resolve(),
])
if (project.organization) {
organization.value = await useFetch(
`https://api.modrinth.com/v3/organization/${project.organization}`,
).catch(handleError)
}
versions.value = versions.value.sort((a, b) => dayjs(b.date_published) - dayjs(a.date_published))
if (instanceProjects.value) {
@@ -220,7 +213,6 @@ async function fetchProjectData() {
installedVersion.value = installedFile.metadata.version_id
}
}
breadcrumbs.setName('Project', data.value.title)
}
@@ -445,10 +437,6 @@ const handleOptionsClick = (args) => {
}
.project-sidebar-section {
@apply p-4 flex flex-col gap-2 border-0 border-[--brand-gradient-border] border-solid;
}
.project-sidebar-section:not(:last-child) {
@apply border-b-[1px];
@apply p-4 flex flex-col gap-2 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid;
}
</style>

View File

@@ -1,156 +0,0 @@
<template>
<Teleport to="#sidebar-teleport-target">
<UserSidebarOrganizations
:organizations="organizations"
:link="(org: Organization) => `/organization/${org.id}${instanceQueryAppendage}`"
class="project-sidebar-section"
/>
<UserSidebarBadges
v-if="user"
:user="user"
:download-count="sumDownloads"
class="project-sidebar-section"
/>
<UserSidebarCollections
:collections="collections"
:link="(collection: Collection) => `/collection/${collection.id}${instanceQueryAppendage}`"
class="project-sidebar-section"
/>
</Teleport>
<div v-if="user" class="p-6 flex flex-col gap-4">
<InstanceIndicator :instance="instance" />
<UserHeader :user="user" :project-count="projects.length" :download-count="sumDownloads">
<template #actions>
<ButtonStyled circular type="transparent" size="large">
<OverflowMenu
:options="[
{
id: 'report',
link: `https://modrinth.com/report?item=user&itemID=${user.id}`,
color: 'red',
},
{ id: 'copy-id', action: () => copyId(), shown: themeStore.devMode },
]"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #report>
<ReportIcon aria-hidden="true" />
{{ formatMessage(commonMessages.reportButton) }}
</template>
<template #copy-id>
<ClipboardCopyIcon aria-hidden="true" />
{{ formatMessage(commonMessages.copyIdButton) }}
</template>
</OverflowMenu>
</ButtonStyled>
</template>
</UserHeader>
<div v-if="projects">
<ProjectsList
:projects="projects"
:project-link="(project) => `/project/${project.id}${instanceQueryAppendage}`"
:experimental-colors="themeStore.featureFlags.project_card_background"
>
<template #project-actions="{ project }">
<ProjectCardActions
:instance="instance"
:instance-content="instanceContent"
:project="project"
/>
</template>
</ProjectsList>
</div>
</div>
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router'
import { ref, type Ref, watch, computed } from 'vue'
import { handleError } from '@/store/notifications.js'
import {
ProjectsList,
UserSidebarOrganizations,
ButtonStyled,
commonMessages,
OverflowMenu,
UserHeader,
UserSidebarBadges,
UserSidebarCollections,
} from '@modrinth/ui'
import { ReportIcon, ClipboardCopyIcon, MoreVerticalIcon } from '@modrinth/assets'
import { useVIntl } from '@vintl/vintl'
import { useFetch } from '@/helpers/fetch'
import type { User, Project, Organization, Collection } from '@modrinth/utils'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { useTheming } from '@/store/theme'
import { useInstanceContext } from '@/composables/instance-context'
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
import ProjectCardActions from '@/components/ui/ProjectCardActions.vue'
const breadcrumbs = useBreadcrumbs()
const route = useRoute()
const { formatMessage } = useVIntl()
const user: Ref<User | null> = ref(null)
const projects: Ref<Project[]> = ref([])
const organizations: Ref<Organization[]> = ref([])
const collections: Ref<Collection[]> = ref([])
async function fetchUser() {
;[user.value, projects.value, organizations.value, collections.value] = await Promise.all([
useFetch(`https://api.modrinth.com/v2/user/${route.params.id}`).catch(handleError),
useFetch(`https://api.modrinth.com/v2/user/${route.params.id}/projects`).catch(handleError),
useFetch(`https://api.modrinth.com/v3/user/${route.params.id}/organizations`).catch(
handleError,
),
useFetch(`https://api.modrinth.com/v3/user/${route.params.id}/collections`).catch(handleError),
])
if (!user.value) {
return
}
breadcrumbs.setContext({ name: 'User', link: `/user/${user.value.username}` })
breadcrumbs.setName('User', user.value.username)
}
await fetchUser()
const { instance, instanceContent, instanceQueryAppendage } = await useInstanceContext()
watch(
() => route.params.id,
async () => {
if (route.params.id && route.path.startsWith('/user')) {
await fetchUser()
}
},
)
const themeStore = useTheming()
async function copyId() {
if (user.value) {
await navigator.clipboard.writeText(String(user.value.id))
}
}
const sumDownloads = computed(() => {
let sum = 0
for (const project of projects.value) {
sum += project.downloads
}
return sum
})
</script>
<style scoped lang="scss">
.project-sidebar-section {
@apply p-4 flex flex-col gap-2 border-0 border-[--brand-gradient-border] border-solid;
}
.project-sidebar-section:not(:last-child) {
@apply border-b-[1px];
}
</style>

View File

@@ -1,3 +0,0 @@
import Index from './Index.vue'
export { Index }

View File

@@ -1,9 +1,6 @@
import { createRouter, createWebHistory } from 'vue-router'
import * as Pages from '@/pages'
import * as Project from '@/pages/project'
import * as User from '@/pages/user'
import * as Organization from '@/pages/organization'
import * as Collection from '@/pages/collection'
import * as Instance from '@/pages/instance'
import * as Library from '@/pages/library'
@@ -103,36 +100,6 @@ export default new createRouter({
},
],
},
{
path: '/user/:id',
name: 'User',
component: User.Index,
props: true,
meta: {
useContext: true,
breadcrumb: [{ name: '?User', link: '/user/{id}' }],
},
},
{
path: '/organization/:id',
name: 'Organization',
component: Organization.Index,
props: true,
meta: {
useContext: true,
breadcrumb: [{ name: '?Organization', link: '/organization/{id}' }],
},
},
{
path: '/collection/:id',
name: 'Collection',
component: Collection.Index,
props: true,
meta: {
useContext: true,
breadcrumb: [{ name: '?Collection', link: '/collection/{id}' }],
},
},
{
path: '/instance/:id',
name: 'Instance',

View File

@@ -1,11 +1,5 @@
import { defineStore } from 'pinia'
export const DEFAULT_FEATURE_FLAGS = {
project_background: false,
page_path: false,
project_card_background: false,
}
export const useTheming = defineStore('themeStore', {
state: () => ({
themeOptions: ['dark', 'light', 'oled', 'system'],
@@ -14,7 +8,7 @@ export const useTheming = defineStore('themeStore', {
toggleSidebar: false,
devMode: false,
featureFlags: DEFAULT_FEATURE_FLAGS,
featureFlags: {},
}),
actions: {
setThemeState(newTheme) {

View File

@@ -7,7 +7,18 @@ edition = "2021"
[dependencies]
theseus = { path = "../../packages/app-lib", features = ["cli"] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
thiserror = "1.0"
url = "2.2"
webbrowser = "0.8.13"
dunce = "1.0.3"
futures = "0.3"
uuid = { version = "1.1", features = ["serde", "v4"] }
tracing = "0.1.37"
tracing-subscriber = "0.3.18"
tracing-error = "0.2.0"

View File

@@ -28,9 +28,12 @@ tauri-plugin-single-instance = { version = "2.2.0" }
tokio = { version = "1", features = ["full"] }
thiserror = "1.0"
futures = "0.3"
daedalus = { path = "../../packages/daedalus" }
chrono = "0.4.26"
dirs = "5.0.1"
url = "2.2"
uuid = { version = "1.1", features = ["serde", "v4"] }
os_info = "3.7.0"
@@ -38,6 +41,9 @@ os_info = "3.7.0"
tracing = "0.1.37"
tracing-error = "0.2.0"
lazy_static = "1"
once_cell = "1"
dashmap = "6.0.1"
paste = "1.0.15"

View File

@@ -39,7 +39,7 @@
"fileAssociations": [
{
"ext": ["mrpack"],
"mimeType": "application/x-modrinth-modpack+zip"
"mimeType": "application/zip+mrpack"
}
]
},

View File

@@ -22,6 +22,7 @@ reqwest = { version = "0.12.5", default-features = false, features = [
"rustls-tls-native-roots",
] }
async_zip = { version = "0.0.17", features = ["full"] }
semver = "1.0"
chrono = { version = "0.4", features = ["serde"] }
bytes = "1.6.0"
rust-s3 = { version = "0.33.0", default-features = false, features = [
@@ -38,3 +39,4 @@ tracing-error = "0.2.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-futures = { version = "0.2.5", features = ["futures", "tokio"] }

View File

@@ -598,7 +598,7 @@ async fn fetch(
))
})?;
let file_name = value.split('/').next_back()
let file_name = value.split('/').last()
.ok_or_else(|| {
crate::ErrorKind::InvalidInput(format!(
"Unable reading filename for data key {key} at path {value}",

View File

@@ -44,10 +44,6 @@ export default defineConfig({
label: 'Contributing to Modrinth',
autogenerate: { directory: 'contributing' },
},
{
label: 'Guides',
autogenerate: { directory: 'guide' },
},
// Add the generated sidebar group to the sidebar.
...openAPISidebarGroups,
],

View File

@@ -1,7 +1,7 @@
openapi: '3.0.0'
info:
version: v2.7.0/366f528
version: v2.7.0/15cf3fc
title: Labrinth
termsOfService: https://modrinth.com/legal/terms
contact:
@@ -51,7 +51,35 @@ info:
Please note that certain scopes and requests cannot be completed with a personal access token or using OAuth.
For example, deleting a user account can only be done through Modrinth's frontend.
A detailed guide on OAuth has been published in [Modrinth's technical documentation](https://docs.modrinth.com/guide/oauth).
### OAuth2
Applications interacting with an authenticated API should create an OAuth2 application.
You can do this in [the developer settings](https://modrinth.com/settings/applications).
Make sure to save your application secret, as you will not be able to access it after you leave the page.
Once you have created a client, use the following URL to have a user authorize your client:
```
https://modrinth.com/auth/authorize?client_id=<CLIENT_ID>&redirect_uri=<CALLBACK_URL>&scope=<SCOPE_ONE>+<SCOPE_TWO>+<SCOPE_THREE>
```
> You can get a list of all scope names [here](https://github.com/modrinth/code/tree/main/apps/labrinth/src/models/v3/pats.rs).
Then, send a `POST` request to the following URL to get the token:
```
https://api.modrinth.com/_internal/oauth/token
```
> Note that you will need to provide your application's secret under the Authorization header.
In the body of your request, make sure to include the following:
- `code`: The code generated when authorizing your client
- `client_id`: Your client ID (found in developer settings)
- `redirect_uri`: A valid redirect URI provided in your application's settings
- `grant_type`: This will need to be `authorization_code`.
If your token request fails for any reason, you will need to get another code from the authorization process.
This route will be changed in the future to move the `_internal` part to `v3`.
### Personal access tokens
Personal access tokens (PATs) can be generated in from [the user settings](https://modrinth.com/settings/account).
@@ -1795,7 +1823,7 @@ components:
description: Number of projects on Modrinth
versions:
type: integer
description: Number of versions on Modrinth
description: Number of projects on Modrinth
files:
type: integer
description: Number of version files on Modrinth
@@ -2990,24 +3018,6 @@ paths:
$ref: '#/components/schemas/InvalidInputError'
'404':
description: The requested item(s) were not found or no authorization to access the requested item(s)
delete:
summary: Remove user's avatar
operationId: deleteUserIcon
tags:
- users
security:
- TokenAuth: ['USER_WRITE']
responses:
'204':
description: Expected response to a valid request
'400':
description: Request was invalid, see given error
content:
application/json:
schema:
$ref: '#/components/schemas/InvalidInputError'
'404':
description: The requested item(s) were not found or no authorization to access the requested item(s)
/user/{id|username}/projects:
parameters:
- $ref: '#/components/parameters/UserIdentifier'

View File

@@ -1,95 +0,0 @@
---
title: The hitchhiker's guide to OAuth
description: Guide for using Modrinth OAuth to interact with the API on users' behalf.
---
Modrinth allows developers to create applications which, once authorized by a Modrinth user, let the developer interact with the API on their behalf. The flow used to get an API token is based on the OAuth 2 protocol. It is recommended that most people use an existing OAuth library to handle the authentication. If you want to implement it from scratch, you will need to look into [RFC 6749]. If the only user of the application is yourself, a personal access token (PAT) may be a better fit.
If you're familiar with OAuth 2, these are the URLs you will need:
| Name | URL |
|--------------------|--------------------------------------------------|
| Authorization page | `https://modrinth.com/auth/authorize` |
| Token exchange | `https://api.modrinth.com/_internal/oauth/token` |
The flow will generally look like this:
1. User is redirected to Modrinth to authorize your application
2. User is redirected back to your site after authorizing, with an authorization code
3. Your backend exchanges this code for an access token
## Register your application
To start off, you need to [register an application] in Modrinth's systems. The settings chosen here can always be changed later. You need to select what permissions you need, called scopes. For security reasons you will want to select only the scopes you need. See the [principle of least privilege].
In addition to name and scopes, you will also need to add one or more redirect URIs. These are the URIs that the user can be redirected to after they authorize your application.
After you've registered your application, it is important that you take note of the client secret somewhere safe. If the client secret is to ever leak, it is important that you regenerate it to ensure the security of your authorized users. If your client secret or access tokens are found exposed in the wild, your application may be disabled without prior notice.
## Getting authorization
Once the user is ready to authorize your application, you need to construct a URL to redirect them to. The authorization URL for Modrinth is `https://api.modrinth.com/_internal/oauth/token`. Supply the following query parameters:
| Query parameter | Description |
|-----------------|-------------------------------------------------------------------------------------------|
| `response_type` | In Modrinth this always needs to be `code`, since only code grants are supported |
| `client_id` | The application identifier found in the settings |
| `scope` | The permissions you need access to |
| `state` | A mechanism to prevent certain attacks. Explained further below. Recommended but optional |
| `redirect_uri` | The URI the user is redirect to after finishing authorization |
You might have noticed the `state` parameter. [CSRF] (Cross-site request forgery), and [clickjacking] are security vulnerabilities that you're recommended to protect against. In OAuth2 this is usually done with the `state` parameter. When the user initiates a request to start authorization, you include a `state` which is unique to this request. This can, for example, be saved in localStorge or a cookie. When the redirect URI is called, you verify that the `state` parameter is the same. Using `state` is optional, but recommended.
The scope identifiers are currently best found in the backend source code located at [`apps/labrinth/src/models/v3/pats.rs`]. The scope parameter is an array of scope identifiers, seperated by a plus sign (`+`).
The redirect URI is the endpoint on your server that will receive the code which can eventually be used to act on the user's behalf. For security reasons the redirect URI used has to be allowlisted in your application settings. The redirect will contain the following query parameters:
| Query parameter | Description |
|-----------------|----------------------------------------------------|
| `code` | The code that can be exchanged for an access token |
| `client_id` | Your client id |
| `redirect_uri` | The redirect URI which was used |
| `grant_type` | Always `authorization_code` in Modrinth |
## Exchanging tokens
If you've followed the previous section on getting authorization, you should now have an authorization code. Before you can access the API, you need to exchange this code for an access token. This is done by sending a POST request to the exchange token endpoint, `https://api.modrinth.com/_internal/oauth/token`. This request has to be of type urlencoded form. Make sure the `Content-Type` header is set to `application/x-www-form-urlencoded`. To authenticate this request you need to place your client secret in the `Authorization` header.
In the body use these fields:
| Field | Description |
|----------------|--------------------------------------------------------------|
| `code` | The authorization code |
| `client_id` | Your client id, the same as in the authorization request |
| `redirect_uri` | The redirect URI which was redirected to after authorization |
| `grant_type` | Always `authorization_code` in Modrinth |
If the request succeeds, you should receive a JSON payload with these fields:
| Field | Description |
|----------------|------------------------------------------------------|
| `access_token` | The access token you can use to access the API |
| `token_type` | Currently only `Bearer` |
| `expires_in` | The amount of seconds until the access token expires |
To use this access token, you attach it to API requests in the `Authorization` header. To get basic information about the authorizer, you can use the [`/user` endpoint], which automatically gets the user from the header.
If you have any questions, you're welcome to ask in #api-development in the [Discord guild], or create a ticket on the [support portal].
[RFC 6749]: https://datatracker.ietf.org/doc/html/rfc6749
[register an application]: https://modrinth.com/settings/applications
[principle of least privilege]: https://en.wikipedia.org/wiki/Principle_of_least_privilege
[`apps/labrinth/src/models/v3/pats.rs`]: https://github.com/modrinth/code/blob/main/apps/labrinth/src/models/v3/pats.rs
[CSRF]: https://en.wikipedia.org/wiki/Cross-site_request_forgery
[Clickjacking]: https://en.wikipedia.org/wiki/Clickjacking
[`/user` endpoint]: https://docs.modrinth.com/api/operations/getuserfromauth/
[Discord guild]: https://discord.modrinth.com
[support portal]: https://support.modrinth.com/en/

View File

@@ -1,3 +1,2 @@
BASE_URL=https://api.modrinth.com/v2/
BROWSER_BASE_URL=https://api.modrinth.com/v2/
PYRO_BASE_URL=https://archon.modrinth.com/

View File

@@ -126,7 +126,6 @@ export default defineNuxtConfig({
homePageSearch?: any[];
homePageNotifs?: any[];
products?: any[];
errors?: number[];
} = {};
try {
@@ -158,14 +157,6 @@ export default defineNuxtConfig({
},
};
const caughtErrorCodes = new Set<number>();
function handleFetchError(err: any, defaultValue: any) {
console.error("Error generating state: ", err);
caughtErrorCodes.add(err.status);
return defaultValue;
}
const [
categories,
loaders,
@@ -177,25 +168,15 @@ export default defineNuxtConfig({
homePageNotifs,
products,
] = await Promise.all([
$fetch(`${API_URL}tag/category`, headers).catch((err) => handleFetchError(err, [])),
$fetch(`${API_URL}tag/loader`, headers).catch((err) => handleFetchError(err, [])),
$fetch(`${API_URL}tag/game_version`, headers).catch((err) => handleFetchError(err, [])),
$fetch(`${API_URL}tag/donation_platform`, headers).catch((err) =>
handleFetchError(err, []),
),
$fetch(`${API_URL}tag/report_type`, headers).catch((err) => handleFetchError(err, [])),
$fetch(`${API_URL}projects_random?count=60`, headers).catch((err) =>
handleFetchError(err, []),
),
$fetch(`${API_URL}search?limit=3&query=leave&index=relevance`, headers).catch((err) =>
handleFetchError(err, {}),
),
$fetch(`${API_URL}search?limit=3&query=&index=updated`, headers).catch((err) =>
handleFetchError(err, {}),
),
$fetch(`${API_URL.replace("/v2/", "/_internal/")}billing/products`, headers).catch((err) =>
handleFetchError(err, []),
),
$fetch(`${API_URL}tag/category`, headers),
$fetch(`${API_URL}tag/loader`, headers),
$fetch(`${API_URL}tag/game_version`, headers),
$fetch(`${API_URL}tag/donation_platform`, headers),
$fetch(`${API_URL}tag/report_type`, headers),
$fetch(`${API_URL}projects_random?count=60`, headers),
$fetch(`${API_URL}search?limit=3&query=leave&index=relevance`, headers),
$fetch(`${API_URL}search?limit=3&query=&index=updated`, headers),
$fetch(`${API_URL.replace("/v2/", "/_internal/")}billing/products`, headers),
]);
state.categories = categories;
@@ -207,7 +188,6 @@ export default defineNuxtConfig({
state.homePageSearch = homePageSearch;
state.homePageNotifs = homePageNotifs;
state.products = products;
state.errors = [...caughtErrorCodes];
await fs.writeFile("./src/generated/state.json", JSON.stringify(state));

View File

@@ -10,7 +10,7 @@
"postinstall": "nuxi prepare",
"lint": "eslint . && prettier --check .",
"fix": "eslint . --fix && prettier --write .",
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/layouts,src/middleware,src/modules,src/pages,src/plugins,src/utils}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" \"src/error.vue\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace"
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/layouts,src/middleware,src/modules,src/pages,src/plugins,src/utils}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace"
},
"devDependencies": {
"@formatjs/cli": "^6.2.12",

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="21" y1="6" x2="3" y2="6"></line>
<line x1="15" y1="12" x2="3" y2="12"></line>
<line x1="17" y1="18" x2="3" y2="18"></line>
</svg>

After

Width:  |  Height:  |  Size: 328 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="4" width="20" height="5" rx="2"></rect>
<path d="M4 9v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9"></path>
<path d="M10 13h4"></path>
</svg>

After

Width:  |  Height:  |  Size: 335 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 6v12"></path>
<path d="M17.196 9 6.804 15"></path>
<path d="m6.804 9 10.392 6"></path>
</svg>

After

Width:  |  Height:  |  Size: 295 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
<path d="M2 8c0-2.2.7-4.3 2-6"></path>
<path d="M22 8a10 10 0 0 0-2-6"></path>
</svg>

After

Width:  |  Height:  |  Size: 383 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
</svg>

After

Width:  |  Height:  |  Size: 300 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bot-icon lucide-bot"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-book-text"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/><path d="M8 7h6"/><path d="M8 11h8"/></svg>

Before

Width:  |  Height:  |  Size: 377 B

After

Width:  |  Height:  |  Size: 330 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
<polyline points="3.29 7 12 12 20.71 7"></polyline>
<line x1="12" y1="22" x2="12" y2="12"></line>
</svg>

After

Width:  |  Height:  |  Size: 433 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-calendar-clock"><path d="M21 7.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.5"/><path d="M16 2v4"/><path d="M8 2v4"/><path d="M3 10h5"/><path d="M17.5 17.5 16 16.25V14"/><path d="M22 16a6 6 0 1 1-12 0 6 6 0 0 1 12 0Z"/></svg>

After

Width:  |  Height:  |  Size: 436 B

View File

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M8 7V3M16 7V3M7 11H17M5 21H19C20.1046 21 21 20.1046 21 19V7C21 5.89543 20.1046 5 19 5H5C3.89543 5 3 5.89543 3 7V19C3 20.1046 3.89543 21 5 21Z"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 349 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 3v18h18"></path>
<path d="M18 17V9"></path>
<path d="M13 17V5"></path>
<path d="M8 17v-3"></path>
</svg>

After

Width:  |  Height:  |  Size: 307 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-cw-icon lucide-rotate-cw"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check-check"><path d="M18 6 7 17l-5-5"/><path d="m22 10-7.5 7.5L13 16"/></svg>

Before

Width:  |  Height:  |  Size: 324 B

After

Width:  |  Height:  |  Size: 280 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"></path>
<path d="m9 12 2 2 4-4"></path>
</svg>

After

Width:  |  Height:  |  Size: 315 B

View File

@@ -0,0 +1,4 @@
<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M20 6L9 17l-5-5" />
</svg>

After

Width:  |  Height:  |  Size: 197 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>

After

Width:  |  Height:  |  Size: 238 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>

After

Width:  |  Height:  |  Size: 233 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="15" y1="9" x2="9" y2="15"></line>
<line x1="9" y1="9" x2="15" y2="15"></line>
</svg>

After

Width:  |  Height:  |  Size: 323 B

View File

@@ -0,0 +1,4 @@
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>

After

Width:  |  Height:  |  Size: 291 B

View File

@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" /></svg>

After

Width:  |  Height:  |  Size: 299 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="8" cy="8" r="6"></circle>
<path d="M18.09 10.37A6 6 0 1 1 10.34 18"></path>
<path d="M7 6h1v4"></path>
<path d="m16.71 13.88.7.71-2.82 2.82"></path>
</svg>

After

Width:  |  Height:  |  Size: 358 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24" xml:space="preserve"><path d="M9 5v4m0 0H5m4 0L4 4m11 1v4m0 0h4m-4 0 5-5M9 19v-4m0 0H5m4 0-5 5m11-5 5 5m-5-5v4m0-4h4" style="fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"/></svg>

After

Width:  |  Height:  |  Size: 322 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<path d="M15 9.354a4 4 0 1 0 0 5.292"></path>
</svg>

After

Width:  |  Height:  |  Size: 280 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-ccw-icon lucide-rotate-ccw"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-crown"><path d="m2 4 3 12h14l3-12-6 7-4-7-4 7-6-7zm3 16h14"/></svg>

Before

Width:  |  Height:  |  Size: 324 B

After

Width:  |  Height:  |  Size: 270 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="2" x2="12" y2="22"></line>
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
</svg>

After

Width:  |  Height:  |  Size: 306 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="7" height="9"></rect>
<rect x="14" y="3" width="7" height="5"></rect>
<rect x="14" y="12" width="7" height="9"></rect>
<rect x="3" y="16" width="7" height="5"></rect>
</svg>

After

Width:  |  Height:  |  Size: 389 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>

After

Width:  |  Height:  |  Size: 244 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 9L12 16L5 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 219 B

View File

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M11 5H6C4.89543 5 4 5.89543 4 7V18C4 19.1046 4.89543 20 6 20H17C18.1046 20 19 19.1046 19 18V13M17.5858 3.58579C18.3668 2.80474 19.6332 2.80474 20.4142 3.58579C21.1953 4.36683 21.1953 5.63316 20.4142 6.41421L11.8284 15H9L9 12.1716L17.5858 3.58579Z"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 454 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>

After

Width:  |  Height:  |  Size: 274 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
</svg>

After

Width:  |  Height:  |  Size: 271 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>

After

Width:  |  Height:  |  Size: 348 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>

After

Width:  |  Height:  |  Size: 417 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>

After

Width:  |  Height:  |  Size: 275 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>

After

Width:  |  Height:  |  Size: 486 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>

After

Width:  |  Height:  |  Size: 320 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-filter"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon></svg>

After

Width:  |  Height:  |  Size: 290 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="2" viewBox="0 0 14 2">
<path d="M18,12H6" transform="translate(-5 -11)" fill="none" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2" />
</svg>

After

Width:  |  Height:  |  Size: 242 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" version="1.1" viewBox="0 0 24 24" xml:space="preserve">
<path fill="currentColor" class="st0"
d="m12 1c-6.3 0-11.3 5-11.3 11.3 0 5 3.2 9.2 7.7 10.7 0.6 0.1 0.8-0.2 0.8-0.5v-1.9c-3.2 0.6-3.8-1.6-3.8-1.6-0.5-1.3-1.3-1.7-1.3-1.7-1-0.7 0.1-0.7 0.1-0.7 1.1 0.1 1.7 1.2 1.7 1.2 1 1.7 2.7 1.2 3.3 0.9 0.1-0.7 0.4-1.2 0.7-1.5-2.5-0.2-5.1-1.2-5.1-5.5 0-1.2 0.4-2.2 1.2-3-0.1-0.3-0.5-1.4 0.1-3 0 0 1-0.3 3.1 1.2 0.9-0.3 1.8-0.5 2.8-0.5s1.9 0.1 2.8 0.4c2.2-1.5 3.1-1.2 3.1-1.2 0.6 1.6 0.2 2.7 0.1 3 0.7 0.8 1.2 1.8 1.2 3 0 4.4-2.6 5.3-5.2 5.6 0.4 0.3 0.8 1 0.8 2.1v3.1c0 0.3 0.2 0.7 0.8 0.5 4.5-1.5 7.7-5.7 7.7-10.7 0-6.2-5-11.2-11.3-11.2z" />
</svg>

After

Width:  |  Height:  |  Size: 717 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="2" y1="12" x2="22" y2="12"></line>
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
</svg>

After

Width:  |  Height:  |  Size: 390 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="7" height="7"></rect>
<rect x="14" y="3" width="7" height="7"></rect>
<rect x="14" y="14" width="7" height="7"></rect>
<rect x="3" y="14" width="7" height="7"></rect>
</svg>

After

Width:  |  Height:  |  Size: 389 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" class="feather feather-menu">
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="18" x2="21" y2="18" />
</svg>

After

Width:  |  Height:  |  Size: 352 B

View File

@@ -0,0 +1 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="9" x2="20" y2="9"></line><line x1="4" y1="15" x2="20" y2="15"></line><line x1="10" y1="3" x2="8" y2="21"></line><line x1="16" y1="3" x2="14" y2="21"></line></svg>

After

Width:  |  Height:  |  Size: 303 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20.42 4.58a5.4 5.4 0 0 0-7.65 0l-.77.78-.77-.78a5.4 5.4 0 0 0-7.65 0C1.46 6.7 1.33 10.28 4 13l8 8 8-8c2.67-2.72 2.54-6.3.42-8.42z"></path>
<path d="M12 5.36 8.87 8.5a2.13 2.13 0 0 0 0 3h0a2.13 2.13 0 0 0 3 0l2.26-2.21a3 3 0 0 1 4.22 0l2.4 2.4"></path>
<path d="m18 15-2-2"></path>
<path d="m15 18-2-2"></path>
</svg>

After

Width:  |  Height:  |  Size: 517 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
</svg>

After

Width:  |  Height:  |  Size: 305 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 3v5h5"></path>
<path d="M3.05 13A9 9 0 1 0 6 5.3L3 8"></path>
<path d="M12 7v5l4 2"></path>
</svg>

After

Width:  |  Height:  |  Size: 299 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="9" cy="9" r="2"></circle>
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"></path>
</svg>

After

Width:  |  Height:  |  Size: 356 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>

After

Width:  |  Height:  |  Size: 296 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>

After

Width:  |  Height:  |  Size: 386 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="7.5" cy="15.5" r="5.5"/><path d="m21 2-9.6 9.6"/><path d="m15.5 7.5 3 3L22 7l-3-3"/></svg>

After

Width:  |  Height:  |  Size: 261 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-languages"><path d="m5 8 6 6"/><path d="m4 14 6-6 2-3"/><path d="M2 5h12"/><path d="M7 2h1"/><path d="m22 22-5-10-5 10"/><path d="M14 18h6"/></svg>

After

Width:  |  Height:  |  Size: 349 B

Some files were not shown because too many files have changed in this diff Show More