Compare commits
10 Commits
main
...
app-users-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61c2ce2107 | ||
|
|
d37c634216 | ||
|
|
cd2fcc06fe | ||
|
|
a731f4758e | ||
|
|
0e0fce0e66 | ||
|
|
ea2f97ae23 | ||
|
|
2a0722d0d0 | ||
|
|
adf213f32a | ||
|
|
54f408dc6c | ||
|
|
c82c4ddc5b |
@ -2,6 +2,7 @@
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { RouterView, useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
UserIcon,
|
||||
ArrowBigUpDashIcon,
|
||||
CompassIcon,
|
||||
DownloadIcon,
|
||||
@ -233,6 +234,9 @@ async function fetchCredentials() {
|
||||
credentials.value = creds
|
||||
}
|
||||
|
||||
const profileMenu = ref()
|
||||
const isProfileMenuOpen = computed(() => profileMenu.value?.isOpen)
|
||||
|
||||
async function signIn() {
|
||||
await login().catch(handleError)
|
||||
await fetchCredentials()
|
||||
@ -410,26 +414,34 @@ function handleAuxClick(e) {
|
||||
<NavButton v-tooltip.right="'Settings'" :to="() => $refs.settingsModal.show()">
|
||||
<SettingsIcon />
|
||||
</NavButton>
|
||||
<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>
|
||||
<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>
|
||||
<NavButton v-else v-tooltip.right="'Sign in'" :to="() => signIn()">
|
||||
<LogInIcon />
|
||||
<template #label>Sign in</template>
|
||||
@ -698,6 +710,9 @@ function handleAuxClick(e) {
|
||||
|
||||
.app-grid-navbar {
|
||||
grid-area: nav;
|
||||
|
||||
// Fixes SVG scaling issues
|
||||
filter: brightness(1.00001);
|
||||
}
|
||||
|
||||
.app-grid-statusbar {
|
||||
@ -781,6 +796,7 @@ function handleAuxClick(e) {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.app-contents::before {
|
||||
|
||||
@ -181,24 +181,26 @@ const maxInstancesPerRow = ref(1)
|
||||
const maxProjectsPerRow = ref(1)
|
||||
|
||||
const calculateCardsPerRow = () => {
|
||||
// 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)
|
||||
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)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -207,13 +209,17 @@ const resizeObserver = ref(null)
|
||||
onMounted(() => {
|
||||
calculateCardsPerRow()
|
||||
resizeObserver.value = new ResizeObserver(calculateCardsPerRow)
|
||||
resizeObserver.value.observe(rowContainer.value)
|
||||
if (rowContainer.value) {
|
||||
resizeObserver.value.observe(rowContainer.value)
|
||||
}
|
||||
window.addEventListener('resize', calculateCardsPerRow)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', calculateCardsPerRow)
|
||||
resizeObserver.value.unobserve(rowContainer.value)
|
||||
if (rowContainer.value) {
|
||||
resizeObserver.value.unobserve(rowContainer.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@ -2,14 +2,14 @@
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
DownloadIcon,
|
||||
GameIcon,
|
||||
PlayIcon,
|
||||
SpinnerIcon,
|
||||
StopCircleIcon,
|
||||
GameIcon,
|
||||
TimerIcon,
|
||||
StopCircleIcon,
|
||||
PlayIcon,
|
||||
DownloadIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled } from '@modrinth/ui'
|
||||
import { ButtonStyled, Avatar, SmartClickable } 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,22 +134,26 @@ onUnmounted(() => unlisten())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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"
|
||||
>
|
||||
<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">
|
||||
<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">
|
||||
<div
|
||||
class="h-full flex items-center font-bold text-contrast leading-normal smart-clickable:underline-on-hover"
|
||||
>
|
||||
<span class="line-clamp-2">{{ instance.name }}</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center smart-clickable:allow-pointer-events">
|
||||
<ButtonStyled v-if="playing" color="red" circular @mousehover="checkProcess">
|
||||
<button v-tooltip="'Stop'" @click="(e) => stop(e, 'InstanceCard')">
|
||||
<StopCircleIcon />
|
||||
@ -176,13 +180,7 @@ onUnmounted(() => unlisten())
|
||||
<span class="text-sm"> Played {{ dayjs(instance.last_played).fromNow() }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else>
|
||||
<div
|
||||
class="button-base bg-bg-raised p-4 rounded-xl flex gap-3 group"
|
||||
@click="seeInstance"
|
||||
@mouseenter="checkProcess"
|
||||
>
|
||||
<div v-else class="p-4 rounded-xl flex gap-3 group" @mouseenter="checkProcess">
|
||||
<div class="relative flex items-center justify-center">
|
||||
<Avatar
|
||||
size="48px"
|
||||
@ -231,7 +229,9 @@ 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">
|
||||
<p
|
||||
class="m-0 text-md font-bold text-contrast leading-tight line-clamp-1 smart-clickable:underline-on-hover"
|
||||
>
|
||||
{{ 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>
|
||||
</div>
|
||||
</SmartClickable>
|
||||
</template>
|
||||
|
||||
@ -3,23 +3,18 @@ import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { formatCategory } from '@modrinth/utils'
|
||||
import { GameIcon, LeftArrowIcon } from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled } from '@modrinth/ui'
|
||||
|
||||
type Instance = {
|
||||
game_version: string
|
||||
loader: string
|
||||
path: string
|
||||
install_stage: string
|
||||
icon_path?: string
|
||||
name: string
|
||||
}
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
|
||||
defineProps<{
|
||||
instance: Instance
|
||||
instance?: GameInstance
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex justify-between items-center border-0 border-b border-solid border-divider pb-4">
|
||||
<div
|
||||
v-if="instance"
|
||||
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"
|
||||
@ -49,5 +44,3 @@ defineProps<{
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@ -1,16 +1,13 @@
|
||||
<script setup>
|
||||
import { Avatar, TagItem } from '@modrinth/ui'
|
||||
import { Avatar, SmartClickable, 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,
|
||||
@ -40,29 +37,15 @@ 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>
|
||||
<div
|
||||
<SmartClickable
|
||||
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="{
|
||||
@ -73,21 +56,13 @@ const toTransparent = computed(() => {
|
||||
'https://launcher-files.modrinth.com/assets/maze-bg.png'
|
||||
})`,
|
||||
}"
|
||||
>
|
||||
<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>
|
||||
<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">
|
||||
<div
|
||||
class="h-full flex items-center font-bold text-contrast leading-normal smart-clickable:underline-on-hover"
|
||||
>
|
||||
<span class="line-clamp-2">{{ project.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -115,7 +90,7 @@ const toTransparent = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SmartClickable>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
194
apps/app-frontend/src/components/ui/ProjectCardActions.vue
Normal file
@ -0,0 +1,194 @@
|
||||
<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>
|
||||
@ -1,157 +1,72 @@
|
||||
<template>
|
||||
<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}`,
|
||||
<NewProjectCard
|
||||
:project="project"
|
||||
:link="
|
||||
asLink(
|
||||
{
|
||||
path: `/project/${projectId}`,
|
||||
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
|
||||
"
|
||||
>
|
||||
<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 #actions>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
:disabled="installed || installing"
|
||||
class="shrink-0 no-wrap"
|
||||
@click.stop="install()"
|
||||
>
|
||||
<template v-if="project.client_side === 'optional' && project.server_side === 'optional'">
|
||||
Client or server
|
||||
<template v-if="!installed">
|
||||
<DownloadIcon />
|
||||
</template>
|
||||
<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>
|
||||
<CheckIcon v-else />
|
||||
{{ installing ? 'Installing' : installed ? 'Installed' : 'Install' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</NewProjectCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { TagsIcon, DownloadIcon, HeartIcon, PlusIcon, CheckIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, Avatar } from '@modrinth/ui'
|
||||
import { formatNumber, formatCategory } from '@modrinth/utils'
|
||||
<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'
|
||||
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 props = defineProps({
|
||||
backgroundImage: {
|
||||
type: String,
|
||||
default: null,
|
||||
const themeStore = useTheming()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
project: Project | SearchResult
|
||||
instance?: GameInstance
|
||||
installed?: boolean
|
||||
}>(),
|
||||
{
|
||||
instance: undefined,
|
||||
installed: false,
|
||||
},
|
||||
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'])
|
||||
|
||||
@ -160,16 +75,19 @@ const installing = ref(false)
|
||||
async function install() {
|
||||
installing.value = true
|
||||
await installVersion(
|
||||
props.project.project_id ?? props.project.id,
|
||||
projectId.value,
|
||||
null,
|
||||
props.instance ? props.instance.path : null,
|
||||
'SearchCard',
|
||||
() => {
|
||||
installing.value = false
|
||||
emit('install', props.project.project_id ?? props.project.id)
|
||||
emit('install', projectId.value)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const modpack = computed(() => props.project.project_type === 'modpack')
|
||||
const projectId = computed(() =>
|
||||
isSearchResult(props.project) ? props.project.project_id : props.project.id,
|
||||
)
|
||||
const creator = computed(() => (isSearchResult(props.project) ? props.project.author : undefined))
|
||||
</script>
|
||||
|
||||
@ -116,10 +116,6 @@ 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>
|
||||
|
||||
@ -1,19 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { Toggle } from '@modrinth/ui'
|
||||
import { ButtonStyled, Toggle } from '@modrinth/ui'
|
||||
import { useTheming } from '@/store/state'
|
||||
import { ref, watch } from 'vue'
|
||||
import { ref, type 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(['project_background', 'page_path'])
|
||||
const options: Ref<FeatureFlag[]> = ref(Object.keys(DEFAULT_FEATURE_FLAGS) as FeatureFlag[])
|
||||
|
||||
function getStoreValue(key: string) {
|
||||
function getStoreValue(key: FeatureFlag) {
|
||||
return themeStore.featureFlags[key] ?? false
|
||||
}
|
||||
|
||||
function setStoreValue(key: string, value: boolean) {
|
||||
function setStoreValue(key: FeatureFlag, value: boolean) {
|
||||
themeStore.featureFlags[key] = value
|
||||
settings.value.feature_flags[key] = value
|
||||
}
|
||||
@ -27,17 +30,39 @@ watch(
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<div v-for="option in options" :key="option" class="mt-4 flex items-center justify-between">
|
||||
<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>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast capitalize">
|
||||
{{ option }}
|
||||
<h2 class="m-0 text-base font-bold text-primary capitalize">
|
||||
{{ option.replace(new RegExp('_', 'g'), ' ') }}
|
||||
</h2>
|
||||
<p class="m-0 text-sm text-secondary">Default: {{ DEFAULT_FEATURE_FLAGS[option] }}</p>
|
||||
</div>
|
||||
|
||||
<Toggle
|
||||
id="advanced-rendering"
|
||||
:model-value="getStoreValue(option)"
|
||||
@update:model-value="() => setStoreValue(option, !themeStore.featureFlags[option])"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
41
apps/app-frontend/src/composables/instance-context.ts
Normal file
@ -0,0 +1,41 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
12
apps/app-frontend/src/helpers/types.d.ts
vendored
@ -32,6 +32,18 @@ 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'
|
||||
|
||||
@ -308,6 +308,18 @@
|
||||
"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"
|
||||
},
|
||||
|
||||
@ -2,7 +2,14 @@
|
||||
import { computed, nextTick, ref, shallowRef, watch } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import { SearchIcon, XIcon, ClipboardCopyIcon, GlobeIcon, ExternalIcon } from '@modrinth/assets'
|
||||
import type { Category, GameVersion, Platform, ProjectType, SortType, Tags } from '@modrinth/ui'
|
||||
import type {
|
||||
CategoryTag,
|
||||
GameVersionTag,
|
||||
PlatformTag,
|
||||
ProjectType,
|
||||
SortType,
|
||||
Tags,
|
||||
} from '@modrinth/ui'
|
||||
import {
|
||||
SearchFilterControl,
|
||||
SearchSidebarFilter,
|
||||
@ -19,14 +26,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()
|
||||
|
||||
@ -38,62 +45,45 @@ const projectTypes = computed(() => {
|
||||
})
|
||||
|
||||
const [categories, loaders, availableGameVersions] = await Promise.all([
|
||||
get_categories().catch(handleError).then(ref),
|
||||
get_loaders().catch(handleError).then(ref),
|
||||
get_game_versions().catch(handleError).then(ref),
|
||||
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)),
|
||||
])
|
||||
|
||||
const tags: Ref<Tags> = computed(() => ({
|
||||
gameVersions: availableGameVersions.value as GameVersion[],
|
||||
loaders: loaders.value as Platform[],
|
||||
categories: categories.value as Category[],
|
||||
gameVersions: availableGameVersions.value as GameVersionTag[],
|
||||
loaders: loaders.value as PlatformTag[],
|
||||
categories: categories.value as CategoryTag[],
|
||||
}))
|
||||
|
||||
type Instance = {
|
||||
game_version: string
|
||||
loader: string
|
||||
path: string
|
||||
install_stage: string
|
||||
icon_path?: string
|
||||
name: string
|
||||
}
|
||||
|
||||
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 newlyInstalled: Ref<string[]> = ref([])
|
||||
|
||||
const { instance, instanceContent } = await useInstanceContext()
|
||||
|
||||
const PERSISTENT_QUERY_PARAMS = ['i', 'ai']
|
||||
|
||||
await updateInstanceContext()
|
||||
await checkHideInstalledQuery()
|
||||
|
||||
watch(route, () => {
|
||||
updateInstanceContext()
|
||||
watch(instance, () => {
|
||||
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 = []
|
||||
}
|
||||
|
||||
async function checkHideInstalledQuery() {
|
||||
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 = null
|
||||
instanceHideInstalled.value = false
|
||||
}
|
||||
// if (instance.value && instance.value.path !== route.query.i && route.path.startsWith('/browse')) {
|
||||
// instance.value = undefined
|
||||
// instanceHideInstalled.value = false
|
||||
// }
|
||||
}
|
||||
|
||||
const instanceFilters = computed(() => {
|
||||
@ -119,10 +109,10 @@ const instanceFilters = computed(() => {
|
||||
})
|
||||
}
|
||||
|
||||
if (instanceHideInstalled.value && instanceProjects.value) {
|
||||
const installedMods = Object.values(instanceProjects.value)
|
||||
if (instanceHideInstalled.value && instanceContent.value) {
|
||||
const installedMods: string[] = Object.values(instanceContent.value)
|
||||
.filter((x) => x.metadata)
|
||||
.map((x) => x.metadata.project_id)
|
||||
.map((x) => x.metadata!.project_id)
|
||||
|
||||
installedMods.push(...newlyInstalled.value)
|
||||
|
||||
@ -173,23 +163,27 @@ breadcrumbs.setContext({ name: 'Discover content', link: route.path, query: rout
|
||||
|
||||
const loading = ref(true)
|
||||
|
||||
const projectType = ref(route.params.projectType)
|
||||
const projectType: Ref<ProjectType | undefined> = ref(
|
||||
typeof route.params.projectType === 'string'
|
||||
? (route.params.projectType as ProjectType)
|
||||
: undefined,
|
||||
)
|
||||
|
||||
watch(projectType, () => {
|
||||
loading.value = true
|
||||
})
|
||||
|
||||
type SearchResult = {
|
||||
project_id: string
|
||||
type ExtendedSearchResult = SearchResult & {
|
||||
installed?: boolean
|
||||
}
|
||||
|
||||
type SearchResults = {
|
||||
total_hits: number
|
||||
limit: number
|
||||
hits: SearchResult[]
|
||||
hits: ExtendedSearchResult[]
|
||||
}
|
||||
|
||||
const results: Ref<SearchResults | null> = shallowRef(null)
|
||||
const results: Ref<SearchResults | undefined> = shallowRef()
|
||||
const pageCount = computed(() =>
|
||||
results.value ? Math.ceil(results.value.total_hits / results.value.limit) : 1,
|
||||
)
|
||||
@ -200,7 +194,7 @@ watch(requestParams, () => {
|
||||
})
|
||||
|
||||
async function refreshSearch() {
|
||||
let rawResults = await get_search_results(requestParams.value)
|
||||
let rawResults = (await get_search_results(requestParams.value)) as { result: SearchResults }
|
||||
if (!rawResults) {
|
||||
rawResults = {
|
||||
result: {
|
||||
@ -211,13 +205,15 @@ async function refreshSearch() {
|
||||
}
|
||||
}
|
||||
if (instance.value) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
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,
|
||||
)),
|
||||
}))
|
||||
}
|
||||
results.value = rawResults.result
|
||||
|
||||
@ -271,9 +267,9 @@ watch(
|
||||
() => route.params.projectType,
|
||||
async (newType) => {
|
||||
// Check if the newType is not the same as the current value
|
||||
if (!newType || newType === projectType.value) return
|
||||
if (!newType || newType === projectType.value || typeof newType !== 'string') return
|
||||
|
||||
projectType.value = newType
|
||||
projectType.value = newType as ProjectType
|
||||
|
||||
currentSortType.value = { display: 'Relevance', name: 'relevance' }
|
||||
query.value = ''
|
||||
@ -287,7 +283,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
|
||||
@ -353,9 +349,10 @@ const messages = defineMessages({
|
||||
},
|
||||
})
|
||||
|
||||
const options = ref(null)
|
||||
const handleRightClick = (event, result) => {
|
||||
options.value.showMenu(event, result, [
|
||||
const options: Ref<InstanceType<typeof ContextMenu> | null> = ref(null)
|
||||
|
||||
const handleRightClick = (event: MouseEvent, result: ExtendedSearchResult) => {
|
||||
options.value?.showMenu(event, result, [
|
||||
{
|
||||
name: 'open_link',
|
||||
},
|
||||
@ -364,7 +361,7 @@ const handleRightClick = (event, result) => {
|
||||
},
|
||||
])
|
||||
}
|
||||
const handleOptionsClick = (args) => {
|
||||
const handleOptionsClick = (args: { item: ExtendedSearchResult; option: string }) => {
|
||||
switch (args.option) {
|
||||
case 'open_link':
|
||||
openUrl(`https://modrinth.com/${args.item.project_type}/${args.item.slug}`)
|
||||
@ -477,33 +474,26 @@ await refreshSearch()
|
||||
<section v-if="loading" class="offline">
|
||||
<LoadingIndicator />
|
||||
</section>
|
||||
<section v-else-if="offline && results.total_hits === 0" class="offline">
|
||||
<section v-else-if="offline && (!results || results.total_hits === 0)" class="offline">
|
||||
You are currently offline. Connect to the internet to browse Modrinth!
|
||||
</section>
|
||||
<section v-else class="project-list display-mode--list instance-results" role="list">
|
||||
<section
|
||||
v-else-if="results"
|
||||
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) => handleRightClick(event, result)"
|
||||
@contextmenu.prevent.stop="(event: MouseEvent) => handleRightClick(event, result)"
|
||||
/>
|
||||
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
|
||||
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
||||
|
||||
136
apps/app-frontend/src/pages/collection/Index.vue
Normal file
@ -0,0 +1,136 @@
|
||||
<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>
|
||||
3
apps/app-frontend/src/pages/collection/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import Index from './Index.vue'
|
||||
|
||||
export { Index }
|
||||
@ -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-6 w-6 text-secondary" />
|
||||
<GameIcon class="h-5 w-5 text-secondary" />
|
||||
{{ instance.loader }} {{ instance.game_version }}
|
||||
</div>
|
||||
<div class="flex items-center gap-2 font-semibold">
|
||||
<TimerIcon class="h-6 w-6 text-secondary" />
|
||||
<TimerIcon class="h-5 w-5 text-secondary" />
|
||||
<template v-if="timePlayed > 0">
|
||||
{{ timePlayedHumanized }}
|
||||
</template>
|
||||
|
||||
@ -69,7 +69,10 @@
|
||||
name: x.author.name,
|
||||
type: x.author.type,
|
||||
id: x.author.slug,
|
||||
link: `https://modrinth.com/${x.author.type}/${x.author.slug}`,
|
||||
link: {
|
||||
path: `/${x.author.type}/${x.author.slug}`,
|
||||
query: { i: props.instance.path },
|
||||
},
|
||||
linkProps: { target: '_blank' },
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ defineProps({
|
||||
</script>
|
||||
<template>
|
||||
<GridDisplay
|
||||
v-if="instances.length > 0"
|
||||
v-if="instances && instances.length > 0"
|
||||
label="Instances"
|
||||
:instances="instances.filter((i) => !i.linked_data)"
|
||||
/>
|
||||
|
||||
@ -10,7 +10,7 @@ defineProps({
|
||||
</script>
|
||||
<template>
|
||||
<GridDisplay
|
||||
v-if="instances.length > 0"
|
||||
v-if="instances && instances.length > 0"
|
||||
label="Instances"
|
||||
:instances="instances.filter((i) => i.linked_data)"
|
||||
/>
|
||||
|
||||
@ -9,5 +9,5 @@ defineProps({
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<GridDisplay v-if="instances.length > 0" label="Instances" :instances="instances" />
|
||||
<GridDisplay v-if="instances && instances.length > 0" label="Instances" :instances="instances" />
|
||||
</template>
|
||||
|
||||
176
apps/app-frontend/src/pages/organization/Index.vue
Normal file
@ -0,0 +1,176 @@
|
||||
<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>
|
||||
3
apps/app-frontend/src/pages/organization/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import Index from './Index.vue'
|
||||
|
||||
export { Index }
|
||||
@ -8,11 +8,10 @@
|
||||
/>
|
||||
<ProjectSidebarLinks link-target="_blank" :project="data" class="project-sidebar-section" />
|
||||
<ProjectSidebarCreators
|
||||
:organization="null"
|
||||
:organization="organization"
|
||||
:members="members"
|
||||
:org-link="(slug) => `https://modrinth.com/organization/${slug}`"
|
||||
:user-link="(username) => `https://modrinth.com/user/${username}`"
|
||||
link-target="_blank"
|
||||
:org-link="(org) => `/organization/${org.id}${instance ? '?i=' + instance.path : ''}`"
|
||||
:user-link="(user) => `/user/${user.id}${instance ? '?i=' + instance.path : ''}`"
|
||||
class="project-sidebar-section"
|
||||
/>
|
||||
<ProjectSidebarDetails
|
||||
@ -23,7 +22,7 @@
|
||||
/>
|
||||
</Teleport>
|
||||
<div class="flex flex-col gap-4 p-6">
|
||||
<InstanceIndicator v-if="instance" :instance="instance" />
|
||||
<InstanceIndicator :instance="instance" />
|
||||
<template v-if="data">
|
||||
<Teleport
|
||||
v-if="themeStore.featureFlags.project_background"
|
||||
@ -92,6 +91,11 @@
|
||||
label: 'Description',
|
||||
href: `/project/${$route.params.id}`,
|
||||
},
|
||||
{
|
||||
label: 'Gallery',
|
||||
href: `/project/${$route.params.id}/gallery`,
|
||||
shown: data.gallery.length > 0,
|
||||
},
|
||||
{
|
||||
label: 'Versions',
|
||||
href: {
|
||||
@ -100,11 +104,6 @@
|
||||
},
|
||||
subpages: ['version'],
|
||||
},
|
||||
{
|
||||
label: 'Gallery',
|
||||
href: `/project/${$route.params.id}/gallery`,
|
||||
shown: data.gallery.length > 0,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<RouterView
|
||||
@ -166,6 +165,7 @@ 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,6 +177,7 @@ 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)
|
||||
@ -202,6 +203,12 @@ 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) {
|
||||
@ -213,6 +220,7 @@ async function fetchProjectData() {
|
||||
installedVersion.value = installedFile.metadata.version_id
|
||||
}
|
||||
}
|
||||
|
||||
breadcrumbs.setName('Project', data.value.title)
|
||||
}
|
||||
|
||||
@ -437,6 +445,10 @@ const handleOptionsClick = (args) => {
|
||||
}
|
||||
|
||||
.project-sidebar-section {
|
||||
@apply p-4 flex flex-col gap-2 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid;
|
||||
@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>
|
||||
|
||||
156
apps/app-frontend/src/pages/user/Index.vue
Normal file
@ -0,0 +1,156 @@
|
||||
<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>
|
||||
3
apps/app-frontend/src/pages/user/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import Index from './Index.vue'
|
||||
|
||||
export { Index }
|
||||
@ -1,6 +1,9 @@
|
||||
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'
|
||||
|
||||
@ -100,6 +103,36 @@ 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',
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
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'],
|
||||
@ -8,7 +14,7 @@ export const useTheming = defineStore('themeStore', {
|
||||
toggleSidebar: false,
|
||||
|
||||
devMode: false,
|
||||
featureFlags: {},
|
||||
featureFlags: DEFAULT_FEATURE_FLAGS,
|
||||
}),
|
||||
actions: {
|
||||
setThemeState(newTheme) {
|
||||
|
||||
@ -520,7 +520,8 @@ input {
|
||||
opacity 0.5s ease-in-out,
|
||||
filter 0.2s ease-in-out,
|
||||
transform 0.05s ease-in-out,
|
||||
outline-width 0.2s ease-in-out;
|
||||
outline-width 0.2s ease-in-out,
|
||||
background-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.button-transparent {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<span
|
||||
:class="
|
||||
'badge flex items-center gap-1 font-semibold text-secondary ' + color + ' type--' + type
|
||||
'badges flex items-center gap-1 font-semibold text-secondary ' + color + ' type--' + type
|
||||
"
|
||||
>
|
||||
<template v-if="color"> <span class="circle" /> {{ capitalizeString(type) }}</template>
|
||||
|
||||
@ -30,6 +30,8 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
|
||||
newProjectCards: false,
|
||||
projectBackground: false,
|
||||
searchBackground: false,
|
||||
newProjectListUserPage: false,
|
||||
projectCardBackground: false,
|
||||
advancedDebugInfo: false,
|
||||
// advancedRendering: true,
|
||||
// externalLinksNewTab: true,
|
||||
|
||||
@ -703,8 +703,8 @@
|
||||
<ProjectSidebarCreators
|
||||
:organization="organization"
|
||||
:members="members"
|
||||
:org-link="(slug) => `/organization/${slug}`"
|
||||
:user-link="(username) => `/user/${username}`"
|
||||
:org-link="(org) => `/organization/${org.slug}`"
|
||||
:user-link="(user) => `/user/${user.username}`"
|
||||
class="card flex-card experimental-styles-within"
|
||||
/>
|
||||
<!-- TODO: Finish license modal and enable -->
|
||||
|
||||
@ -87,19 +87,19 @@
|
||||
<div
|
||||
class="flex items-center gap-2 border-0 border-r border-solid border-divider pr-4 font-semibold"
|
||||
>
|
||||
<UsersIcon class="h-6 w-6 text-secondary" />
|
||||
<UsersIcon class="h-5 w-5 text-secondary" />
|
||||
{{ formatCompactNumber(acceptedMembers?.length || 0) }}
|
||||
members
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-2 border-0 border-r border-solid border-divider pr-4 font-semibold"
|
||||
>
|
||||
<BoxIcon class="h-6 w-6 text-secondary" />
|
||||
<BoxIcon class="h-5 w-5 text-secondary" />
|
||||
{{ formatCompactNumber(projects?.length || 0) }}
|
||||
projects
|
||||
</div>
|
||||
<div class="flex items-center gap-2 font-semibold">
|
||||
<DownloadIcon class="h-6 w-6 text-secondary" />
|
||||
<DownloadIcon class="h-5 w-5 text-secondary" />
|
||||
{{ formatCompactNumber(sumDownloads) }}
|
||||
downloads
|
||||
</div>
|
||||
|
||||
@ -99,7 +99,7 @@ import { products } from "~/generated/state.json";
|
||||
|
||||
const title = "Subscribe to Modrinth Plus!";
|
||||
const description =
|
||||
"Subscribe to Modrinth Plus to go ad-free, support Modrinth's development, and get an exclusive profile badge! Half your subscription goes directly to Modrinth creators.";
|
||||
"Subscribe to Modrinth Plus to go ad-free, support Modrinth's development, and get an exclusive profile badges! Half your subscription goes directly to Modrinth creators.";
|
||||
|
||||
useSeoMeta({
|
||||
title,
|
||||
|
||||
@ -289,8 +289,14 @@
|
||||
v-if="flags.newProjectCards"
|
||||
:to="`/${projectType.id}/${result.slug ? result.slug : result.project_id}`"
|
||||
>
|
||||
<NewProjectCard :project="result" :categories="result.display_categories">
|
||||
<template v-if="false" #actions></template>
|
||||
<NewProjectCard
|
||||
v-if="flags.newProjectCards"
|
||||
:project="result"
|
||||
:categories="result.display_categories"
|
||||
:link="`/${projectType.id}/${result.slug ? result.slug : result.project_id}`"
|
||||
:creator-link="`/user/${result.author}`"
|
||||
>
|
||||
<template v-if="false" #actions> </template>
|
||||
</NewProjectCard>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
@ -55,51 +55,7 @@
|
||||
</NewModal>
|
||||
<div class="new-page sidebar" :class="{ 'alt-layout': cosmetics.leftContentLayout }">
|
||||
<div class="normal-page__header py-4">
|
||||
<ContentPageHeader>
|
||||
<template #icon>
|
||||
<Avatar :src="user.avatar_url" :alt="user.username" size="96px" circle />
|
||||
</template>
|
||||
<template #title>
|
||||
{{ user.username }}
|
||||
</template>
|
||||
<template #summary>
|
||||
{{
|
||||
user.bio
|
||||
? user.bio
|
||||
: projects.length === 0
|
||||
? "A Modrinth user."
|
||||
: "A Modrinth creator."
|
||||
}}
|
||||
</template>
|
||||
<template #stats>
|
||||
<div
|
||||
class="flex items-center gap-2 border-0 border-r border-solid border-divider pr-4 font-semibold"
|
||||
>
|
||||
<BoxIcon class="h-6 w-6 text-secondary" />
|
||||
{{ formatCompactNumber(projects?.length || 0) }}
|
||||
projects
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-2 border-0 border-r border-solid border-divider pr-4 font-semibold"
|
||||
>
|
||||
<DownloadIcon class="h-6 w-6 text-secondary" />
|
||||
{{ formatCompactNumber(sumDownloads) }}
|
||||
downloads
|
||||
</div>
|
||||
<div
|
||||
v-tooltip="
|
||||
formatMessage(commonMessages.dateAtTimeTooltip, {
|
||||
date: new Date(user.created),
|
||||
time: new Date(user.created),
|
||||
})
|
||||
"
|
||||
class="flex items-center gap-2 font-semibold"
|
||||
>
|
||||
<CalendarIcon class="h-6 w-6 text-secondary" />
|
||||
Joined
|
||||
{{ formatRelativeTime(user.created) }}
|
||||
</div>
|
||||
</template>
|
||||
<UserHeader :user="user" :project-count="projects.length" :download-count="sumDownloads">
|
||||
<template #actions>
|
||||
<ButtonStyled size="large">
|
||||
<NuxtLink v-if="auth.user && auth.user.id === user.id" to="/settings/profile">
|
||||
@ -162,168 +118,181 @@
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</ContentPageHeader>
|
||||
</UserHeader>
|
||||
</div>
|
||||
<div class="normal-page__content">
|
||||
<div v-if="navLinks.length >= 2" class="mb-4 max-w-full overflow-x-auto">
|
||||
<NavTabs :links="navLinks" />
|
||||
</div>
|
||||
<div v-if="projects.length > 0">
|
||||
<div
|
||||
v-if="route.params.projectType !== 'collections'"
|
||||
:class="'project-list display-mode--' + cosmetics.searchDisplayMode.user"
|
||||
>
|
||||
<ProjectCard
|
||||
v-for="project in (route.params.projectType !== undefined
|
||||
? projects.filter(
|
||||
(x) =>
|
||||
x.project_type ===
|
||||
route.params.projectType.substr(0, route.params.projectType.length - 1),
|
||||
)
|
||||
: projects
|
||||
)
|
||||
.slice()
|
||||
.sort((a, b) => b.downloads - a.downloads)"
|
||||
:id="project.slug || project.id"
|
||||
:key="project.id"
|
||||
:name="project.title"
|
||||
:display="cosmetics.searchDisplayMode.user"
|
||||
:featured-image="project.gallery.find((element) => element.featured)?.url"
|
||||
:description="project.description"
|
||||
:created-at="project.published"
|
||||
:updated-at="project.updated"
|
||||
:downloads="project.downloads.toString()"
|
||||
:follows="project.followers.toString()"
|
||||
:icon-url="project.icon_url"
|
||||
:categories="project.categories"
|
||||
:client-side="project.client_side"
|
||||
:server-side="project.server_side"
|
||||
:status="
|
||||
auth.user && (auth.user.id === user.id || tags.staffRoles.includes(auth.user.role))
|
||||
? project.status
|
||||
: null
|
||||
"
|
||||
:type="project.project_type"
|
||||
:color="project.color"
|
||||
/>
|
||||
<ProjectsList
|
||||
v-if="flags.newProjectListUserPage"
|
||||
:projects="projects.filter((x) => x.status === 'approved' || x.status === 'archived')"
|
||||
:project-link="(project) => `/project/${project.id}`"
|
||||
:experimental-colors="flags.projectCardBackground"
|
||||
>
|
||||
<template #project-actions>
|
||||
<ButtonStyled color="brand">
|
||||
<button><DownloadIcon /> Install</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<button v-tooltip="'Follow'">
|
||||
<HeartIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<button v-tooltip="'Save'">
|
||||
<BookmarkIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<button>
|
||||
<MoreVerticalIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</ProjectsList>
|
||||
<template v-else>
|
||||
<div v-if="navLinks.length >= 2" class="mb-4 max-w-full overflow-x-auto">
|
||||
<NavTabs :links="navLinks" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="route.params.projectType !== 'collections'" class="error">
|
||||
<UpToDate class="icon" />
|
||||
<br />
|
||||
<span v-if="auth.user && auth.user.id === user.id" class="preserve-lines text">
|
||||
<IntlFormatted :message-id="messages.profileNoProjectsAuthLabel">
|
||||
<template #create-link="{ children }">
|
||||
<a class="link" @click.prevent="$refs.modal_creation.show()">
|
||||
<component :is="() => children" />
|
||||
</a>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</span>
|
||||
<span v-else class="text">{{ formatMessage(messages.profileNoProjectsLabel) }}</span>
|
||||
</div>
|
||||
<div v-if="['collections'].includes(route.params.projectType)" class="collections-grid">
|
||||
<nuxt-link
|
||||
v-for="collection in collections.sort(
|
||||
(a, b) => new Date(b.created) - new Date(a.created),
|
||||
)"
|
||||
:key="collection.id"
|
||||
:to="`/collection/${collection.id}`"
|
||||
class="card collection-item"
|
||||
>
|
||||
<div class="collection">
|
||||
<Avatar :src="collection.icon_url" class="icon" />
|
||||
<div class="details">
|
||||
<h2 class="title">{{ collection.name }}</h2>
|
||||
<div class="stats">
|
||||
<LibraryIcon aria-hidden="true" />
|
||||
Collection
|
||||
<div v-if="projects.length > 0">
|
||||
<div
|
||||
v-if="route.params.projectType !== 'collections'"
|
||||
:class="'project-list display-mode--' + cosmetics.searchDisplayMode.user"
|
||||
>
|
||||
<ProjectCard
|
||||
v-for="project in (route.params.projectType !== undefined
|
||||
? projects.filter(
|
||||
(x) =>
|
||||
x.project_type ===
|
||||
route.params.projectType.substr(0, route.params.projectType.length - 1),
|
||||
)
|
||||
: projects
|
||||
)
|
||||
.slice()
|
||||
.sort((a, b) => b.downloads - a.downloads)"
|
||||
:id="project.slug || project.id"
|
||||
:key="project.id"
|
||||
:name="project.title"
|
||||
:display="cosmetics.searchDisplayMode.user"
|
||||
:featured-image="project.gallery.find((element) => element.featured)?.url"
|
||||
:description="project.description"
|
||||
:created-at="project.published"
|
||||
:updated-at="project.updated"
|
||||
:downloads="project.downloads.toString()"
|
||||
:follows="project.followers.toString()"
|
||||
:icon-url="project.icon_url"
|
||||
:categories="project.categories"
|
||||
:client-side="project.client_side"
|
||||
:server-side="project.server_side"
|
||||
:status="
|
||||
auth.user &&
|
||||
(auth.user.id === user.id || tags.staffRoles.includes(auth.user.role))
|
||||
? project.status
|
||||
: null
|
||||
"
|
||||
:type="project.project_type"
|
||||
:color="project.color"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="route.params.projectType !== 'collections'" class="error">
|
||||
<UpToDate class="icon" /><br />
|
||||
<span v-if="auth.user && auth.user.id === user.id" class="preserve-lines text">
|
||||
<IntlFormatted :message-id="messages.profileNoProjectsAuthLabel">
|
||||
<template #create-link="{ children }">
|
||||
<a class="link" @click.prevent="$refs.modal_creation.show()">
|
||||
<component :is="() => children" />
|
||||
</a>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</span>
|
||||
<span v-else class="text">{{ formatMessage(messages.profileNoProjectsLabel) }}</span>
|
||||
</div>
|
||||
<div v-if="['collections'].includes(route.params.projectType)" class="collections-grid">
|
||||
<nuxt-link
|
||||
v-for="collection in collections.sort(
|
||||
(a, b) => new Date(b.created) - new Date(a.created),
|
||||
)"
|
||||
:key="collection.id"
|
||||
:to="`/collection/${collection.id}`"
|
||||
class="card collection-item"
|
||||
>
|
||||
<div class="collection">
|
||||
<Avatar :src="collection.icon_url" class="icon" />
|
||||
<div class="details">
|
||||
<h2 class="title">{{ collection.name }}</h2>
|
||||
<div class="stats">
|
||||
<LibraryIcon aria-hidden="true" />
|
||||
Collection
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="description">
|
||||
{{ collection.description }}
|
||||
</div>
|
||||
<div class="stat-bar">
|
||||
<div class="stats">
|
||||
<BoxIcon />
|
||||
{{
|
||||
`${$formatNumber(collection.projects?.length || 0, false)} project${(collection.projects?.length || 0) !== 1 ? "s" : ""}`
|
||||
}}
|
||||
<div class="description">
|
||||
{{ collection.description }}
|
||||
</div>
|
||||
<div class="stats">
|
||||
<template v-if="collection.status === 'listed'">
|
||||
<GlobeIcon />
|
||||
<span> Public </span>
|
||||
</template>
|
||||
<template v-else-if="collection.status === 'unlisted'">
|
||||
<LinkIcon />
|
||||
<span> Unlisted </span>
|
||||
</template>
|
||||
<template v-else-if="collection.status === 'private'">
|
||||
<LockIcon />
|
||||
<span> Private </span>
|
||||
</template>
|
||||
<template v-else-if="collection.status === 'rejected'">
|
||||
<XIcon />
|
||||
<span> Rejected </span>
|
||||
</template>
|
||||
<div class="stat-bar">
|
||||
<div class="stats">
|
||||
<BoxIcon />
|
||||
{{
|
||||
`${$formatNumber(collection.projects?.length || 0, false)} project${(collection.projects?.length || 0) !== 1 ? "s" : ""}`
|
||||
}}
|
||||
</div>
|
||||
<div class="stats">
|
||||
<template v-if="collection.status === 'listed'">
|
||||
<WorldIcon />
|
||||
<span> Public </span>
|
||||
</template>
|
||||
<template v-else-if="collection.status === 'unlisted'">
|
||||
<LinkIcon />
|
||||
<span> Unlisted </span>
|
||||
</template>
|
||||
<template v-else-if="collection.status === 'private'">
|
||||
<LockIcon />
|
||||
<span> Private </span>
|
||||
</template>
|
||||
<template v-else-if="collection.status === 'rejected'">
|
||||
<XIcon />
|
||||
<span> Rejected </span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
<div
|
||||
v-if="route.params.projectType === 'collections' && collections.length === 0"
|
||||
class="error"
|
||||
>
|
||||
<UpToDate class="icon" />
|
||||
<br />
|
||||
<span v-if="auth.user && auth.user.id === user.id" class="preserve-lines text">
|
||||
<IntlFormatted :message-id="messages.profileNoCollectionsAuthLabel">
|
||||
<template #create-link="{ children }">
|
||||
<a
|
||||
class="link"
|
||||
@click.prevent="(event) => $refs.modal_collection_creation.show(event)"
|
||||
>
|
||||
<component :is="() => children" />
|
||||
</a>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</span>
|
||||
<span v-else class="text">{{ formatMessage(messages.profileNoCollectionsLabel) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="normal-page__sidebar">
|
||||
<div v-if="organizations.length > 0" class="card flex-card">
|
||||
<h2 class="text-lg text-contrast">{{ formatMessage(messages.profileOrganizations) }}</h2>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<nuxt-link
|
||||
v-for="org in organizations"
|
||||
:key="org.id"
|
||||
v-tooltip="org.name"
|
||||
class="organization"
|
||||
:to="`/organization/${org.slug}`"
|
||||
>
|
||||
<Avatar :src="org.icon_url" :alt="'Icon for ' + org.name" size="3rem" />
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="badges.length > 0" class="card flex-card">
|
||||
<h2 class="text-lg text-contrast">{{ formatMessage(messages.profileBadges) }}</h2>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div v-for="badge in badges" :key="badge">
|
||||
<StaffBadge v-if="badge === 'staff'" class="h-14 w-14" />
|
||||
<ModBadge v-else-if="badge === 'mod'" class="h-14 w-14" />
|
||||
<nuxt-link v-else-if="badge === 'plus'" to="/plus">
|
||||
<PlusBadge class="h-14 w-14" />
|
||||
</nuxt-link>
|
||||
<TenMClubBadge v-else-if="badge === '10m-club'" class="h-14 w-14" />
|
||||
<EarlyAdopterBadge v-else-if="badge === 'early-adopter'" class="h-14 w-14" />
|
||||
<AlphaTesterBadge v-else-if="badge === 'alpha-tester'" class="h-14 w-14" />
|
||||
<BetaTesterBadge v-else-if="badge === 'beta-tester'" class="h-14 w-14" />
|
||||
</div>
|
||||
<div
|
||||
v-if="route.params.projectType === 'collections' && collections.length === 0"
|
||||
class="error"
|
||||
>
|
||||
<UpToDate class="icon" /><br />
|
||||
<span v-if="auth.user && auth.user.id === user.id" class="preserve-lines text">
|
||||
<IntlFormatted :message-id="messages.profileNoCollectionsAuthLabel">
|
||||
<template #create-link="{ children }">
|
||||
<a
|
||||
class="link"
|
||||
@click.prevent="(event) => $refs.modal_collection_creation.show(event)"
|
||||
>
|
||||
<component :is="() => children" />
|
||||
</a>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</span>
|
||||
<span v-else class="text">{{ formatMessage(messages.profileNoCollectionsLabel) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="normal-page__sidebar">
|
||||
<UserSidebarOrganizations
|
||||
:organizations="organizations"
|
||||
:link="(org) => '/organization/' + org.slug"
|
||||
class="card flex-card experimental-styles-within"
|
||||
/>
|
||||
<UserSidebarBadges
|
||||
:user="user"
|
||||
:download-count="sumDownloads"
|
||||
class="card flex-card experimental-styles-within"
|
||||
/>
|
||||
<UserSidebarCollections
|
||||
:collections="collections.filter((x) => x.status === 'listed')"
|
||||
:link="(collection) => `/collection/${collection.id}`"
|
||||
class="card flex-card experimental-styles-within"
|
||||
/>
|
||||
<AdPlaceholder
|
||||
v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus"
|
||||
/>
|
||||
@ -338,10 +307,11 @@ import {
|
||||
LinkIcon,
|
||||
LockIcon,
|
||||
XIcon,
|
||||
CalendarIcon,
|
||||
DownloadIcon,
|
||||
ClipboardCopyIcon,
|
||||
MoreVerticalIcon,
|
||||
DownloadIcon,
|
||||
BookmarkIcon,
|
||||
HeartIcon,
|
||||
CurrencyIcon,
|
||||
InfoIcon,
|
||||
CheckIcon,
|
||||
@ -352,23 +322,19 @@ import {
|
||||
import {
|
||||
OverflowMenu,
|
||||
ButtonStyled,
|
||||
ContentPageHeader,
|
||||
commonMessages,
|
||||
NewModal,
|
||||
UserHeader,
|
||||
commonMessages,
|
||||
UserSidebarBadges,
|
||||
UserSidebarOrganizations,
|
||||
ProjectsList,
|
||||
UserSidebarCollections,
|
||||
} from "@modrinth/ui";
|
||||
import { isStaff } from "~/helpers/users.js";
|
||||
import NavTabs from "~/components/ui/NavTabs.vue";
|
||||
import ProjectCard from "~/components/ui/ProjectCard.vue";
|
||||
import { reportUser } from "~/utils/report-helpers.ts";
|
||||
|
||||
import StaffBadge from "~/assets/images/badges/staff.svg?component";
|
||||
import ModBadge from "~/assets/images/badges/mod.svg?component";
|
||||
import PlusBadge from "~/assets/images/badges/plus.svg?component";
|
||||
import TenMClubBadge from "~/assets/images/badges/10m-club.svg?component";
|
||||
import EarlyAdopterBadge from "~/assets/images/badges/early-adopter.svg?component";
|
||||
import AlphaTesterBadge from "~/assets/images/badges/alpha-tester.svg?component";
|
||||
import BetaTesterBadge from "~/assets/images/badges/beta-tester.svg?component";
|
||||
|
||||
import UpToDate from "~/assets/images/illustrations/up_to_date.svg?component";
|
||||
import ModalCreation from "~/components/ui/ModalCreation.vue";
|
||||
import Avatar from "~/components/ui/Avatar.vue";
|
||||
@ -385,10 +351,6 @@ const flags = useFeatureFlags();
|
||||
const vintl = useVIntl();
|
||||
const { formatMessage } = vintl;
|
||||
|
||||
const formatCompactNumber = useCompactNumber();
|
||||
|
||||
const formatRelativeTime = useRelativeTime();
|
||||
|
||||
const messages = defineMessages({
|
||||
profileProjectsStats: {
|
||||
id: "profile.stats.projects",
|
||||
@ -564,54 +526,6 @@ const sumDownloads = computed(() => {
|
||||
return sum;
|
||||
});
|
||||
|
||||
const joinDate = computed(() => new Date(user.value.created));
|
||||
const MODRINTH_BETA_END_DATE = new Date("2022-02-27T08:00:00.000Z");
|
||||
const MODRINTH_ALPHA_END_DATE = new Date("2020-11-30T08:00:00.000Z");
|
||||
|
||||
const badges = computed(() => {
|
||||
const badges = [];
|
||||
|
||||
if (user.value.role === "admin") {
|
||||
badges.push("staff");
|
||||
}
|
||||
|
||||
if (user.value.role === "moderator") {
|
||||
badges.push("mod");
|
||||
}
|
||||
|
||||
if (isPermission(user.value.badges, 1 << 0)) {
|
||||
badges.push("plus");
|
||||
}
|
||||
|
||||
if (sumDownloads.value > 10000000) {
|
||||
badges.push("10m-club");
|
||||
}
|
||||
|
||||
if (
|
||||
isPermission(user.value.badges, 1 << 1) ||
|
||||
isPermission(user.value.badges, 1 << 2) ||
|
||||
isPermission(user.value.badges, 1 << 3)
|
||||
) {
|
||||
badges.push("early-adopter");
|
||||
}
|
||||
|
||||
if (isPermission(user.value.badges, 1 << 4) || joinDate.value < MODRINTH_ALPHA_END_DATE) {
|
||||
badges.push("alpha-tester");
|
||||
} else if (isPermission(user.value.badges, 1 << 4) || joinDate.value < MODRINTH_BETA_END_DATE) {
|
||||
badges.push("beta-tester");
|
||||
}
|
||||
|
||||
if (isPermission(user.value.badges, 1 << 5)) {
|
||||
badges.push("contributor");
|
||||
}
|
||||
|
||||
if (isPermission(user.value.badges, 1 << 6)) {
|
||||
badges.push("translator");
|
||||
}
|
||||
|
||||
return badges;
|
||||
});
|
||||
|
||||
async function copyId() {
|
||||
await navigator.clipboard.writeText(user.value.id);
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
export const isPermission = (perms?: number, bitflag?: number) => {
|
||||
if (!perms || !bitflag) return false;
|
||||
return (perms & bitflag) === bitflag;
|
||||
};
|
||||
import { isPermission as _isPermission } from "@modrinth/utils";
|
||||
|
||||
export const isPermission = _isPermission;
|
||||
|
||||
@ -44,6 +44,7 @@ pub struct Settings {
|
||||
pub enum FeatureFlag {
|
||||
PagePath,
|
||||
ProjectBackground,
|
||||
ProjectCardBackground,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
8
packages/assets/badges/contributor.svg
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
14
packages/assets/badges/translator.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<svg width="59" height="59" viewBox="0 0 59 59" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M49.5999 34.8C53.7719 30.712 57.9999 25.812 57.9999 19.4C57.9999 15.3157 56.3774 11.3986 53.4893 8.51056C50.6013 5.6225 46.6842 4 42.5999 4C37.6719 4 34.1999 5.4 29.9999 9.6C25.7999 5.4 22.3279 4 17.3999 4C13.3155 4 9.39849 5.6225 6.51043 8.51056C3.62237 11.3986 1.99988 15.3157 1.99988 19.4C1.99988 25.84 6.19988 30.74 10.3999 34.8L29.9999 54.4L49.5999 34.8Z" fill="#584D43" stroke="#FBDDB0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.62402 17.4929V16.6364H13.3122V17.4929H11.9891V21H10.9493V17.4929H9.62402ZM13.8273 21V16.6364H15.6299C15.9566 16.6364 16.2385 16.6953 16.4758 16.8132C16.7144 16.9297 16.8983 17.0973 17.0276 17.3161C17.1569 17.5334 17.2215 17.7912 17.2215 18.0895C17.2215 18.392 17.1555 18.6491 17.0233 18.8608C16.8912 19.071 16.7037 19.2315 16.4608 19.3423C16.218 19.4517 15.9303 19.5064 15.5979 19.5064H14.458V18.6754H15.4019C15.561 18.6754 15.6938 18.6548 15.8003 18.6136C15.9083 18.571 15.99 18.5071 16.0454 18.4219C16.1008 18.3352 16.1285 18.2244 16.1285 18.0895C16.1285 17.9545 16.1008 17.843 16.0454 17.755C15.99 17.6655 15.9083 17.5987 15.8003 17.5547C15.6924 17.5092 15.5596 17.4865 15.4019 17.4865H14.882V21H13.8273ZM16.284 19.0057L17.3707 21H16.2201L15.1547 19.0057H16.284ZM18.7439 21H17.6104L19.0827 16.6364H20.4868L21.9591 21H20.8256L19.8007 17.7358H19.7666L18.7439 21ZM18.5926 19.2827H20.9619V20.0838H18.5926V19.2827ZM26.1427 16.6364V21H25.2478L23.5113 18.4815H23.4836V21H22.4289V16.6364H23.3366L25.0539 19.1506H25.0901V16.6364H26.1427ZM29.1645 17.9446C29.1503 17.7884 29.0871 17.6669 28.9749 17.5803C28.8641 17.4922 28.7057 17.4482 28.4997 17.4482C28.3634 17.4482 28.2497 17.4659 28.1588 17.5014C28.0679 17.5369 27.9997 17.5859 27.9543 17.6484C27.9088 17.7095 27.8854 17.7798 27.884 17.8594C27.8811 17.9247 27.8939 17.9822 27.9223 18.032C27.9521 18.0817 27.9948 18.1257 28.0502 18.1641C28.107 18.201 28.1752 18.2337 28.2547 18.2621C28.3343 18.2905 28.4237 18.3153 28.5232 18.3366L28.8982 18.4219C29.1141 18.4688 29.3044 18.5312 29.4692 18.6094C29.6354 18.6875 29.7746 18.7805 29.8868 18.8885C30.0004 18.9964 30.0864 19.1207 30.1446 19.2614C30.2029 19.402 30.2327 19.5597 30.2341 19.7344C30.2327 20.0099 30.1631 20.2464 30.0253 20.4439C29.8875 20.6413 29.6894 20.7926 29.4308 20.8977C29.1737 21.0028 28.8634 21.0554 28.4997 21.0554C28.1347 21.0554 27.8165 21.0007 27.5452 20.8913C27.2739 20.782 27.0629 20.6158 26.9124 20.3928C26.7618 20.1697 26.6844 19.8878 26.6801 19.5469H27.6901C27.6986 19.6875 27.7362 19.8047 27.803 19.8984C27.8698 19.9922 27.9614 20.0632 28.0779 20.1115C28.1958 20.1598 28.3321 20.1839 28.4869 20.1839C28.629 20.1839 28.7497 20.1648 28.8492 20.1264C28.95 20.0881 29.0274 20.0348 29.0814 19.9666C29.1354 19.8984 29.1631 19.8203 29.1645 19.7322C29.1631 19.6499 29.1375 19.5795 29.0878 19.5213C29.0381 19.4616 28.9614 19.4105 28.8577 19.3679C28.7554 19.3239 28.6247 19.2834 28.4656 19.2464L28.0097 19.1399C27.6318 19.0533 27.3343 18.9134 27.1169 18.7202C26.8996 18.5256 26.7916 18.2628 26.7931 17.9318C26.7916 17.6619 26.8641 17.4254 27.0104 17.2223C27.1567 17.0192 27.3591 16.8608 27.6176 16.7472C27.8762 16.6335 28.1709 16.5767 28.5019 16.5767C28.8399 16.5767 29.1333 16.6342 29.3818 16.7493C29.6318 16.8629 29.8257 17.0227 29.9635 17.2287C30.1013 17.4347 30.1716 17.6733 30.1744 17.9446H29.1645ZM30.7668 21V16.6364H31.8215V20.1435H33.6368V21H30.7668ZM35.1853 21H34.0518L35.5241 16.6364H36.9282L38.4005 21H37.267L36.2421 17.7358H36.208L35.1853 21ZM35.034 19.2827H37.4033V20.0838H35.034V19.2827ZM38.1826 17.4929V16.6364H41.8708V17.4929H40.5477V21H39.5079V17.4929H38.1826ZM46.3053 18.8182C46.3053 19.2983 46.213 19.7053 46.0283 20.0391C45.8437 20.3729 45.5937 20.6264 45.2783 20.7997C44.9644 20.973 44.6121 21.0597 44.2215 21.0597C43.8295 21.0597 43.4765 20.9723 43.1626 20.7976C42.8486 20.6229 42.5993 20.3693 42.4147 20.0369C42.2314 19.7031 42.1398 19.2969 42.1398 18.8182C42.1398 18.3381 42.2314 17.9311 42.4147 17.5973C42.5993 17.2635 42.8486 17.0099 43.1626 16.8366C43.4765 16.6634 43.8295 16.5767 44.2215 16.5767C44.6121 16.5767 44.9644 16.6634 45.2783 16.8366C45.5937 17.0099 45.8437 17.2635 46.0283 17.5973C46.213 17.9311 46.3053 18.3381 46.3053 18.8182ZM45.2272 18.8182C45.2272 18.5341 45.1867 18.294 45.1057 18.098C45.0262 17.902 44.9111 17.7536 44.7606 17.6527C44.6114 17.5518 44.4317 17.5014 44.2215 17.5014C44.0127 17.5014 43.833 17.5518 43.6824 17.6527C43.5319 17.7536 43.4161 17.902 43.3351 18.098C43.2556 18.294 43.2158 18.5341 43.2158 18.8182C43.2158 19.1023 43.2556 19.3423 43.3351 19.5384C43.4161 19.7344 43.5319 19.8828 43.6824 19.9837C43.833 20.0845 44.0127 20.1349 44.2215 20.1349C44.4317 20.1349 44.6114 20.0845 44.7606 19.9837C44.9111 19.8828 45.0262 19.7344 45.1057 19.5384C45.1867 19.3423 45.2272 19.1023 45.2272 18.8182ZM46.9211 21V16.6364H48.7236C49.0503 16.6364 49.3323 16.6953 49.5695 16.8132C49.8082 16.9297 49.9921 17.0973 50.1214 17.3161C50.2506 17.5334 50.3153 17.7912 50.3153 18.0895C50.3153 18.392 50.2492 18.6491 50.1171 18.8608C49.985 19.071 49.7975 19.2315 49.5546 19.3423C49.3117 19.4517 49.0241 19.5064 48.6917 19.5064H47.5518V18.6754H48.4956C48.6547 18.6754 48.7876 18.6548 48.8941 18.6136C49.002 18.571 49.0837 18.5071 49.1391 18.4219C49.1945 18.3352 49.2222 18.2244 49.2222 18.0895C49.2222 17.9545 49.1945 17.843 49.1391 17.755C49.0837 17.6655 49.002 17.5987 48.8941 17.5547C48.7861 17.5092 48.6533 17.4865 48.4956 17.4865H47.9758V21H46.9211ZM49.3778 19.0057L50.4644 21H49.3138L48.2485 19.0057H49.3778Z" fill="white"/>
|
||||
<g clip-path="url(#clip0_3663_6247)">
|
||||
<path d="M30 40.5C34.1421 40.5 37.5 37.1421 37.5 33C37.5 28.8579 34.1421 25.5 30 25.5C25.8579 25.5 22.5 28.8579 22.5 33C22.5 37.1421 25.8579 40.5 30 40.5Z" stroke="#FBDDB0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M22.5 33H37.5" stroke="#FBDDB0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M30 25.5C31.876 27.5538 32.9421 30.219 33 33C32.9421 35.781 31.876 38.4462 30 40.5C28.124 38.4462 27.0579 35.781 27 33C27.0579 30.219 28.124 27.5538 30 25.5Z" stroke="#FBDDB0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3663_6247">
|
||||
<rect width="18" height="18" fill="white" transform="translate(21 24)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
1
packages/assets/icons/badge-check.svg
Normal 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-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg>
|
||||
|
After Width: | Height: | Size: 417 B |
1
packages/assets/icons/category/adventure.svg
Normal file
@ -0,0 +1 @@
|
||||
<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"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
|
||||
|
After Width: | Height: | Size: 235 B |
1
packages/assets/icons/category/atmosphere.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="M20 12h2"/><path d="m19.07 4.93-1.41 1.41"/><path d="M15.947 12.65a4 4 0 0 0-5.925-4.128"/><path d="M3 20a5 5 0 1 1 8.9-4H13a3 3 0 0 1 2 5.24"/><path d="M11 20v2"/><path d="M7 19v2"/></svg>
|
||||
|
After Width: | Height: | Size: 372 B |
1
packages/assets/icons/category/audio.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></svg>
|
||||
|
After Width: | Height: | Size: 290 B |
1
packages/assets/icons/category/blocks.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg 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"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>
|
||||
|
After Width: | Height: | Size: 354 B |
1
packages/assets/icons/category/bloom.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2h8l4 10H4L8 2Z"/><path d="M12 12v6"/><path d="M8 22v-2c0-1.1.9-2 2-2h4a2 2 0 0 1 2 2v2H8Z"/></svg>
|
||||
|
After Width: | Height: | Size: 234 B |
1
packages/assets/icons/category/cartoon.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="" data-darkreader-inline-stroke=""><path d="m9.06 11.9 8.07-8.06a2.85 2.85 0 1 1 4.03 4.03l-8.06 8.08"/><path d="M7.07 14.94c-1.66 0-3 1.35-3 3.02 0 1.33-2.5 1.52-2 2.02 1.08 1.1 2.49 2.02 4 2.02 2.2 0 4-1.8 4-4.04a3.01 3.01 0 0 0-3-3.02z"/></svg>
|
||||
|
After Width: | Height: | Size: 377 B |
1
packages/assets/icons/category/challenging.svg
Normal 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="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /></svg>
|
||||
|
After Width: | Height: | Size: 338 B |
1
packages/assets/icons/category/colored-lighting.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="7.618" cy="6.578" r="5.422" style="" transform="translate(3.143 .726) scale(1.16268)"/><circle cx="7.618" cy="6.578" r="5.422" style="" transform="translate(-.862 7.796) scale(1.16268)"/><circle cx="7.618" cy="6.578" r="5.422" style="" transform="translate(7.148 7.796) scale(1.16268)"/></svg>
|
||||
|
After Width: | Height: | Size: 364 B |
1
packages/assets/icons/category/combat.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.573 20.038L3.849 7.913 2.753 2.755 7.838 4.06 19.47 18.206l-1.898 1.832z"/><path d="M7.45 14.455l-3.043 3.661 1.887 1.843 3.717-3.25"/><path d="M16.75 10.82l3.333-2.913 1.123-5.152-5.091 1.28-2.483 2.985"/><path d="M21.131 16.602l-5.187 5.01 2.596-2.508 2.667 2.761"/><path d="M2.828 16.602l5.188 5.01-2.597-2.508-2.667 2.761"/></svg>
|
||||
|
After Width: | Height: | Size: 470 B |
1
packages/assets/icons/category/core-shaders.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/><line x1="20" y1="9" x2="23" y2="9"/><line x1="20" y1="14" x2="23" y2="14"/><line x1="1" y1="9" x2="4" y2="9"/><line x1="1" y1="14" x2="4" y2="14"/></svg>
|
||||
|
After Width: | Height: | Size: 521 B |
1
packages/assets/icons/category/cursed.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="7.5" width="10" height="14" rx="5"/><polyline points="2 12.5 4 14.5 7 14.5"/><polyline points="22 12.5 20 14.5 17 14.5"/><polyline points="3 21.5 5 18.5 7 17.5"/><polyline points="21 21.5 19 18.5 17 17.5"/><polyline points="3 8.5 5 10.5 7 11.5"/><polyline points="21 8.5 19 10.5 17 11.5"/><line x1="12" y1="7.5" x2="12" y2="21.5"/><path d="M15.38,8.82A3,3,0,0,0,16,7h0a3,3,0,0,0-3-3H11A3,3,0,0,0,8,7H8a3,3,0,0,0,.61,1.82"/><line x1="9" y1="4.5" x2="8" y2="2.5"/><line x1="15" y1="4.5" x2="16" y2="2.5"/></svg>
|
||||
|
After Width: | Height: | Size: 647 B |
1
packages/assets/icons/category/decoration.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
|
After Width: | Height: | Size: 229 B |
1
packages/assets/icons/category/economy.svg
Normal 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="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
|
||||
|
After Width: | Height: | Size: 228 B |
5
packages/assets/icons/category/entities.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg xml:space="preserve" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.5" clip-rule="evenodd" viewBox="0 0 24 24">
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path fill="none" stroke="currentColor" stroke-width="2" d="M3 3h18v18H3z"/>
|
||||
<path stroke="currentColor" fill="currentColor" d="M6 6h4v4H6zm8 0h4v4h-4zm-4 4h4v2h2v6h-2v-2h-4v2H8v-6h2v-2Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 375 B |
1
packages/assets/icons/category/environment.svg
Normal file
@ -0,0 +1 @@
|
||||
<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="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
|
||||
|
After Width: | Height: | Size: 509 B |
1
packages/assets/icons/category/equipment.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>
|
||||
|
After Width: | Height: | Size: 293 B |
1
packages/assets/icons/category/fantasy.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72Z"/><path d="m14 7 3 3"/><path d="M5 6v4"/><path d="M19 14v4"/><path d="M10 2v2"/><path d="M7 8H3"/><path d="M21 16h-4"/><path d="M11 3H9"/></svg>
|
||||
|
After Width: | Height: | Size: 419 B |
1
packages/assets/icons/category/foliage.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="" data-darkreader-inline-stroke=""><path d="M12 22v-7l-2-2"/><path d="M17 8v.8A6 6 0 0 1 13.8 20v0H10v0A6.5 6.5 0 0 1 7 8h0a5 5 0 0 1 10 0Z"/><path d="m14 14-2 2"/></svg>
|
||||
|
After Width: | Height: | Size: 300 B |
1
packages/assets/icons/category/fonts.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 7 4 4 20 4 20 7"/><line x1="9" y1="20" x2="15" y2="20"/><line x1="12" y1="4" x2="12" y2="20"/></svg>
|
||||
|
After Width: | Height: | Size: 243 B |
1
packages/assets/icons/category/food.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.27 21.7s9.87-3.5 12.73-6.36a4.5 4.5 0 0 0-6.36-6.37C5.77 11.84 2.27 21.7 2.27 21.7zM8.64 14l-2.05-2.04M15.34 15l-2.46-2.46"></path><path d="M22 9s-1.33-2-3.5-2C16.86 7 15 9 15 9s1.33 2 3.5 2S22 9 22 9z"></path><path d="M15 2s-2 1.33-2 3.5S15 9 15 9s2-1.84 2-3.5C17 3.33 15 2 15 2z"></path></svg>
|
||||
|
After Width: | Height: | Size: 430 B |
1
packages/assets/icons/category/game-mechanics.svg
Normal 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="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></svg>
|
||||
|
After Width: | Height: | Size: 467 B |
1
packages/assets/icons/category/gui.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg 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"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/></svg>
|
||||
|
After Width: | Height: | Size: 257 B |
1
packages/assets/icons/category/high.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M2 20h.01"></path><path d="M7 20v-4"></path><path d="M12 20v-8"></path><path d="M17 20V8"></path></svg>
|
||||
|
After Width: | Height: | Size: 235 B |
1
packages/assets/icons/category/items.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
|
||||
|
After Width: | Height: | Size: 246 B |
1
packages/assets/icons/category/kitchen-sink.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" xml:space="preserve"><g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m19.9 14-1.4 4.9c-.3 1-1.1 1.7-2.1 1.7H7.6c-.9 0-1.8-.7-2.1-1.7L4.1 14h15.8zM12 10V4.5M12 4.5c0-1.2.9-2.1 2.1-2.1M14.1 2.4c1.2 0 2.1.9 2.1 2.1M22.2 12c0 .6-.2 1.1-.6 1.4-.4.4-.9.6-1.4.6H3.8c-1.1 0-2-.9-2-2 0-.6.2-1.1.6-1.4.4-.4.9-.6 1.4-.6h16.4c1.1 0 2 .9 2 2z"/></g><path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M16.2 7.2h0"/></svg>
|
||||
|
After Width: | Height: | Size: 550 B |
1
packages/assets/icons/category/lightweight.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.24 12.24a6 6 0 0 0-8.49-8.49L5 10.5V19h8.5z"></path><line x1="16" y1="8" x2="2" y2="22"></line><line x1="17.5" y1="15" x2="9" y2="15"></line></svg>
|
||||
|
After Width: | Height: | Size: 284 B |
1
packages/assets/icons/category/locale.svg
Normal file
@ -0,0 +1 @@
|
||||
<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="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: 321 B |
1
packages/assets/icons/category/low.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M2 20h.01"></path><path d="M7 20v-4"></path></svg>
|
||||
|
After Width: | Height: | Size: 182 B |
1
packages/assets/icons/category/magic.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 4V2"></path><path d="M15 16v-2"></path><path d="M8 9h2"></path><path d="M20 9h2"></path><path d="M17.8 11.8 19 13"></path><path d="M15 9h0"></path><path d="M17.8 6.2 19 5"></path><path d="m3 21 9-9"></path><path d="M12.2 6.2 11 5"></path></svg>
|
||||
|
After Width: | Height: | Size: 380 B |
1
packages/assets/icons/category/management.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>
|
||||
|
After Width: | Height: | Size: 318 B |
1
packages/assets/icons/category/medium.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M2 20h.01"></path><path d="M7 20v-4"></path><path d="M12 20v-8"></path></svg>
|
||||
|
After Width: | Height: | Size: 209 B |
1
packages/assets/icons/category/minigame.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="7"/><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"/></svg>
|
||||
|
After Width: | Height: | Size: 219 B |
1
packages/assets/icons/category/mobs.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xml:space="preserve" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.5" clip-rule="evenodd" viewBox="0 0 24 24">\n <path fill="none" d="M0 0h24v24H0z"/>\n <path fill="none" stroke="currentColor" stroke-width="2" d="M3 3h18v18H3z"/>\n <path stroke="currentColor" fill="currentColor" d="M6 6h4v4H6zm8 0h4v4h-4zm-4 4h4v2h2v6h-2v-2h-4v2H8v-6h2v-2Z"/></svg>
|
||||
|
After Width: | Height: | Size: 378 B |
1
packages/assets/icons/category/modded.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg 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"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
|
||||
|
After Width: | Height: | Size: 262 B |
1
packages/assets/icons/category/models.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg>
|
||||
|
After Width: | Height: | Size: 247 B |
1
packages/assets/icons/category/multiplayer.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
|
||||
|
After Width: | Height: | Size: 312 B |
1
packages/assets/icons/category/optimization.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
||||
|
After Width: | Height: | Size: 187 B |
1
packages/assets/icons/category/path-tracing.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" style="" fill="none" stroke="currentColor" stroke-width="2"><path d="M2.977 19.17h16.222" style="" transform="translate(-.189 -.328) scale(1.09932)"/><path d="M3.889 3.259 12 19.17l5.749-11.277" style="" transform="translate(-1.192 -.328) scale(1.09932)"/><path d="M9.865 6.192h4.623v4.623" style="" transform="scale(1.09931) rotate(-18 20.008 .02)"/></svg>
|
||||
|
After Width: | Height: | Size: 382 B |
1
packages/assets/icons/category/pbr.svg
Normal 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="9" y1="18" x2="15" y2="18"/><line x1="10" y1="22" x2="14" y2="22"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"/></svg>
|
||||
|
After Width: | Height: | Size: 332 B |
1
packages/assets/icons/category/potato.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 512 512" fill="currentColor" stroke="currentColor"><g><g><path d="M218.913,116.8c-6.4-6.4-16-6.4-22.4,0c-3.2,3.2-4.8,6.4-4.8,11.2s1.6,8,4.8,11.2c3.2,3.2,8,4.8,11.2,4.8 c4.8,0,8-1.6,11.2-4.8c3.2-3.2,4.8-6.4,4.8-11.2S222.113,120,218.913,116.8z"/></g></g><g><g><path d="M170.913,372.8c-6.4-6.4-16-6.4-22.4,0c-3.2,3.2-4.8,6.4-4.8,11.2s1.6,8,4.8,11.2c3.2,3.2,8,4.8,11.2,4.8 c4.8,0,8-1.6,11.2-4.8c3.2-3.2,4.8-8,4.8-11.2C175.713,379.2,174.113,376,170.913,372.8z"/></g></g><g><g><path d="M250.913,228.8c-4.8-6.4-16-6.4-22.4,0c-3.2,3.2-4.8,6.4-4.8,11.2s1.6,8,4.8,11.2c3.2,3.2,8,4.8,11.2,4.8 c4.8,0,8-1.6,11.2-4.8c3.2-3.2,4.8-8,4.8-11.2C255.713,235.2,254.113,232,250.913,228.8z"/></g></g><g><g><path d="M410.913,212.8c-4.8-6.4-16-6.4-22.4,0c-3.2,3.2-4.8,6.4-4.8,11.2s1.6,8,4.8,11.2c3.2,3.2,8,4.8,11.2,4.8 c4.8,0,8-1.6,11.2-4.8c3.2-3.2,4.8-8,4.8-11.2C415.713,219.2,414.113,216,410.913,212.8z"/></g></g><g><g><path d="M346.913,308.8c-4.8-6.4-16-6.4-22.4,0c-3.2,3.2-4.8,6.4-4.8,11.2s1.6,8,4.8,11.2c3.2,3.2,8,4.8,11.2,4.8 c4.8,0,8-1.6,11.2-4.8c3.2-3.2,4.8-8,4.8-11.2C351.713,315.2,350.113,312,346.913,308.8z"/></g></g><g><g><path d="M346.913,100.8c-6.4-6.4-16-6.4-22.4,0c-3.2,3.2-4.8,6.4-4.8,11.2s1.6,8,4.8,11.2c3.2,3.2,8,4.8,11.2,4.8 c4.8,0,8-1.6,11.2-4.8s4.8-6.4,4.8-11.2S350.113,104,346.913,100.8z"/></g></g><g><g><path d="M503.713,142.4c-28.8-136-179.2-142.4-208-142.4c-4.8,0-9.6,0-16,0c-67.2,1.6-132.8,36.8-187.2,97.6 c-60.8,67.2-96,155.2-91.2,227.2c8,126.4,70.4,187.2,192,187.2c115.2,0,201.6-33.6,256-100.8 C513.313,331.2,519.713,219.2,503.713,142.4z M423.713,392c-48,59.2-126.4,89.6-230.4,89.6s-152-48-160-158.4 c-4.8-64,28.8-144,83.2-203.2c48-54.4,107.2-84.8,164.8-88c4.8,0,9.6,0,14.4,0c140.8,0,171.2,89.6,176,116.8 C486.113,219.2,481.313,320,423.713,392z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
1
packages/assets/icons/category/quests.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="2" width="6" height="6"></rect><rect x="16" y="16" width="6" height="6"></rect><rect x="2" y="16" width="6" height="6"></rect><path d="M12 8v4m0 0H5v4m7-4h7v4"></path></svg>
|
||||
|
After Width: | Height: | Size: 311 B |
1
packages/assets/icons/category/realistic.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"/><circle cx="12" cy="13" r="3"/></svg>
|
||||
|
After Width: | Height: | Size: 262 B |
1
packages/assets/icons/category/reflections.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style=""><path d="m3 7 5 5-5 5V7"/><path d="m21 7-5 5 5 5V7"/><path d="M12 20v2"/><path d="M12 14v2"/><path d="M12 8v2"/><path d="M12 2v2"/></svg>
|
||||
|
After Width: | Height: | Size: 269 B |
1
packages/assets/icons/category/screenshot.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg 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: 287 B |
1
packages/assets/icons/category/semi-realistic.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"/><line x1="7" y1="2" x2="7" y2="22"/><line x1="17" y1="2" x2="17" y2="22"/><line x1="2" y1="12" x2="22" y2="12"/><line x1="2" y1="7" x2="7" y2="7"/><line x1="2" y1="17" x2="7" y2="17"/><line x1="17" y1="17" x2="22" y2="17"/><line x1="17" y1="7" x2="22" y2="7"/></svg>
|
||||
|
After Width: | Height: | Size: 451 B |
1
packages/assets/icons/category/shadows.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m8 3 4 8 5-5 5 15H2L8 3z"/></svg>
|
||||
|
After Width: | Height: | Size: 165 B |
1
packages/assets/icons/category/simplistic.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg 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"/></svg>
|
||||
|
After Width: | Height: | Size: 262 B |
1
packages/assets/icons/category/social.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 325 B |
1
packages/assets/icons/category/storage.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="21 8 21 21 3 21 3 8"/><rect x="1" y="3" width="22" height="5"/><line x1="10" y1="12" x2="14" y2="12"/></svg>
|
||||
|
After Width: | Height: | Size: 249 B |
1
packages/assets/icons/category/technology.svg
Normal 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="22" y1="12" x2="2" y2="12"/><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/><line x1="6" y1="16" x2="6.01" y2="16"/><line x1="10" y1="16" x2="10.01" y2="16"/></svg>
|
||||
|
After Width: | Height: | Size: 367 B |
1
packages/assets/icons/category/themed.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/></svg>
|
||||
|
After Width: | Height: | Size: 274 B |
1
packages/assets/icons/category/transportation.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="3" width="15" height="13"/><polygon points="16 8 20 8 23 11 23 16 16 16 16 8"/><circle cx="5.5" cy="18.5" r="2.5"/><circle cx="18.5" cy="18.5" r="2.5"/></svg>
|
||||
|
After Width: | Height: | Size: 296 B |
1
packages/assets/icons/category/tweaks.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 266 B |