Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61c2ce2107 | ||
|
|
d37c634216 | ||
|
|
cd2fcc06fe | ||
|
|
a731f4758e | ||
|
|
0e0fce0e66 | ||
|
|
ea2f97ae23 | ||
|
|
2a0722d0d0 | ||
|
|
adf213f32a | ||
|
|
54f408dc6c | ||
|
|
c82c4ddc5b |
No files matched your search
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<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="#313744" stroke="#7B899B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12.1252 18.2173H11.0598C11.0456 18.108 11.0165 18.0092 10.9725 17.9212C10.9284 17.8331 10.8702 17.7578 10.7978 17.6953C10.7253 17.6328 10.6394 17.5852 10.54 17.5526C10.4419 17.5185 10.3333 17.5014 10.214 17.5014C10.0023 17.5014 9.81978 17.5533 9.66637 17.657C9.51438 17.7607 9.39719 17.9105 9.31481 18.1065C9.23384 18.3026 9.19336 18.5398 9.19336 18.8182C9.19336 19.108 9.23455 19.3509 9.31694 19.5469C9.40075 19.7415 9.51793 19.8885 9.6685 19.9879C9.82049 20.0859 10.0002 20.1349 10.2076 20.1349C10.324 20.1349 10.4299 20.12 10.525 20.0902C10.6216 20.0604 10.7061 20.017 10.7786 19.9602C10.8525 19.902 10.9128 19.8317 10.9597 19.7493C11.008 19.6655 11.0414 19.571 11.0598 19.4659L12.1252 19.4723C12.1067 19.6655 12.0506 19.8558 11.9569 20.0433C11.8645 20.2308 11.7374 20.402 11.5755 20.5568C11.4135 20.7102 11.2161 20.8324 10.9831 20.9233C10.7516 21.0142 10.486 21.0597 10.1863 21.0597C9.79137 21.0597 9.43768 20.973 9.12518 20.7997C8.8141 20.625 8.56836 20.3707 8.38796 20.0369C8.20756 19.7031 8.11737 19.2969 8.11737 18.8182C8.11737 18.3381 8.20898 17.9311 8.39222 17.5973C8.57546 17.2635 8.82333 17.0099 9.13583 16.8366C9.44833 16.6634 9.79847 16.5767 10.1863 16.5767C10.4505 16.5767 10.6948 16.6136 10.9192 16.6875C11.1436 16.7599 11.3411 16.8665 11.5115 17.0071C11.682 17.1463 11.8205 17.3175 11.927 17.5206C12.0336 17.7237 12.0996 17.956 12.1252 18.2173ZM16.8473 18.8182C16.8473 19.2983 16.755 19.7053 16.5703 20.0391C16.3857 20.3729 16.1357 20.6264 15.8203 20.7997C15.5064 20.973 15.1541 21.0597 14.7635 21.0597C14.3714 21.0597 14.0185 20.9723 13.7045 20.7976C13.3906 20.6229 13.1413 20.3693 12.9567 20.0369C12.7734 19.7031 12.6818 19.2969 12.6818 18.8182C12.6818 18.3381 12.7734 17.9311 12.9567 17.5973C13.1413 17.2635 13.3906 17.0099 13.7045 16.8366C14.0185 16.6634 14.3714 16.5767 14.7635 16.5767C15.1541 16.5767 15.5064 16.6634 15.8203 16.8366C16.1357 17.0099 16.3857 17.2635 16.5703 17.5973C16.755 17.9311 16.8473 18.3381 16.8473 18.8182ZM15.7692 18.8182C15.7692 18.5341 15.7287 18.294 15.6477 18.098C15.5682 17.902 15.4531 17.7536 15.3026 17.6527C15.1534 17.5518 14.9737 17.5014 14.7635 17.5014C14.5547 17.5014 14.375 17.5518 14.2244 17.6527C14.0739 17.7536 13.9581 17.902 13.8771 18.098C13.7976 18.294 13.7578 18.5341 13.7578 18.8182C13.7578 19.1023 13.7976 19.3423 13.8771 19.5384C13.9581 19.7344 14.0739 19.8828 14.2244 19.9837C14.375 20.0845 14.5547 20.1349 14.7635 20.1349C14.9737 20.1349 15.1534 20.0845 15.3026 19.9837C15.4531 19.8828 15.5682 19.7344 15.6477 19.5384C15.7287 19.3423 15.7692 19.1023 15.7692 18.8182ZM21.1768 16.6364V21H20.282L18.5455 18.4815H18.5178V21H17.4631V16.6364H18.3707L20.0881 19.1506H20.1243V16.6364H21.1768ZM21.6973 17.4929V16.6364H25.3855V17.4929H24.0623V21H23.0225V17.4929H21.6973ZM25.9006 21V16.6364H27.7031C28.0298 16.6364 28.3118 16.6953 28.549 16.8132C28.7876 16.9297 28.9716 17.0973 29.1009 17.3161C29.2301 17.5334 29.2947 17.7912 29.2947 18.0895C29.2947 18.392 29.2287 18.6491 29.0966 18.8608C28.9645 19.071 28.777 19.2315 28.5341 19.3423C28.2912 19.4517 28.0036 19.5064 27.6712 19.5064H26.5312V18.6754H27.4751C27.6342 18.6754 27.767 18.6548 27.8736 18.6136C27.9815 18.571 28.0632 18.5071 28.1186 18.4219C28.174 18.3352 28.2017 18.2244 28.2017 18.0895C28.2017 17.9545 28.174 17.843 28.1186 17.755C28.0632 17.6655 27.9815 17.5987 27.8736 17.5547C27.7656 17.5092 27.6328 17.4865 27.4751 17.4865H26.9553V21H25.9006ZM28.3572 19.0057L29.4439 21H28.2933L27.228 19.0057H28.3572ZM30.9279 16.6364V21H29.8732V16.6364H30.9279ZM31.59 21V16.6364H33.4096C33.7363 16.6364 34.0098 16.6825 34.2299 16.7749C34.4515 16.8672 34.6177 16.9964 34.7285 17.1626C34.8407 17.3288 34.8968 17.5213 34.8968 17.7401C34.8968 17.9062 34.862 18.0547 34.7924 18.1854C34.7228 18.3146 34.627 18.4219 34.5048 18.5071C34.3826 18.5923 34.2413 18.652 34.0808 18.6861V18.7287C34.2569 18.7372 34.4196 18.7848 34.5687 18.8714C34.7193 18.9581 34.84 19.0788 34.9309 19.2337C35.0218 19.3871 35.0673 19.5689 35.0673 19.7791C35.0673 20.0135 35.0076 20.223 34.8883 20.4077C34.769 20.5909 34.5964 20.7358 34.3706 20.8423C34.1447 20.9474 33.8706 21 33.5481 21H31.59ZM32.6447 20.1499H33.2967C33.5254 20.1499 33.6937 20.1065 33.8017 20.0199C33.911 19.9332 33.9657 19.8125 33.9657 19.6577C33.9657 19.5455 33.9395 19.4489 33.8869 19.3679C33.8343 19.2855 33.7598 19.2223 33.6632 19.1783C33.5666 19.1328 33.4508 19.1101 33.3159 19.1101H32.6447V20.1499ZM32.6447 18.4304H33.2285C33.3436 18.4304 33.4458 18.4112 33.5353 18.3729C33.6248 18.3345 33.6944 18.2791 33.7441 18.2067C33.7953 18.1342 33.8208 18.0469 33.8208 17.9446C33.8208 17.7983 33.769 17.6832 33.6653 17.5994C33.5616 17.5156 33.4217 17.4737 33.2456 17.4737H32.6447V18.4304ZM38.2143 16.6364H39.2669V19.451C39.2669 19.7763 39.1895 20.0597 39.0346 20.3011C38.8798 20.5412 38.6639 20.7273 38.3869 20.8594C38.1099 20.9901 37.7882 21.0554 37.4217 21.0554C37.051 21.0554 36.7271 20.9901 36.4501 20.8594C36.1731 20.7273 35.9579 20.5412 35.8045 20.3011C35.6511 20.0597 35.5744 19.7763 35.5744 19.451V16.6364H36.6291V19.3594C36.6291 19.5099 36Line truncated
|
||||
<path d="M36.3149 27.435C35.9387 27.0574 35.4916 26.7579 34.9993 26.5534C34.507 26.349 33.9792 26.2438 33.4462 26.2438C32.9132 26.2438 32.3854 26.349 31.8931 26.5534C31.4008 26.7579 30.9537 27.0574 30.5774 27.435L29.9999 28.02L29.4224 27.435C29.0462 27.0574 28.5991 26.7579 28.1068 26.5534C27.6145 26.349 27.0867 26.2438 26.5537 26.2438C26.0207 26.2438 25.4929 26.349 25.0006 26.5534C24.5083 26.7579 24.0612 27.0574 23.6849 27.435C22.0949 29.025 21.9974 31.71 23.9999 33.75L29.9999 39.75L35.9999 33.75C38.0024 31.71 37.9049 29.025 36.3149 27.435Z" stroke="#96A2B0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M30 28.02L27.6525 30.375C27.3557 30.6742 27.1892 31.0786 27.1892 31.5C27.1892 31.9214 27.3557 32.3258 27.6525 32.625C27.9517 32.9218 28.3561 33.0883 28.7775 33.0883C29.199 33.0883 29.6033 32.9218 29.9025 32.625L31.5975 30.9675C32.0188 30.5507 32.5874 30.3169 33.18 30.3169C33.7726 30.3169 34.3413 30.5507 34.7625 30.9675L36.5625 32.7675" stroke="#96A2B0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M34.5 35.25L33 33.75" stroke="#96A2B0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M32.25 37.5L30.75 36" stroke="#96A2B0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.9 KiB |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |