feat: linked server instances (#5221)

* ping queue with tests

* mc ping server info + timeout

* sqlx prepare

* tombi fmt

* tombi fmt

* allow querying server ping data

* fix shear

* wip: resolve comments with pings

* Switch to Redis for server pings

* tombi fmt

* fix compile error

* clear cache on project ping, add server store link

* Schema changes

* Improve server messages for app pinging

* synthetic server project version for search indexing

* wip: clean up server ping, background tasks

* fix migration to sync with main, propagate background task errors

* wip: server modpack content query, components in search

* wip: massive component query refactor

* fix more defaults stuff

* sqlx

* fix serde deser flatten

* fix search indexing not showing fields

* remove leftover prompt

* fix import

* add diff detection for version dependencies without version_id/project_id

* move servers tab to end

* hide app nav tabs if only one tab

* fix undefined property

* on click link for server side bar info

* show recommended & supported versions for vanilla

* fix how install.js installs instance with modpack content title instead of server project title and dont fetch icon when installing to existing instance

* use large play button instance

* show update success instead of launching right into the game

* add global installing server project state

* add comment

* small change: open discover to modpack

* implement ping server projects for latency in app

* add projectV3 to nag context for moderation package

* fix play server project button when instance is launched

* add ping to project header

* wip: server verified plays

* server verified plays compiling

* queue up server plays in batches

* report server plays improved in frontend

* fixes to tracking server joins

* fix: server project detection to do loose null check

* fix server projects showing license

* fix empty server info card

* fix server projects links title

* Fix backend impl for server player count analytics

* fix: allow for links to be set to empty

* hook up server recent plays

* cargo sqlx prepare

* add project sidebar stories

* feat: update project sidebar server info card to new design

* update server project header and project card

* feat: add hide label for project cards

* feat: add tags sidebar card

* small fix to keep color consistent

* fix: remove required content tab from server project page

* many small fixes

* handle locking server instance content

* fix hiding modal after saving server compatibility version

* copy content card item and table from content tab update branch

* fix nav tabs active tag

* fix switching between server instance vs regular instance persisted invalid state

* fix a lot of the bugginess of navtabs when theres hidden/shown tabs between instances. match frontend nav tabs

* hook up backend searchfor frontend in websiet

* fix: server project card tags

* hook up search v3 in app backend for app frontend

* Don't return missing components in project query

* Add game versions to server filters

* move reporting server joins to backend

* send account UUID along with server play analytics

* update java server ping schema

* feat: implement use server search for search sorting and filter facets

* pnpm prepr

* fix game version filter facet

* fix: allow java and bedrock addresses to be deleted

* feat: hook up languages

* Default deserialize `ProjectSerial`

* feat: show server project tags

* small fix on languages multi select

* also default java server content

* fix: update compatibility modal not closing after successful upload

* remove play button in website discovery for servers

* reenable fence in app backend

* update online/offline tag

* add online status indicator pulsing

* revert pulsing

* disable link for custom modpack project and show tooltip

* change modpack to modded type

* update ip address entire button to be clickable

* polish server info card styles

* make offline tag red and properly hook up online tag

* move server related settings into own tab

* fix setting project compatibility resets unsaved changes

* fix javaServerPatchaData wiping content field

* updates to compatibility card, add download button and display supported versions better

* fix unsaved changes popup for tags

* remove console.log

* fix incorrect project type in projects in dashboard

* fix: savable.ts to reset currentValues to data() after save

* upload server banner as gallery image with title == "__mc_server_banner__" and filter it from frontend gallery

* fix error handling and helper text copy

* ensure gallery banners are filtered in app backend gallery display

* add grouped filters for search

* add query params for server search

* feat: deep linking to open server project page then open install to play

* fix search in app frontend

* fix: server project showing offline

* fix: profile create error app backend

Here's what was happening and the fix:

Root cause: In create.rs:107, profile_create assumed the icon_path parameter was always a local filename relative to the caches directory. It did caches_dir().join(icon) which produced a path like ...\caches\https://staging-cdn.modrinth.com/... — the colons in https:// are illegal in Windows paths (OS error 123).

The frontend's installServerProject and createVanillaInstance in install.js:290 both pass project.icon_url (a full URL) directly as the icon parameter.

Fix: Modified profile_create to detect when the icon parameter is a URL (starts with http:// or https://). When it is, it downloads the icon via fetch(), extracts the filename from the URL path, and passes the downloaded bytes and filename to set_icon() which hashes and caches it properly. The existing local-file path continues to work as before.

* pass undefined instead of unknown for modpack content modal

* fix: wrong way to determine offline status

* delete required content page placeholder

* fix: redirect running function instead of passing function

* add in wiki page

* fix diffs which have unknown project/filename

* pnpm prepr

* feat: add handling for "stop" instance state for server project card and page play button

* fix updating modpack shouldn't launch right into game

* small fix on external icon

* fix refresh search causing infinite rerender i.e. maximum call stack size exceeded

watch(route) → watch(() => [route.query.i, route.query.ai, route.path]) (line 102): The deep watch on the entire Vue Router route object was the most likely cause of the stack overflow. Vue Router's route object contains matched records with component definitions and other deeply nested structures. Deep-watching it triggers recursive traversal on every route change (including those from router.replace() inside refreshSearch()). Now it only watches the specific properties that updateInstanceContext() actually needs.

ref → shallowRef for serverHits and serverPings (line 189-190): The v3 search results can be deeply nested objects (minecraft_java_server.ping.data, content, etc.). Using shallowRef prevents Vue from creating deep reactive proxies on these objects, which is consistent with how results already uses shallowRef on line 295.

Re-entrance guard + try/catch on refreshSearch() (line 310): The watcher calls refreshSearch() without awaiting, so state changes during the async execution could trigger the watcher again, causing concurrent calls. The guard prevents overlapping calls, and the try/catch ensures loading.value = false is always reached (fixing the infinite loading).

* don't require auth token for logging server play

* fetch latest server player count from redis instead of search doc

* remove components. in search facet

* Category and search sort fixes

* add logging for refreshSearch in browse.vue

* fix: use windows.history.replace instead of router.replace due to vue production bug and remove logs

* fix: server refresh search reactivity

* fix: type errors

* conquer the type errors in Browse.vue

* update search input background

* fix tags location

* slight change to color

* feat: add linked to modpack project for regular modpack instances

* feat: installation tab updates

* fix: copy ip missing hover effect

* feat: implement category and countries negative filters

* fix servers tab label in profile page

* implement add server to instance

* feat: implement allow editing server instances

* update installation settings to handle vanilla server instance case

* hide servers tab when installing content to instance

* add sorting for user installed content to be top of list in content

* update categories filters from one group filter card to separate filters cards

* add active scale

* fix offline server showing online

* update language display

* update tooltip

* hide navtabs if theres only one tab

* fix: modpack content name truncate in project card

* feat: add server projects to moderation queue

* update redirect middleware no longer needs projectV3

* update comment

* fix: server tags labels

* feat: add the mf icons finally

* Revert "update redirect middleware no longer needs projectV3"

This reverts commit 1289cb52869185abe1481dfb6b0c00c0233bf59e.

* fix open in browser

* revert any handling for handling base linked modpack content for content tab

* update instance online players to be client ping

* fix showing modpack/loader version for server instance in installation settings

* server projects are not marked as modpacks

* skip license check for server projects

* feat: add the concept of linked worlds for server instances and keep in sync with server project

* fix: router.push doesn't add history state, use nagivateTo instead

* fix: get server modpack content wrong link

* update some categories to default collapse

* small fixes

* optional languages & bedrock

* move creator below tags

* sort linked worlds to be first

* add red orange and green ping variants

* bring back content tab

* add download button in required content in app

* fix: server info card loading

* fix: brief flash of normal project before server project stuff loads in

* misc fixes

* invalidate project v3

* fix unused imports

* Quick pass for moderation related changes (#5429)

* filter certain nags out from server projects.

* move add-links nag to links.ts

* first few server related nags

* moderation checklist groundwork

* Prevent undefined stage from appearing on servers.

* add projectV3 to shouldShow callback

* Filter buttons by server project type

* fix, revert private use msg, adjust server & link nags

* starting tags + servers msg

* fix no projectV3

* fix: router.push doesn't add history state, use nagivateTo instead

* Tags nag works with servers now

* support servers' v3 exclusive links

* reupload, and status messages + nag tweaks.

* fixes

* Update tags.vue warning for server projects.

* don't suggest adding a bedrock IP

* Tweak phrasing on servers alert msg

---------

Signed-off-by: Truman Gao <106889354+tdgao@users.noreply.github.com>
Co-authored-by: tdgao <mr.trumgao@gmail.com>
Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com>

* only show unique tags in project card

* add projectV3 to cache purge

* fix type: add projectV3 to cache purge

* update caching behaviour for installing

* max 3 plays per user

* accept date_modified and date_created for sorting

* add locking environment filter for server instance and update copy

* custom pack button only shows when needed (#5444)

* expose server pinging route to frontend

* feat: add server field validation with pinging on unfocus

* improve pinging logs

* try another pinging crate

* small fixes

* prefill published project id for updating published project

* fix running app bar for mac

* cargo sqlx prepare

* fix app login avatar

* pnpm prepr

* fix download menu for mac

* FIX CI

* fix lint errors

* cargo fmt

* fix toml

* fix more lint

* add server copy

* more lint

* fix any types

* also ping unlisted and private servers

* fix lint

* remove option for showTypeSelector

* fix cannot read user from undefined

* pnpm prepr

* update pinging to make it better

* update copy

* fix login cache issue

* add project select default icon

* fix: minecraft_java_server not redirecting

* pnpm prepr

* fix required content card in project page for custom modpack

* fix app project cards custom modpacks

* update pre-collapsed for app frontend

* don't send server projects to discord webhook

* add lock icon to linked world managed by server project

* pnpm prepr

* make automod msgs on server projects private

* fix pagination for server projects tab

* fix recent plays copy

* fix sync linked world with server project

* pnpm prepr

* add 0.11.0 changelog

* update date

---------

Signed-off-by: Truman Gao <106889354+tdgao@users.noreply.github.com>
Co-authored-by: aecsocket <aecsocket@tutanota.com>
Co-authored-by: coolbot <76798835+coolbot100s@users.noreply.github.com>
This commit is contained in:
Truman Gao
2026-03-02 15:38:09 -08:00
committed by GitHub
parent 51066c476a
commit 51ceb9d851
318 changed files with 19891 additions and 4524 deletions

View File

@@ -42,7 +42,7 @@ client.archon.backups_v1
client.archon.content_v0
client.kyros.files_v0
client.iso3166.data
... ect.
... etc.
```
This structure is derived at runtime from the flat `MODULE_REGISTRY` in `modules/index.ts` via `buildModuleStructure()`, and the TypeScript types are inferred automatically via `InferredClientModules`.

View File

@@ -12,8 +12,10 @@ import { LabrinthBillingInternalModule } from './labrinth/billing/internal'
import { LabrinthCollectionsModule } from './labrinth/collections'
import { LabrinthProjectsV2Module } from './labrinth/projects/v2'
import { LabrinthProjectsV3Module } from './labrinth/projects/v3'
import { LabrinthServerPingInternalModule } from './labrinth/server-ping/internal'
import { LabrinthStateModule } from './labrinth/state'
import { LabrinthTechReviewInternalModule } from './labrinth/tech-review/internal'
import { LabrinthThreadsV3Module } from './labrinth/threads/v3'
type ModuleConstructor = new (client: AbstractModrinthClient) => AbstractModule
@@ -38,8 +40,10 @@ export const MODULE_REGISTRY = {
labrinth_collections: LabrinthCollectionsModule,
labrinth_projects_v2: LabrinthProjectsV2Module,
labrinth_projects_v3: LabrinthProjectsV3Module,
labrinth_server_ping_internal: LabrinthServerPingInternalModule,
labrinth_state: LabrinthStateModule,
labrinth_tech_review_internal: LabrinthTechReviewInternalModule,
labrinth_threads_v3: LabrinthThreadsV3Module,
labrinth_versions_v3: LabrinthVersionsV3Module,
} as const satisfies Record<string, ModuleConstructor>

View File

@@ -2,6 +2,8 @@ export * from './billing/internal'
export * from './collections'
export * from './projects/v2'
export * from './projects/v3'
export * from './server-ping/internal'
export * from './state'
export * from './tech-review/internal'
export * from './threads/v3'
export * from './versions/v3'

View File

@@ -135,4 +135,109 @@ export class LabrinthProjectsV2Module extends AbstractModule {
method: 'GET',
})
}
/**
* Create a gallery image for a project
*
* @param id - Project ID or slug
* @param file - Image file to upload
* @param options - Gallery image options
*
* @example
* ```typescript
* await client.labrinth.projects_v2.createGalleryImage('sodium', imageFile, {
* featured: true,
* title: 'Screenshot 1',
* description: 'Main menu with Sodium enabled'
* })
* ```
*/
public async createGalleryImage(
id: string,
file: Blob,
options: {
ext: string
featured: boolean
title?: string
description?: string
ordering?: number
},
): Promise<void> {
const params: Record<string, string> = {
ext: options.ext,
featured: String(options.featured),
}
if (options.title) params.title = options.title
if (options.description) params.description = options.description
if (options.ordering !== undefined) params.ordering = String(options.ordering)
return this.client.request(`/project/${id}/gallery`, {
api: 'labrinth',
version: 2,
method: 'POST',
params,
body: file,
})
}
/**
* Edit a gallery image for a project
*
* @param id - Project ID or slug
* @param url - URL of the existing gallery image to edit
* @param options - Gallery image options to update
*
* @example
* ```typescript
* await client.labrinth.projects_v2.editGalleryImage('sodium', 'https://cdn.modrinth.com/...', {
* featured: false,
* title: 'Updated title'
* })
* ```
*/
public async editGalleryImage(
id: string,
url: string,
options: {
featured: boolean
title?: string
description?: string
ordering?: number
},
): Promise<void> {
const params: Record<string, string> = {
url,
featured: String(options.featured),
}
if (options.title) params.title = options.title
if (options.description) params.description = options.description
if (options.ordering !== undefined) params.ordering = String(options.ordering)
return this.client.request(`/project/${id}/gallery`, {
api: 'labrinth',
version: 2,
method: 'PATCH',
params,
})
}
/**
* Delete a gallery image from a project
*
* @param id - Project ID or slug
* @param url - URL of the gallery image to delete
*
* @example
* ```typescript
* await client.labrinth.projects_v2.deleteGalleryImage('sodium', 'https://cdn.modrinth.com/...')
* ```
*/
public async deleteGalleryImage(id: string, url: string): Promise<void> {
return this.client.request(`/project/${id}/gallery`, {
api: 'labrinth',
version: 2,
method: 'DELETE',
params: { url },
})
}
}

View File

@@ -27,6 +27,32 @@ export class LabrinthProjectsV3Module extends AbstractModule {
})
}
/**
* Get a project's dependencies (v3)
*
* Returns all projects and versions that are dependencies of this project's versions.
*
* @param id - Project ID or slug
* @returns Promise resolving to dependency data with projects and versions
*
* @example
* ```typescript
* const deps = await client.labrinth.projects_v3.getDependencies('sodium')
* console.log(deps.projects) // Array of project objects
* console.log(deps.versions) // Array of version objects
* ```
*/
public async getDependencies(id: string): Promise<Labrinth.Projects.v3.ProjectDependencies> {
return this.client.request<Labrinth.Projects.v3.ProjectDependencies>(
`/project/${id}/dependencies`,
{
api: 'labrinth',
version: 3,
method: 'GET',
},
)
}
/**
* Get multiple projects by IDs (v3)
*
@@ -103,4 +129,73 @@ export class LabrinthProjectsV3Module extends AbstractModule {
method: 'GET',
})
}
public async createServerProject(
data: Labrinth.Projects.v3.CreateServerProjectRequest,
): Promise<Labrinth.Projects.v3.Project> {
return this.client.request<Labrinth.Projects.v3.Project>(`/project`, {
api: 'labrinth',
version: 3,
method: 'PUT',
body: data,
})
}
/**
* Delete a project
*
* @param id - Project ID or slug
*
* @example
* ```typescript
* await client.labrinth.projects_v3.deleteProject('my-project')
* ```
*/
public async deleteProject(id: string): Promise<void> {
return this.client.request(`/project/${id}`, {
api: 'labrinth',
version: 3,
method: 'DELETE',
})
}
/**
* Change the icon of a project
*
* @param id - Project ID or slug
* @param file - Image file to upload
* @param ext - File extension (e.g., 'png', 'jpeg', 'gif', 'webp')
*
* @example
* ```typescript
* await client.labrinth.projects_v3.changeIcon('sodium', imageFile, 'png')
* ```
*/
public async changeIcon(id: string, file: Blob, ext: string): Promise<void> {
return this.client.request(`/project/${id}/icon`, {
api: 'labrinth',
version: 3,
method: 'PATCH',
params: { ext },
body: file,
})
}
/**
* Delete the icon of a project
*
* @param id - Project ID or slug
*
* @example
* ```typescript
* await client.labrinth.projects_v3.deleteIcon('sodium')
* ```
*/
public async deleteIcon(id: string): Promise<void> {
return this.client.request(`/project/${id}/icon`, {
api: 'labrinth',
version: 3,
method: 'DELETE',
})
}
}

View File

@@ -0,0 +1,23 @@
import { AbstractModule } from '../../../core/abstract-module'
import type { Labrinth } from '../types'
export class LabrinthServerPingInternalModule extends AbstractModule {
public getModuleID(): string {
return 'labrinth_server_ping_internal'
}
/**
* Ping a Minecraft Java server
* POST /_internal/server-ping/minecraft-java
*/
public async pingMinecraftJava(
request: Labrinth.ServerPing.Internal.MinecraftJavaPingRequest,
): Promise<void> {
return this.client.request<void>('/server-ping/minecraft-java', {
api: 'labrinth',
version: 'internal',
method: 'POST',
body: request,
})
}
}

View File

@@ -0,0 +1,73 @@
import { AbstractModule } from '../../../core/abstract-module'
import type { Labrinth } from '../types'
export class LabrinthThreadsV3Module extends AbstractModule {
public getModuleID(): string {
return 'labrinth_threads_v3'
}
/**
* Get a thread by ID (v3)
*
* @param id - Thread ID
* @returns Promise resolving to the thread data
*
* @example
* ```typescript
* const thread = await client.labrinth.threads_v3.getThread('abc123')
* console.log(thread.messages)
* ```
*/
public async getThread(id: string): Promise<Labrinth.Threads.v3.Thread> {
return this.client.request<Labrinth.Threads.v3.Thread>(`/thread/${id}`, {
api: 'labrinth',
version: 3,
method: 'GET',
})
}
/**
* Send a message to a thread (v3)
*
* @param id - Thread ID
* @param message - Message body to send
* @returns Promise resolving when message is sent
*
* @example
* ```typescript
* await client.labrinth.threads_v3.sendMessage('abc123', {
* body: { type: 'text', body: 'Hello!' }
* })
* ```
*/
public async sendMessage(
id: string,
message: Labrinth.Threads.v3.SendMessageRequest,
): Promise<void> {
return this.client.request(`/thread/${id}`, {
api: 'labrinth',
version: 3,
method: 'POST',
body: message,
})
}
/**
* Delete a message from a thread (v3)
*
* @param messageId - Message ID
* @returns Promise resolving when message is deleted
*
* @example
* ```typescript
* await client.labrinth.threads_v3.deleteMessage('msg123')
* ```
*/
public async deleteMessage(messageId: string): Promise<void> {
return this.client.request(`/message/${messageId}`, {
api: 'labrinth',
version: 3,
method: 'DELETE',
})
}
}

View File

@@ -189,10 +189,28 @@ export namespace Labrinth {
url: string
}
export interface CreateProjectBase {
title: string
project_type: 'mod'
slug: string
description: string
body: string
requested_status: v2.ProjectStatus
initial_versions: unknown[]
team_members: any[]
categories: string[]
client_side: string
server_side: string
license_id: string
is_draft: boolean
organization_id?: string
}
export type Project = {
id: string
slug: string
project_type: ProjectType
actualProjectType: ProjectType
team: string
organization: string | null
title: string
@@ -280,6 +298,14 @@ export namespace Labrinth {
}
export namespace v3 {
export type ProjectType =
| 'mod'
| 'modpack'
| 'resourcepack'
| 'shader'
| 'plugin'
| 'datapack'
export type Environment =
| 'client_and_server'
| 'client_only'
@@ -311,7 +337,7 @@ export namespace Labrinth {
export type Project = {
id: string
slug?: string
project_types: string[]
project_types: ProjectType[]
games: string[]
team_id: string
organization?: string
@@ -344,12 +370,83 @@ export namespace Labrinth {
side_types_migration_review_status: 'reviewed' | 'pending'
environment?: Environment[]
minecraft_server?: MinecraftServer
minecraft_java_server?: MinecraftJavaServer
minecraft_bedrock_server?: MinecraftBedrockServer
/**
* @deprecated Not recommended to use.
**/
[key: string]: unknown
}
interface CreateProjectBase {
name: string // 3-64 chars
slug: string // 3-64 chars, URL-safe
summary: string // 3-255 chars
description: string // max 65536 chars, markdown
requested_status: v2.ProjectStatus
organization_id?: string // automatically transfer the project to this organization
}
export interface MinecraftJavaServerPing {
address: string
data?: {
description: string
latency: {
nanos: number
secs: number
}
players_max: number
players_online: number
version_name: string
version_protocol: number
}
port: number
when: string
}
export interface MinecraftServer {
max_players?: number
country?: string
active_version?: string | null
languages?: string[]
}
export interface ModpackContent {
kind: 'modpack'
version_id: string
project_id?: string
project_name?: string
project_icon?: string
}
export interface VanillaContent {
kind: 'vanilla'
supported_game_versions: string[]
recommended_game_version?: string
}
export interface MinecraftJavaServer {
address?: string
port?: number
content?: ModpackContent | VanillaContent
verified_plays_4w?: number | null
verified_plays_2w?: number | null
ping: Projects.v3.MinecraftJavaServerPing | null
}
export interface MinecraftBedrockServer {
address?: string
port?: number
}
export interface CreateServerProjectRequest {
base: CreateProjectBase
minecraft_server?: MinecraftServer
minecraft_java_server?: Omit<MinecraftJavaServer, 'ping'>
minecraft_bedrock_server?: MinecraftBedrockServer
}
export type EditProjectRequest = {
name?: string
summary?: string
@@ -367,6 +464,10 @@ export namespace Labrinth {
monetization_status?: v2.MonetizationStatus
side_types_migration_review_status?: 'reviewed' | 'pending'
environment?: Environment
minecraft_server?: MinecraftServer
minecraft_java_server?: MinecraftJavaServer
minecraft_bedrock_server?: MinecraftBedrockServer
[key: string]: unknown
}
@@ -404,6 +505,51 @@ export namespace Labrinth {
payouts_split: number | null
ordering: number
}
export type Team = {
id: string
members: TeamMember[]
}
export type ProjectDependencies = {
projects: Project[]
versions: Labrinth.Versions.v3.Version[]
}
}
}
export namespace Organizations {
export namespace v3 {
export type Organization = {
id: string
slug: string
name: string
team_id: string
description: string
icon_url: string | null
color: number | null
members: Projects.v3.TeamMember[]
}
export type CreateOrganizationRequest = {
slug: string
name: string
description: string
}
export type EditOrganizationRequest = {
description?: string
slug?: string
name?: string
}
export type AddProjectRequest = {
project_id: string
}
export type RemoveProjectRequest = {
new_owner: string
}
}
}
@@ -512,6 +658,13 @@ export namespace Labrinth {
file_type?: FileType
}
interface JavaServerVersion {
/**
* The version id of the modpack
*/
modpack: string
}
export interface Version {
name: string
version_number: string
@@ -530,6 +683,8 @@ export namespace Labrinth {
files: VersionFile[]
environment?: Labrinth.Projects.v3.Environment
mrpack_loaders?: string[]
minecraft_java_server?: JavaServerVersion
}
export interface DraftVersionFile {
@@ -658,6 +813,15 @@ export namespace Labrinth {
}
}
export namespace ServerPing {
export namespace Internal {
export type MinecraftJavaPingRequest = {
address: string
port: number
}
}
}
export namespace Tags {
export namespace v2 {
export interface Category {
@@ -720,6 +884,103 @@ export namespace Labrinth {
total_hits: number
}
}
export namespace v3 {
export interface ResultSearchProject {
version_id: string
project_id: string
project_types: string[]
slug: string | null
author: string
name: string
summary: string
categories: string[]
display_categories: string[]
downloads: number
follows: number
icon_url: string | null
date_created: string
date_modified: string
license: string
gallery: string[]
featured_gallery: string | null
color: number | null
loaders: string[]
project_loader_fields?: Record<string, unknown[]>
minecraft_server?: Projects.v3.MinecraftServer | null
minecraft_java_server?: Projects.v3.MinecraftJavaServer | null
minecraft_bedrock_server?: Projects.v3.MinecraftBedrockServer | null
minecraft_mod?: unknown | null
}
export interface SearchResults {
hits: ResultSearchProject[]
page: number
hits_per_page: number
total_hits: number
}
}
}
export namespace Threads {
export namespace v3 {
export type ThreadType = 'report' | 'project' | 'direct_message'
export type MessageBody =
| {
type: 'text'
body: string
private?: boolean
replying_to?: string
associated_images?: string[]
}
| {
type: 'status_change'
new_status: Projects.v2.ProjectStatus
old_status: Projects.v2.ProjectStatus
}
| {
type: 'thread_closure'
}
| {
type: 'thread_reopen'
}
| {
type: 'deleted'
private?: boolean
}
export type ThreadMessage = {
id: string | null
author_id: string | null
body: MessageBody
created: string
hide_identity: boolean
}
export type ThreadMember = {
id: string
username: string
avatar_url: string
role: string
badges: number
created: string
bio?: string
}
export type Thread = {
id: string
type: ThreadType
project_id: string | null
report_id: string | null
messages: ThreadMessage[]
members: ThreadMember[]
}
export type SendMessageRequest = {
body: MessageBody
}
}
}
export namespace Collections {

View File

@@ -1,4 +1,5 @@
MODRINTH_URL=http://localhost:3000/
MODRINTH_API_BASE_URL=http://localhost:8000/
MODRINTH_API_URL=http://127.0.0.1:8000/v2/
MODRINTH_API_URL_V3=http://127.0.0.1:8000/v3/
MODRINTH_SOCKET_URL=ws://127.0.0.1:8000/

View File

@@ -1,4 +1,5 @@
MODRINTH_URL=https://modrinth.com/
MODRINTH_API_BASE_URL=https://api.modrinth.com/
MODRINTH_API_URL=https://api.modrinth.com/v2/
MODRINTH_API_URL_V3=https://api.modrinth.com/v3/
MODRINTH_SOCKET_URL=wss://api.modrinth.com/

View File

@@ -1,4 +1,5 @@
MODRINTH_URL=https://staging.modrinth.com/
MODRINTH_API_BASE_URL=https://staging-api.modrinth.com/
MODRINTH_API_URL=https://staging-api.modrinth.com/v2/
MODRINTH_API_URL_V3=https://staging-api.modrinth.com/v3/
MODRINTH_SOCKET_URL=wss://staging-api.modrinth.com/

View File

@@ -1,6 +1,6 @@
use crate::state::{
CacheBehaviour, CacheValueType, CachedEntry, Organization, Project,
SearchResults, TeamMember, User, Version,
ProjectV3, SearchResults, SearchResultsV3, TeamMember, User, Version,
};
macro_rules! impl_cache_methods {
@@ -36,11 +36,13 @@ macro_rules! impl_cache_methods {
impl_cache_methods!(
(Project, Project),
(ProjectV3, ProjectV3),
(Version, Version),
(User, User),
(Team, Vec<TeamMember>),
(Organization, Organization),
(SearchResults, SearchResults)
(SearchResults, SearchResults),
(SearchResultsV3, SearchResultsV3)
);
pub async fn purge_cache_types(

View File

@@ -24,6 +24,10 @@ pub async fn handle_url(sublink: &str) -> crate::Result<CommandPayload> {
Some(("modpack", id)) => {
CommandPayload::InstallModpack { id: id.to_string() }
}
// /server/{id} - Opens a server project page and triggers play flow
Some(("server", id)) => {
CommandPayload::InstallServer { id: id.to_string() }
}
_ => {
emit_warning(&format!(
"Invalid command, unrecognized path: {sublink}"

View File

@@ -21,8 +21,9 @@ pub mod data {
CacheBehaviour, CacheValueType, Credentials, Dependency, DirectoryInfo,
Hooks, JavaVersion, LinkedData, MemorySettings, ModLoader,
ModrinthCredentials, Organization, ProcessMetadata, ProfileFile,
Project, ProjectType, SearchResult, SearchResults, Settings,
TeamMember, Theme, User, UserFriend, Version, WindowSize,
Project, ProjectType, ProjectV3, SearchResult, SearchResults,
SearchResultsV3, Settings, TeamMember, Theme, User, UserFriend,
Version, WindowSize,
};
pub use ariadne::users::UserStatus;
}

View File

@@ -2,7 +2,9 @@ use crate::State;
use crate::data::ModLoader;
use crate::event::emit::{emit_loading, init_loading};
use crate::event::{LoadingBarId, LoadingBarType};
use crate::state::{CachedEntry, LinkedData, ProfileInstallStage, SideType};
use crate::state::{
CacheBehaviour, CachedEntry, LinkedData, ProfileInstallStage, SideType,
};
use crate::util::fetch::{fetch, fetch_advanced, write_cached_icon};
use crate::util::io;
@@ -190,6 +192,7 @@ pub async fn generate_pack_from_version_id(
initialized_loading_bar: Option<LoadingBarId>,
) -> crate::Result<CreatePack> {
let state = State::get().await?;
let has_icon_url = icon_url.is_some();
let loading_bar = if let Some(bar) = initialized_loading_bar {
emit_loading(&bar, 0.0, Some("Downloading pack file"))?;
@@ -198,7 +201,7 @@ pub async fn generate_pack_from_version_id(
init_loading(
LoadingBarType::PackFileDownload {
profile_path: profile_path.clone(),
pack_name: title,
pack_name: title.clone(),
icon: icon_url,
pack_version: version_id.clone(),
},
@@ -211,7 +214,7 @@ pub async fn generate_pack_from_version_id(
emit_loading(&loading_bar, 0.0, Some("Fetching version"))?;
let version = CachedEntry::get_version(
&version_id,
None,
Some(CacheBehaviour::Bypass),
&state.pool,
&state.api_semaphore,
)
@@ -264,37 +267,47 @@ pub async fn generate_pack_from_version_id(
)
})?;
emit_loading(&loading_bar, 10.0, Some("Retrieving icon"))?;
let icon = if let Some(icon_url) = project.icon_url {
let state = State::get().await?;
let icon_bytes =
fetch(&icon_url, None, &state.fetch_semaphore, &state.pool).await?;
// Only fetch the pack icon when icon_url is provided (new profile).
// When installing to an existing profile (e.g. server projects),
// icon_url is None and we preserve the profile's existing icon.
let icon = if has_icon_url {
emit_loading(&loading_bar, 10.0, Some("Retrieving icon"))?;
let fetched = if let Some(icon_url) = project.icon_url {
let state = State::get().await?;
let icon_bytes =
fetch(&icon_url, None, &state.fetch_semaphore, &state.pool)
.await?;
let filename = icon_url.rsplit('/').next();
let filename = icon_url.rsplit('/').next();
if let Some(filename) = filename {
Some(
write_cached_icon(
filename,
&state.directories.caches_dir(),
icon_bytes,
&state.io_semaphore,
if let Some(filename) = filename {
Some(
write_cached_icon(
filename,
&state.directories.caches_dir(),
icon_bytes,
&state.io_semaphore,
)
.await?,
)
.await?,
)
} else {
None
}
} else {
None
}
};
emit_loading(&loading_bar, 10.0, None)?;
fetched
} else {
emit_loading(&loading_bar, 20.0, None)?;
None
};
emit_loading(&loading_bar, 10.0, None)?;
Ok(CreatePack {
file,
description: CreatePackDescription {
icon,
override_title: None,
override_title: Some(title),
project_id: Some(project_id),
version_id: Some(version_id),
existing_loading_bar: Some(loading_bar),
@@ -398,10 +411,12 @@ pub async fn set_profile_information(
})
}
prof.icon_path = description
.icon
.clone()
.map(|x| x.to_string_lossy().to_string());
// Only update the icon if the pack provides one.
// When installing to an existing profile, icon is None
// and we preserve the profile's existing icon.
if let Some(ref icon) = description.icon {
prof.icon_path = Some(icon.to_string_lossy().to_string());
}
prof.game_version.clone_from(game_version);
prof.loader_version = loader_version.clone().map(|x| x.id);
prof.loader = mod_loader;

View File

@@ -103,14 +103,30 @@ pub async fn profile_create(
let result = async {
if let Some(ref icon) = icon_path {
let bytes =
io::read(state.directories.caches_dir().join(icon)).await?;
let (bytes, file_name) = if icon.starts_with("https://")
|| icon.starts_with("http://")
{
let fetched = crate::util::fetch::fetch(
icon,
None,
&state.fetch_semaphore,
&state.pool,
)
.await?;
let name =
icon.rsplit('/').next().unwrap_or("icon").to_string();
(fetched, name)
} else {
let data =
io::read(state.directories.caches_dir().join(icon)).await?;
(bytes::Bytes::from(data), icon.clone())
};
profile
.set_icon(
&state.directories.caches_dir(),
&state.io_semaphore,
bytes::Bytes::from(bytes),
icon,
bytes,
&file_name,
)
.await?;
}

View File

@@ -20,6 +20,7 @@ use async_zip::tokio::write::ZipFileWriter;
use async_zip::{Compression, ZipEntryBuilder};
use path_util::SafeRelativeUtf8UnixPathBuf;
use serde_json::json;
use tracing::{info, warn};
use std::collections::{HashMap, HashSet};
@@ -734,6 +735,33 @@ async fn run_credentials(
mc_set_options.push(("fullscreen".to_string(), "true".to_string()));
}
// For server projects: track this play in analytics
if let Some(linked_data) = &profile.linked_data {
let project_id = &linked_data.project_id;
if !project_id.trim().is_empty() {
let result = fetch::post_json(
concat!(
env!("MODRINTH_API_BASE_URL"),
"analytics/minecraft-server-play"
),
json!({
"project_id": &linked_data.project_id,
"minecraft_uuid": credentials.offline_profile.id,
}),
&state.api_semaphore,
&state.pool,
)
.await;
match result {
Ok(()) => {
info!("Tracked server play for '{project_id}' in analytics")
}
Err(err) => warn!("Failed to report server play: {err:?}"),
}
}
}
crate::launcher::launch_minecraft(
&java_args,
&env_args,
@@ -796,7 +824,7 @@ pub async fn try_update_playtime(path: &str) -> crate::Result<()> {
}
fetch::post_json(
"https://api.modrinth.com/analytics/playtime",
concat!(env!("MODRINTH_API_BASE_URL"), "analytics/playtime"),
serde_json::to_value(hashmap)?,
&state.api_semaphore,
&state.pool,

View File

@@ -139,6 +139,8 @@ pub enum WorldDetails {
index: usize,
address: String,
pack_status: ServerPackStatus,
#[serde(skip_serializing_if = "Option::is_none")]
linked_project_id: Option<String>,
},
}
@@ -423,6 +425,7 @@ async fn get_server_worlds_in_profile(
index,
address: server.ip,
pack_status: server.accept_textures.into(),
linked_project_id: server.linked_project_id,
},
};
worlds.push(world);
@@ -712,6 +715,7 @@ pub async fn add_server_to_profile(
name: String,
address: String,
pack_status: ServerPackStatus,
linked_project_id: Option<String>,
) -> Result<usize> {
let mut servers = servers_data::read(profile_path).await?;
let insert_index = servers
@@ -726,6 +730,7 @@ pub async fn add_server_to_profile(
accept_textures: pack_status.into(),
hidden: false,
icon: None,
linked_project_id,
},
);
servers_data::write(profile_path, &servers).await?;
@@ -738,6 +743,7 @@ pub async fn edit_server_in_profile(
name: String,
address: String,
pack_status: ServerPackStatus,
linked_project_id: Option<String>,
) -> Result<()> {
let mut servers = servers_data::read(profile_path).await?;
let server =
@@ -753,6 +759,9 @@ pub async fn edit_server_in_profile(
server.name = name;
server.ip = address;
server.accept_textures = pack_status.into();
if let Some(id) = linked_project_id {
server.linked_project_id = Some(id);
}
servers_data::write(profile_path, &servers).await?;
Ok(())
}
@@ -792,6 +801,8 @@ mod servers_data {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub accept_textures: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub linked_project_id: Option<String>,
}
pub async fn read(instance_dir: &Path) -> Result<Vec<ServerData>> {

View File

@@ -209,6 +209,9 @@ pub enum CommandPayload {
InstallModpack {
id: String,
},
InstallServer {
id: String,
},
RunMRPack {
// run or install .mrpack
path: PathBuf,

View File

@@ -19,6 +19,7 @@ const DEFAULT_ID: &str = "0";
#[serde(rename_all = "snake_case")]
pub enum CacheValueType {
Project,
ProjectV3,
Version,
User,
Team,
@@ -34,12 +35,14 @@ pub enum CacheValueType {
FileHash,
FileUpdate,
SearchResults,
SearchResultsV3,
}
impl CacheValueType {
pub fn as_str(&self) -> &'static str {
match self {
CacheValueType::Project => "project",
CacheValueType::ProjectV3 => "project_v3",
CacheValueType::Version => "version",
CacheValueType::User => "user",
CacheValueType::Team => "team",
@@ -55,12 +58,14 @@ impl CacheValueType {
CacheValueType::FileHash => "file_hash",
CacheValueType::FileUpdate => "file_update",
CacheValueType::SearchResults => "search_results",
CacheValueType::SearchResultsV3 => "search_results_v3",
}
}
pub fn from_string(val: &str) -> CacheValueType {
match val {
"project" => CacheValueType::Project,
"project_v3" => CacheValueType::ProjectV3,
"version" => CacheValueType::Version,
"user" => CacheValueType::User,
"team" => CacheValueType::Team,
@@ -76,6 +81,7 @@ impl CacheValueType {
"file_hash" => CacheValueType::FileHash,
"file_update" => CacheValueType::FileUpdate,
"search_results" => CacheValueType::SearchResults,
"search_results_v3" => CacheValueType::SearchResultsV3,
_ => CacheValueType::Project,
}
}
@@ -102,6 +108,7 @@ impl CacheValueType {
pub fn case_sensitive_alias(&self) -> Option<bool> {
match self {
CacheValueType::Project
| CacheValueType::ProjectV3
| CacheValueType::User
| CacheValueType::Organization => Some(false),
@@ -118,7 +125,8 @@ impl CacheValueType {
| CacheValueType::File
| CacheValueType::LoaderManifest
| CacheValueType::FileUpdate
| CacheValueType::SearchResults => None,
| CacheValueType::SearchResults
| CacheValueType::SearchResultsV3 => None,
}
}
}
@@ -129,6 +137,8 @@ impl CacheValueType {
pub enum CacheValue {
Project(Project),
ProjectV3(ProjectV3),
Version(Version),
User(User),
@@ -151,6 +161,7 @@ pub enum CacheValue {
FileHash(CachedFileHash),
FileUpdate(CachedFileUpdate),
SearchResults(SearchResults),
SearchResultsV3(SearchResultsV3),
}
#[derive(Serialize, Deserialize, Clone, Debug)]
@@ -192,6 +203,23 @@ pub struct SearchEntry {
pub color: Option<u32>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SearchResultsV3 {
pub search: String,
pub result: SearchResultV3,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SearchResultV3 {
pub hits: Vec<serde_json::Value>,
#[serde(default)]
pub offset: u32,
#[serde(default)]
pub limit: u32,
#[serde(default)]
pub total_hits: u32,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct CachedFileUpdate {
pub hash: String,
@@ -264,6 +292,15 @@ pub struct Project {
pub color: Option<u32>,
}
/// Uses serde_json::Value for flexibility since the v3. properly typed in frontend
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ProjectV3 {
pub id: String,
pub slug: Option<String>,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct License {
pub id: String,
@@ -437,6 +474,7 @@ impl CacheValue {
pub fn get_type(&self) -> CacheValueType {
match self {
CacheValue::Project(_) => CacheValueType::Project,
CacheValue::ProjectV3(_) => CacheValueType::ProjectV3,
CacheValue::Version(_) => CacheValueType::Version,
CacheValue::User(_) => CacheValueType::User,
CacheValue::Team { .. } => CacheValueType::Team,
@@ -456,12 +494,14 @@ impl CacheValue {
CacheValue::FileHash(_) => CacheValueType::FileHash,
CacheValue::FileUpdate(_) => CacheValueType::FileUpdate,
CacheValue::SearchResults(_) => CacheValueType::SearchResults,
CacheValue::SearchResultsV3(_) => CacheValueType::SearchResultsV3,
}
}
fn get_key(&self) -> String {
match self {
CacheValue::Project(project) => project.id.clone(),
CacheValue::ProjectV3(project) => project.id.clone(),
CacheValue::Version(version) => version.id.clone(),
CacheValue::User(user) => user.id.clone(),
CacheValue::Team(members) => members
@@ -496,12 +536,14 @@ impl CacheValue {
)
}
CacheValue::SearchResults(search) => search.search.clone(),
CacheValue::SearchResultsV3(search) => search.search.clone(),
}
}
fn get_alias(&self) -> Option<String> {
match self {
CacheValue::Project(project) => project.slug.clone(),
CacheValue::ProjectV3(project) => project.slug.clone(),
CacheValue::User(user) => Some(user.username.clone()),
CacheValue::Organization(org) => Some(org.slug.clone()),
@@ -520,7 +562,8 @@ impl CacheValue {
| CacheValue::File { .. }
| CacheValue::LoaderManifest { .. }
| CacheValue::FileUpdate(_)
| CacheValue::SearchResults(_) => None,
| CacheValue::SearchResults(_)
| CacheValue::SearchResultsV3(_) => None,
}
}
}
@@ -620,6 +663,7 @@ macro_rules! impl_cache_method_singular {
impl_cache_methods!(
(Project, Project),
(ProjectV3, ProjectV3),
(Version, Version),
(User, User),
(Team, Vec<TeamMember>),
@@ -628,7 +672,8 @@ impl_cache_methods!(
(LoaderManifest, CachedLoaderManifest),
(FileHash, CachedFileHash),
(FileUpdate, CachedFileUpdate),
(SearchResults, SearchResults)
(SearchResults, SearchResults),
(SearchResultsV3, SearchResultsV3)
);
impl_cache_method_singular!(
@@ -953,6 +998,14 @@ impl CachedEntry {
CacheValue::Project
)
}
CacheValueType::ProjectV3 => {
fetch_original_values!(
ProjectV3,
env!("MODRINTH_API_URL_V3"),
"projects",
CacheValue::ProjectV3
)
}
CacheValueType::Version => {
fetch_original_values!(
Version,
@@ -1411,6 +1464,48 @@ impl CachedEntry {
})
.collect()
}
CacheValueType::SearchResultsV3 => {
let fetch_urls = keys
.iter()
.map(|x| {
(
x.key().to_string(),
format!(
"{}search{}",
env!("MODRINTH_API_URL_V3"),
x.key()
),
)
})
.collect::<Vec<_>>();
futures::future::try_join_all(fetch_urls.iter().map(
|(_, url)| {
fetch_json(
Method::GET,
url,
None,
None,
fetch_semaphore,
pool,
)
},
))
.await?
.into_iter()
.enumerate()
.map(|(index, result)| {
(
CacheValue::SearchResultsV3(SearchResultsV3 {
search: fetch_urls[index].0.to_string(),
result,
})
.get_entry(),
true,
)
})
.collect()
}
})
}

View File

@@ -340,15 +340,12 @@ pub async fn fetch_mirrors(
/// Posts a JSON to a URL
#[tracing::instrument(skip(json_body, semaphore))]
pub async fn post_json<T>(
pub async fn post_json(
url: &str,
json_body: serde_json::Value,
semaphore: &FetchSemaphore,
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
) -> crate::Result<T>
where
T: DeserializeOwned,
{
) -> crate::Result<()> {
let _permit = semaphore.0.acquire().await?;
let mut req = REQWEST_CLIENT.post(url).json(&json_body);
@@ -359,10 +356,8 @@ where
req = req.header("Authorization", &creds.session);
}
let result = req.send().await?.error_for_status()?;
let value = result.json().await?;
Ok(value)
req.send().await?.error_for_status()?;
Ok(())
}
pub async fn read_json<T>(

View File

@@ -58,6 +58,7 @@ import _CoinsIcon from './icons/coins.svg?component'
import _CollapseIcon from './icons/collapse.svg?component'
import _CollectionIcon from './icons/collection.svg?component'
import _CompassIcon from './icons/compass.svg?component'
import _ComponentIcon from './icons/component.svg?component'
import _ContractIcon from './icons/contract.svg?component'
import _CopyIcon from './icons/copy.svg?component'
import _CopyrightIcon from './icons/copyright.svg?component'
@@ -112,6 +113,7 @@ import _HeartMinusIcon from './icons/heart-minus.svg?component'
import _HistoryIcon from './icons/history.svg?component'
import _HomeIcon from './icons/home.svg?component'
import _ImageIcon from './icons/image.svg?component'
import _ImagesIcon from './icons/images.svg?component'
import _ImportIcon from './icons/import.svg?component'
import _InProgressIcon from './icons/in-progress.svg?component'
import _InfoIcon from './icons/info.svg?component'
@@ -155,16 +157,20 @@ import _NewspaperIcon from './icons/newspaper.svg?component'
import _NoSignalIcon from './icons/no-signal.svg?component'
import _NotepadTextIcon from './icons/notepad-text.svg?component'
import _OmorphiaIcon from './icons/omorphia.svg?component'
import _OnlineIndicatorIcon from './icons/online-indicator.svg?component'
import _OrganizationIcon from './icons/organization.svg?component'
import _PackageIcon from './icons/package.svg?component'
import _PackageClosedIcon from './icons/package-closed.svg?component'
import _PackageOpenIcon from './icons/package-open.svg?component'
import _PackagePlusIcon from './icons/package-plus.svg?component'
import _PaintbrushIcon from './icons/paintbrush.svg?component'
import _PaletteIcon from './icons/palette.svg?component'
import _PickaxeIcon from './icons/pickaxe.svg?component'
import _PlayIcon from './icons/play.svg?component'
import _PlugIcon from './icons/plug.svg?component'
import _PlusIcon from './icons/plus.svg?component'
import _PowerIcon from './icons/power.svg?component'
import _PowerOffIcon from './icons/power-off.svg?component'
import _RadioButtonIcon from './icons/radio-button.svg?component'
import _RadioButtonCheckedIcon from './icons/radio-button-checked.svg?component'
import _ReceiptTextIcon from './icons/receipt-text.svg?component'
@@ -199,6 +205,7 @@ import _SparklesIcon from './icons/sparkles.svg?component'
import _SpinnerIcon from './icons/spinner.svg?component'
import _StarIcon from './icons/star.svg?component'
import _StopCircleIcon from './icons/stop-circle.svg?component'
import _StoreIcon from './icons/store.svg?component'
import _StrikethroughIcon from './icons/strikethrough.svg?component'
import _SunIcon from './icons/sun.svg?component'
import _SunriseIcon from './icons/sunrise.svg?component'
@@ -208,60 +215,105 @@ import _TagsIcon from './icons/tags.svg?component'
import _TagCategoryAdventureIcon from './icons/tags/categories/adventure.svg?component'
import _TagCategoryAtmosphereIcon from './icons/tags/categories/atmosphere.svg?component'
import _TagCategoryAudioIcon from './icons/tags/categories/audio.svg?component'
import _TagCategoryBackpackIcon from './icons/tags/categories/backpack.svg?component'
import _TagCategoryBadgeIcon from './icons/tags/categories/badge.svg?component'
import _TagCategoryBadgeCheckIcon from './icons/tags/categories/badge-check.svg?component'
import _TagCategoryBedDoubleIcon from './icons/tags/categories/bed-double.svg?component'
import _TagCategoryBlocksIcon from './icons/tags/categories/blocks.svg?component'
import _TagCategoryBloomIcon from './icons/tags/categories/bloom.svg?component'
import _TagCategoryBuilding2Icon from './icons/tags/categories/building-2.svg?component'
import _TagCategoryCameraIcon from './icons/tags/categories/camera.svg?component'
import _TagCategoryCartoonIcon from './icons/tags/categories/cartoon.svg?component'
import _TagCategoryCastleIcon from './icons/tags/categories/castle.svg?component'
import _TagCategoryChallengingIcon from './icons/tags/categories/challenging.svg?component'
import _TagCategoryClapperboardIcon from './icons/tags/categories/clapperboard.svg?component'
import _TagCategoryCloudIcon from './icons/tags/categories/cloud.svg?component'
import _TagCategoryColoredLightingIcon from './icons/tags/categories/colored-lighting.svg?component'
import _TagCategoryCombatIcon from './icons/tags/categories/combat.svg?component'
import _TagCategoryCompassIcon from './icons/tags/categories/compass.svg?component'
import _TagCategoryCoreShadersIcon from './icons/tags/categories/core-shaders.svg?component'
import _TagCategoryCrownIcon from './icons/tags/categories/crown.svg?component'
import _TagCategoryCursedIcon from './icons/tags/categories/cursed.svg?component'
import _TagCategoryDecorationIcon from './icons/tags/categories/decoration.svg?component'
import _TagCategoryDicesIcon from './icons/tags/categories/dices.svg?component'
import _TagCategoryEconomyIcon from './icons/tags/categories/economy.svg?component'
import _TagCategoryEntitiesIcon from './icons/tags/categories/entities.svg?component'
import _TagCategoryEnvironmentIcon from './icons/tags/categories/environment.svg?component'
import _TagCategoryEquipmentIcon from './icons/tags/categories/equipment.svg?component'
import _TagCategoryFantasyIcon from './icons/tags/categories/fantasy.svg?component'
import _TagCategoryFilmIcon from './icons/tags/categories/film.svg?component'
import _TagCategoryFlagIcon from './icons/tags/categories/flag.svg?component'
import _TagCategoryFoliageIcon from './icons/tags/categories/foliage.svg?component'
import _TagCategoryFontsIcon from './icons/tags/categories/fonts.svg?component'
import _TagCategoryFoodIcon from './icons/tags/categories/food.svg?component'
import _TagCategoryFootprintsIcon from './icons/tags/categories/footprints.svg?component'
import _TagCategoryGameMechanicsIcon from './icons/tags/categories/game-mechanics.svg?component'
import _TagCategoryGamepad2Icon from './icons/tags/categories/gamepad-2.svg?component'
import _TagCategoryGaugeIcon from './icons/tags/categories/gauge.svg?component'
import _TagCategoryGlobeIcon from './icons/tags/categories/globe.svg?component'
import _TagCategoryGrid3x3Icon from './icons/tags/categories/grid-3x3.svg?component'
import _TagCategoryGuiIcon from './icons/tags/categories/gui.svg?component'
import _TagCategoryHandshakeIcon from './icons/tags/categories/handshake.svg?component'
import _TagCategoryHeartCrackIcon from './icons/tags/categories/heart-crack.svg?component'
import _TagCategoryHeartPulseIcon from './icons/tags/categories/heart-pulse.svg?component'
import _TagCategoryHighIcon from './icons/tags/categories/high.svg?component'
import _TagCategoryHouseIcon from './icons/tags/categories/house.svg?component'
import _TagCategoryItemsIcon from './icons/tags/categories/items.svg?component'
import _TagCategoryKitchenSinkIcon from './icons/tags/categories/kitchen-sink.svg?component'
import _TagCategoryLibraryIcon from './icons/tags/categories/library.svg?component'
import _TagCategoryLightweightIcon from './icons/tags/categories/lightweight.svg?component'
import _TagCategoryLocaleIcon from './icons/tags/categories/locale.svg?component'
import _TagCategoryLockIcon from './icons/tags/categories/lock.svg?component'
import _TagCategoryLowIcon from './icons/tags/categories/low.svg?component'
import _TagCategoryMagicIcon from './icons/tags/categories/magic.svg?component'
import _TagCategoryManagementIcon from './icons/tags/categories/management.svg?component'
import _TagCategoryMapPinnedIcon from './icons/tags/categories/map-pinned.svg?component'
import _TagCategoryMediumIcon from './icons/tags/categories/medium.svg?component'
import _TagCategoryMinigameIcon from './icons/tags/categories/minigame.svg?component'
import _TagCategoryMobsIcon from './icons/tags/categories/mobs.svg?component'
import _TagCategoryModdedIcon from './icons/tags/categories/modded.svg?component'
import _TagCategoryModelsIcon from './icons/tags/categories/models.svg?component'
import _TagCategoryMultiplayerIcon from './icons/tags/categories/multiplayer.svg?component'
import _TagCategoryNetworkIcon from './icons/tags/categories/network.svg?component'
import _TagCategoryOptimizationIcon from './icons/tags/categories/optimization.svg?component'
import _TagCategoryPaletteIcon from './icons/tags/categories/palette.svg?component'
import _TagCategoryPathTracingIcon from './icons/tags/categories/path-tracing.svg?component'
import _TagCategoryPawPrintIcon from './icons/tags/categories/paw-print.svg?component'
import _TagCategoryPbrIcon from './icons/tags/categories/pbr.svg?component'
import _TagCategoryPickaxeIcon from './icons/tags/categories/pickaxe.svg?component'
import _TagCategoryPotatoIcon from './icons/tags/categories/potato.svg?component'
import _TagCategoryQuestsIcon from './icons/tags/categories/quests.svg?component'
import _TagCategoryRealisticIcon from './icons/tags/categories/realistic.svg?component'
import _TagCategoryReflectionsIcon from './icons/tags/categories/reflections.svg?component'
import _TagCategoryRefreshCcwIcon from './icons/tags/categories/refresh-ccw.svg?component'
import _TagCategoryScreenshotIcon from './icons/tags/categories/screenshot.svg?component'
import _TagCategoryScrollTextIcon from './icons/tags/categories/scroll-text.svg?component'
import _TagCategorySemiRealisticIcon from './icons/tags/categories/semi-realistic.svg?component'
import _TagCategoryShadowsIcon from './icons/tags/categories/shadows.svg?component'
import _TagCategoryShieldIcon from './icons/tags/categories/shield.svg?component'
import _TagCategorySimplisticIcon from './icons/tags/categories/simplistic.svg?component'
import _TagCategorySkullIcon from './icons/tags/categories/skull.svg?component'
import _TagCategorySocialIcon from './icons/tags/categories/social.svg?component'
import _TagCategorySquareIcon from './icons/tags/categories/square.svg?component'
import _TagCategoryStorageIcon from './icons/tags/categories/storage.svg?component'
import _TagCategorySwordIcon from './icons/tags/categories/sword.svg?component'
import _TagCategorySwordsIcon from './icons/tags/categories/swords.svg?component'
import _TagCategoryTargetIcon from './icons/tags/categories/target.svg?component'
import _TagCategoryTechnologyIcon from './icons/tags/categories/technology.svg?component'
import _TagCategoryTerminalIcon from './icons/tags/categories/terminal.svg?component'
import _TagCategoryTheaterIcon from './icons/tags/categories/theater.svg?component'
import _TagCategoryThemedIcon from './icons/tags/categories/themed.svg?component'
import _TagCategoryTransportationIcon from './icons/tags/categories/transportation.svg?component'
import _TagCategoryTreePineIcon from './icons/tags/categories/tree-pine.svg?component'
import _TagCategoryTrophyIcon from './icons/tags/categories/trophy.svg?component'
import _TagCategoryTweaksIcon from './icons/tags/categories/tweaks.svg?component'
import _TagCategoryUsersIcon from './icons/tags/categories/users.svg?component'
import _TagCategoryUtilityIcon from './icons/tags/categories/utility.svg?component'
import _TagCategoryVanillaLikeIcon from './icons/tags/categories/vanilla-like.svg?component'
import _TagCategoryWandSparklesIcon from './icons/tags/categories/wand-sparkles.svg?component'
import _TagCategoryWifiOffIcon from './icons/tags/categories/wifi-off.svg?component'
import _TagCategoryWorldgenIcon from './icons/tags/categories/worldgen.svg?component'
import _TagCategoryZapIcon from './icons/tags/categories/zap.svg?component'
import _TagLoaderBabricIcon from './icons/tags/loaders/babric.svg?component'
import _TagLoaderBtaBabricIcon from './icons/tags/loaders/bta-babric.svg?component'
import _TagLoaderBukkitIcon from './icons/tags/loaders/bukkit.svg?component'
@@ -383,6 +435,7 @@ export const CoinsIcon = _CoinsIcon
export const CollapseIcon = _CollapseIcon
export const CollectionIcon = _CollectionIcon
export const CompassIcon = _CompassIcon
export const ComponentIcon = _ComponentIcon
export const ContractIcon = _ContractIcon
export const CopyIcon = _CopyIcon
export const CopyrightIcon = _CopyrightIcon
@@ -437,6 +490,7 @@ export const HeartMinusIcon = _HeartMinusIcon
export const HistoryIcon = _HistoryIcon
export const HomeIcon = _HomeIcon
export const ImageIcon = _ImageIcon
export const ImagesIcon = _ImagesIcon
export const ImportIcon = _ImportIcon
export const InProgressIcon = _InProgressIcon
export const InfoIcon = _InfoIcon
@@ -480,16 +534,20 @@ export const NewspaperIcon = _NewspaperIcon
export const NoSignalIcon = _NoSignalIcon
export const NotepadTextIcon = _NotepadTextIcon
export const OmorphiaIcon = _OmorphiaIcon
export const OnlineIndicatorIcon = _OnlineIndicatorIcon
export const OrganizationIcon = _OrganizationIcon
export const PackageIcon = _PackageIcon
export const PackageClosedIcon = _PackageClosedIcon
export const PackageOpenIcon = _PackageOpenIcon
export const PackagePlusIcon = _PackagePlusIcon
export const PaintbrushIcon = _PaintbrushIcon
export const PaletteIcon = _PaletteIcon
export const PickaxeIcon = _PickaxeIcon
export const PlayIcon = _PlayIcon
export const PlugIcon = _PlugIcon
export const PlusIcon = _PlusIcon
export const PowerIcon = _PowerIcon
export const PowerOffIcon = _PowerOffIcon
export const RadioButtonIcon = _RadioButtonIcon
export const RadioButtonCheckedIcon = _RadioButtonCheckedIcon
export const ReceiptTextIcon = _ReceiptTextIcon
@@ -524,6 +582,7 @@ export const SparklesIcon = _SparklesIcon
export const SpinnerIcon = _SpinnerIcon
export const StarIcon = _StarIcon
export const StopCircleIcon = _StopCircleIcon
export const StoreIcon = _StoreIcon
export const StrikethroughIcon = _StrikethroughIcon
export const SunIcon = _SunIcon
export const SunriseIcon = _SunriseIcon
@@ -533,60 +592,105 @@ export const TagsIcon = _TagsIcon
export const TagCategoryAdventureIcon = _TagCategoryAdventureIcon
export const TagCategoryAtmosphereIcon = _TagCategoryAtmosphereIcon
export const TagCategoryAudioIcon = _TagCategoryAudioIcon
export const TagCategoryBackpackIcon = _TagCategoryBackpackIcon
export const TagCategoryBadgeIcon = _TagCategoryBadgeIcon
export const TagCategoryBadgeCheckIcon = _TagCategoryBadgeCheckIcon
export const TagCategoryBedDoubleIcon = _TagCategoryBedDoubleIcon
export const TagCategoryBlocksIcon = _TagCategoryBlocksIcon
export const TagCategoryBloomIcon = _TagCategoryBloomIcon
export const TagCategoryBuilding2Icon = _TagCategoryBuilding2Icon
export const TagCategoryCameraIcon = _TagCategoryCameraIcon
export const TagCategoryCartoonIcon = _TagCategoryCartoonIcon
export const TagCategoryCastleIcon = _TagCategoryCastleIcon
export const TagCategoryChallengingIcon = _TagCategoryChallengingIcon
export const TagCategoryClapperboardIcon = _TagCategoryClapperboardIcon
export const TagCategoryCloudIcon = _TagCategoryCloudIcon
export const TagCategoryColoredLightingIcon = _TagCategoryColoredLightingIcon
export const TagCategoryCombatIcon = _TagCategoryCombatIcon
export const TagCategoryCompassIcon = _TagCategoryCompassIcon
export const TagCategoryCoreShadersIcon = _TagCategoryCoreShadersIcon
export const TagCategoryCrownIcon = _TagCategoryCrownIcon
export const TagCategoryCursedIcon = _TagCategoryCursedIcon
export const TagCategoryDecorationIcon = _TagCategoryDecorationIcon
export const TagCategoryDicesIcon = _TagCategoryDicesIcon
export const TagCategoryEconomyIcon = _TagCategoryEconomyIcon
export const TagCategoryEntitiesIcon = _TagCategoryEntitiesIcon
export const TagCategoryEnvironmentIcon = _TagCategoryEnvironmentIcon
export const TagCategoryEquipmentIcon = _TagCategoryEquipmentIcon
export const TagCategoryFantasyIcon = _TagCategoryFantasyIcon
export const TagCategoryFilmIcon = _TagCategoryFilmIcon
export const TagCategoryFlagIcon = _TagCategoryFlagIcon
export const TagCategoryFoliageIcon = _TagCategoryFoliageIcon
export const TagCategoryFontsIcon = _TagCategoryFontsIcon
export const TagCategoryFoodIcon = _TagCategoryFoodIcon
export const TagCategoryFootprintsIcon = _TagCategoryFootprintsIcon
export const TagCategoryGameMechanicsIcon = _TagCategoryGameMechanicsIcon
export const TagCategoryGamepad2Icon = _TagCategoryGamepad2Icon
export const TagCategoryGaugeIcon = _TagCategoryGaugeIcon
export const TagCategoryGlobeIcon = _TagCategoryGlobeIcon
export const TagCategoryGrid3x3Icon = _TagCategoryGrid3x3Icon
export const TagCategoryGuiIcon = _TagCategoryGuiIcon
export const TagCategoryHandshakeIcon = _TagCategoryHandshakeIcon
export const TagCategoryHeartCrackIcon = _TagCategoryHeartCrackIcon
export const TagCategoryHeartPulseIcon = _TagCategoryHeartPulseIcon
export const TagCategoryHighIcon = _TagCategoryHighIcon
export const TagCategoryHouseIcon = _TagCategoryHouseIcon
export const TagCategoryItemsIcon = _TagCategoryItemsIcon
export const TagCategoryKitchenSinkIcon = _TagCategoryKitchenSinkIcon
export const TagCategoryLibraryIcon = _TagCategoryLibraryIcon
export const TagCategoryLightweightIcon = _TagCategoryLightweightIcon
export const TagCategoryLocaleIcon = _TagCategoryLocaleIcon
export const TagCategoryLockIcon = _TagCategoryLockIcon
export const TagCategoryLowIcon = _TagCategoryLowIcon
export const TagCategoryMagicIcon = _TagCategoryMagicIcon
export const TagCategoryManagementIcon = _TagCategoryManagementIcon
export const TagCategoryMapPinnedIcon = _TagCategoryMapPinnedIcon
export const TagCategoryMediumIcon = _TagCategoryMediumIcon
export const TagCategoryMinigameIcon = _TagCategoryMinigameIcon
export const TagCategoryMobsIcon = _TagCategoryMobsIcon
export const TagCategoryModdedIcon = _TagCategoryModdedIcon
export const TagCategoryModelsIcon = _TagCategoryModelsIcon
export const TagCategoryMultiplayerIcon = _TagCategoryMultiplayerIcon
export const TagCategoryNetworkIcon = _TagCategoryNetworkIcon
export const TagCategoryOptimizationIcon = _TagCategoryOptimizationIcon
export const TagCategoryPaletteIcon = _TagCategoryPaletteIcon
export const TagCategoryPathTracingIcon = _TagCategoryPathTracingIcon
export const TagCategoryPawPrintIcon = _TagCategoryPawPrintIcon
export const TagCategoryPbrIcon = _TagCategoryPbrIcon
export const TagCategoryPickaxeIcon = _TagCategoryPickaxeIcon
export const TagCategoryPotatoIcon = _TagCategoryPotatoIcon
export const TagCategoryQuestsIcon = _TagCategoryQuestsIcon
export const TagCategoryRealisticIcon = _TagCategoryRealisticIcon
export const TagCategoryReflectionsIcon = _TagCategoryReflectionsIcon
export const TagCategoryRefreshCcwIcon = _TagCategoryRefreshCcwIcon
export const TagCategoryScreenshotIcon = _TagCategoryScreenshotIcon
export const TagCategoryScrollTextIcon = _TagCategoryScrollTextIcon
export const TagCategorySemiRealisticIcon = _TagCategorySemiRealisticIcon
export const TagCategoryShadowsIcon = _TagCategoryShadowsIcon
export const TagCategoryShieldIcon = _TagCategoryShieldIcon
export const TagCategorySimplisticIcon = _TagCategorySimplisticIcon
export const TagCategorySkullIcon = _TagCategorySkullIcon
export const TagCategorySocialIcon = _TagCategorySocialIcon
export const TagCategorySquareIcon = _TagCategorySquareIcon
export const TagCategoryStorageIcon = _TagCategoryStorageIcon
export const TagCategorySwordIcon = _TagCategorySwordIcon
export const TagCategorySwordsIcon = _TagCategorySwordsIcon
export const TagCategoryTargetIcon = _TagCategoryTargetIcon
export const TagCategoryTechnologyIcon = _TagCategoryTechnologyIcon
export const TagCategoryTerminalIcon = _TagCategoryTerminalIcon
export const TagCategoryTheaterIcon = _TagCategoryTheaterIcon
export const TagCategoryThemedIcon = _TagCategoryThemedIcon
export const TagCategoryTransportationIcon = _TagCategoryTransportationIcon
export const TagCategoryTreePineIcon = _TagCategoryTreePineIcon
export const TagCategoryTrophyIcon = _TagCategoryTrophyIcon
export const TagCategoryTweaksIcon = _TagCategoryTweaksIcon
export const TagCategoryUsersIcon = _TagCategoryUsersIcon
export const TagCategoryUtilityIcon = _TagCategoryUtilityIcon
export const TagCategoryVanillaLikeIcon = _TagCategoryVanillaLikeIcon
export const TagCategoryWandSparklesIcon = _TagCategoryWandSparklesIcon
export const TagCategoryWifiOffIcon = _TagCategoryWifiOffIcon
export const TagCategoryWorldgenIcon = _TagCategoryWorldgenIcon
export const TagCategoryZapIcon = _TagCategoryZapIcon
export const TagLoaderBabricIcon = _TagLoaderBabricIcon
export const TagLoaderBtaBabricIcon = _TagLoaderBtaBabricIcon
export const TagLoaderBukkitIcon = _TagLoaderBukkitIcon
@@ -655,60 +759,105 @@ export const categoryIconMap: Record<string, IconComponent> = {
adventure: TagCategoryAdventureIcon,
atmosphere: TagCategoryAtmosphereIcon,
audio: TagCategoryAudioIcon,
backpack: TagCategoryBackpackIcon,
badge: TagCategoryBadgeIcon,
'badge-check': TagCategoryBadgeCheckIcon,
'bed-double': TagCategoryBedDoubleIcon,
blocks: TagCategoryBlocksIcon,
bloom: TagCategoryBloomIcon,
'building-2': TagCategoryBuilding2Icon,
camera: TagCategoryCameraIcon,
cartoon: TagCategoryCartoonIcon,
castle: TagCategoryCastleIcon,
challenging: TagCategoryChallengingIcon,
clapperboard: TagCategoryClapperboardIcon,
cloud: TagCategoryCloudIcon,
'colored-lighting': TagCategoryColoredLightingIcon,
combat: TagCategoryCombatIcon,
compass: TagCategoryCompassIcon,
'core-shaders': TagCategoryCoreShadersIcon,
crown: TagCategoryCrownIcon,
cursed: TagCategoryCursedIcon,
decoration: TagCategoryDecorationIcon,
dices: TagCategoryDicesIcon,
economy: TagCategoryEconomyIcon,
entities: TagCategoryEntitiesIcon,
environment: TagCategoryEnvironmentIcon,
equipment: TagCategoryEquipmentIcon,
fantasy: TagCategoryFantasyIcon,
film: TagCategoryFilmIcon,
flag: TagCategoryFlagIcon,
foliage: TagCategoryFoliageIcon,
fonts: TagCategoryFontsIcon,
food: TagCategoryFoodIcon,
footprints: TagCategoryFootprintsIcon,
'game-mechanics': TagCategoryGameMechanicsIcon,
'gamepad-2': TagCategoryGamepad2Icon,
gauge: TagCategoryGaugeIcon,
globe: TagCategoryGlobeIcon,
'grid-3x3': TagCategoryGrid3x3Icon,
gui: TagCategoryGuiIcon,
handshake: TagCategoryHandshakeIcon,
'heart-crack': TagCategoryHeartCrackIcon,
'heart-pulse': TagCategoryHeartPulseIcon,
high: TagCategoryHighIcon,
house: TagCategoryHouseIcon,
items: TagCategoryItemsIcon,
'kitchen-sink': TagCategoryKitchenSinkIcon,
library: TagCategoryLibraryIcon,
lightweight: TagCategoryLightweightIcon,
locale: TagCategoryLocaleIcon,
lock: TagCategoryLockIcon,
low: TagCategoryLowIcon,
magic: TagCategoryMagicIcon,
management: TagCategoryManagementIcon,
'map-pinned': TagCategoryMapPinnedIcon,
medium: TagCategoryMediumIcon,
minigame: TagCategoryMinigameIcon,
mobs: TagCategoryMobsIcon,
modded: TagCategoryModdedIcon,
models: TagCategoryModelsIcon,
multiplayer: TagCategoryMultiplayerIcon,
network: TagCategoryNetworkIcon,
optimization: TagCategoryOptimizationIcon,
palette: TagCategoryPaletteIcon,
'path-tracing': TagCategoryPathTracingIcon,
'paw-print': TagCategoryPawPrintIcon,
pbr: TagCategoryPbrIcon,
pickaxe: TagCategoryPickaxeIcon,
potato: TagCategoryPotatoIcon,
quests: TagCategoryQuestsIcon,
realistic: TagCategoryRealisticIcon,
reflections: TagCategoryReflectionsIcon,
'refresh-ccw': TagCategoryRefreshCcwIcon,
screenshot: TagCategoryScreenshotIcon,
'scroll-text': TagCategoryScrollTextIcon,
'semi-realistic': TagCategorySemiRealisticIcon,
shadows: TagCategoryShadowsIcon,
shield: TagCategoryShieldIcon,
simplistic: TagCategorySimplisticIcon,
skull: TagCategorySkullIcon,
social: TagCategorySocialIcon,
square: TagCategorySquareIcon,
storage: TagCategoryStorageIcon,
sword: TagCategorySwordIcon,
swords: TagCategorySwordsIcon,
target: TagCategoryTargetIcon,
technology: TagCategoryTechnologyIcon,
terminal: TagCategoryTerminalIcon,
theater: TagCategoryTheaterIcon,
themed: TagCategoryThemedIcon,
transportation: TagCategoryTransportationIcon,
'tree-pine': TagCategoryTreePineIcon,
trophy: TagCategoryTrophyIcon,
tweaks: TagCategoryTweaksIcon,
users: TagCategoryUsersIcon,
utility: TagCategoryUtilityIcon,
'vanilla-like': TagCategoryVanillaLikeIcon,
'wand-sparkles': TagCategoryWandSparklesIcon,
'wifi-off': TagCategoryWifiOffIcon,
worldgen: TagCategoryWorldgenIcon,
zap: TagCategoryZapIcon,
}
export const loaderIconMap: Record<string, IconComponent> = {

View File

@@ -0,0 +1,18 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-component"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M15.536 11.293a1 1 0 0 0 0 1.414l2.376 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z" />
<path d="M2.297 11.293a1 1 0 0 0 0 1.414l2.377 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414L6.088 8.916a1 1 0 0 0-1.414 0z" />
<path d="M8.916 17.912a1 1 0 0 0 0 1.415l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.415l-2.377-2.376a1 1 0 0 0-1.414 0z" />
<path d="M8.916 4.674a1 1 0 0 0 0 1.414l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z" />
</svg>

After

Width:  |  Height:  |  Size: 844 B

View File

@@ -0,0 +1,18 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-images"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m22 11-1.296-1.296a2.4 2.4 0 0 0-3.408 0L11 16" />
<path d="M4 8a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2" />
<circle cx="13" cy="7" r="1" fill="currentColor" />
<rect x="8" y="2" width="14" height="14" rx="2" />
</svg>

After

Width:  |  Height:  |  Size: 523 B

View File

@@ -0,0 +1,4 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 9C15 12.3137 12.3137 15 9 15C5.68629 15 3 12.3137 3 9C3 5.68629 5.68629 3 9 3C12.3137 3 15 5.68629 15 9Z" fill="var(--_color-inner, var(--color-green))"/>
<path d="M15 9C15 5.68629 12.3137 3 9 3C5.68629 3 3 5.68629 3 9C3 12.3137 5.68629 15 9 15C12.3137 15 15 12.3137 15 9ZM18 9C18 13.9706 13.9706 18 9 18C4.02944 18 0 13.9706 0 9C0 4.02944 4.02944 0 9 0C13.9706 0 18 4.02944 18 9Z" fill="var(--_color-outer, var(--color-green-highlight))" />
</svg>

After

Width:  |  Height:  |  Size: 558 B

View File

@@ -0,0 +1,20 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-package-plus"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M16 16h6" />
<path d="M19 13v6" />
<path d="M21 10V8a2 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 0l2-1.14" />
<path d="m7.5 4.27 9 5.15" />
<polyline points="3.29 7 12 12 20.71 7" />
<line x1="12" x2="12" y1="22" y2="12" />
</svg>

After

Width:  |  Height:  |  Size: 583 B

View File

@@ -0,0 +1,18 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-power-off"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18.36 6.64A9 9 0 0 1 20.77 15" />
<path d="M6.16 6.16a9 9 0 1 0 12.68 12.68" />
<path d="M12 2v4" />
<path d="m2 2 20 20" />
</svg>

After

Width:  |  Height:  |  Size: 432 B

View File

@@ -0,0 +1,16 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-power"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 2v10" />
<path d="M18.4 6.6a9 9 0 1 1-12.77.04" />
</svg>

After

Width:  |  Height:  |  Size: 353 B

View File

@@ -0,0 +1,17 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-store"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M15 21v-5a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v5" />
<path d="M17.774 10.31a1.12 1.12 0 0 0-1.549 0 2.5 2.5 0 0 1-3.451 0 1.12 1.12 0 0 0-1.548 0 2.5 2.5 0 0 1-3.452 0 1.12 1.12 0 0 0-1.549 0 2.5 2.5 0 0 1-3.77-3.248l2.889-4.184A2 2 0 0 1 7 2h10a2 2 0 0 1 1.653.873l2.895 4.192a2.5 2.5 0 0 1-3.774 3.244" />
<path d="M4 10.95V19a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8.05" />
</svg>

After

Width:  |  Height:  |  Size: 664 B

View File

@@ -0,0 +1,19 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-backpack"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M4 10a4 4 0 0 1 4-4h8a4 4 0 0 1 4 4v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z" />
<path d="M8 10h8" />
<path d="M8 18h8" />
<path d="M8 22v-6a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v6" />
<path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2" />
</svg>

After

Width:  |  Height:  |  Size: 529 B

View File

@@ -0,0 +1,16 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-badge-check"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3.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: 495 B

View File

@@ -0,0 +1,15 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-badge"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3.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" />
</svg>

After

Width:  |  Height:  |  Size: 460 B

View File

@@ -0,0 +1,18 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-bed-double"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M2 20v-8a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v8" />
<path d="M4 10V6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v4" />
<path d="M12 4v6" />
<path d="M2 18h20" />
</svg>

After

Width:  |  Height:  |  Size: 450 B

View File

@@ -0,0 +1,19 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-building-2"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M10 12h4" />
<path d="M10 8h4" />
<path d="M14 21v-3a2 2 0 0 0-4 0v3" />
<path d="M6 10H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-2" />
<path d="M6 21V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v16" />
</svg>

After

Width:  |  Height:  |  Size: 523 B

View File

@@ -0,0 +1,16 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-camera"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M13.997 4a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 1.759-1.048l.489-.904A2 2 0 0 1 10.004 4z" />
<circle cx="12" cy="13" r="3" />
</svg>

After

Width:  |  Height:  |  Size: 515 B

View File

@@ -0,0 +1,22 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-castle"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M10 5V3" />
<path d="M14 5V3" />
<path d="M15 21v-3a3 3 0 0 0-6 0v3" />
<path d="M18 3v8" />
<path d="M18 5H6" />
<path d="M22 11H2" />
<path d="M22 9v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9" />
<path d="M6 3v8" />
</svg>

After

Width:  |  Height:  |  Size: 521 B

View File

@@ -0,0 +1,18 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-clapperboard"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M20.2 6 3 11l-.9-2.4c-.3-1.1.3-2.2 1.3-2.5l13.5-4c1.1-.3 2.2.3 2.5 1.3Z" />
<path d="m6.2 5.3 3.1 3.9" />
<path d="m12.4 3.4 3.1 4" />
<path d="M3 11h18v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z" />
</svg>

After

Width:  |  Height:  |  Size: 499 B

View File

@@ -0,0 +1,15 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-cloud"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" />
</svg>

After

Width:  |  Height:  |  Size: 352 B

View File

@@ -0,0 +1,16 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-compass"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z" />
<circle cx="12" cy="12" r="10" />
</svg>

After

Width:  |  Height:  |  Size: 430 B

View File

@@ -0,0 +1,16 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-crown"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M11.562 3.266a.5.5 0 0 1 .876 0L15.39 8.87a1 1 0 0 0 1.516.294L21.183 5.5a.5.5 0 0 1 .798.519l-2.834 10.246a1 1 0 0 1-.956.734H5.81a1 1 0 0 1-.957-.734L2.02 6.02a.5.5 0 0 1 .798-.519l4.276 3.664a1 1 0 0 0 1.516-.294z" />
<path d="M5 21h14" />
</svg>

After

Width:  |  Height:  |  Size: 541 B

View File

@@ -0,0 +1,20 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-dices"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="12" height="12" x="2" y="10" rx="2" ry="2" />
<path d="m17.92 14 3.5-3.5a2.24 2.24 0 0 0 0-3l-5-4.92a2.24 2.24 0 0 0-3 0L10 6" />
<path d="M6 18h.01" />
<path d="M10 14h.01" />
<path d="M15 6h.01" />
<path d="M18 9h.01" />
</svg>

After

Width:  |  Height:  |  Size: 533 B

View File

@@ -0,0 +1,22 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-film"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M7 3v18" />
<path d="M3 7.5h4" />
<path d="M3 12h18" />
<path d="M3 16.5h4" />
<path d="M17 3v18" />
<path d="M17 7.5h4" />
<path d="M17 16.5h4" />
</svg>

After

Width:  |  Height:  |  Size: 508 B

View File

@@ -0,0 +1,15 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-flag"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M4 22V4a1 1 0 0 1 .4-.8A6 6 0 0 1 8 2c3 0 5 2 7.333 2q2 0 3.067-.8A1 1 0 0 1 20 4v10a1 1 0 0 1-.4.8A6 6 0 0 1 16 16c-3 0-5-2-8-2a6 6 0 0 0-4 1.528" />
</svg>

After

Width:  |  Height:  |  Size: 446 B

View File

@@ -0,0 +1,18 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-footprints"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M4 16v-2.38C4 11.5 2.97 10.5 3 8c.03-2.72 1.49-6 4.5-6C9.37 2 10 3.8 10 5.5c0 3.11-2 5.66-2 8.68V16a2 2 0 1 1-4 0Z" />
<path d="M20 20v-2.38c0-2.12 1.03-3.12 1-5.62-.03-2.72-1.49-6-4.5-6C14.63 6 14 7.8 14 9.5c0 3.11 2 5.66 2 8.68V20a2 2 0 1 0 4 0Z" />
<path d="M16 17h4" />
<path d="M4 13h4" />
</svg>

After

Width:  |  Height:  |  Size: 602 B

View File

@@ -0,0 +1,19 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-gamepad-2"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="6" x2="10" y1="11" y2="11" />
<line x1="8" x2="8" y1="9" y2="13" />
<line x1="15" x2="15.01" y1="12" y2="12" />
<line x1="18" x2="18.01" y1="10" y2="10" />
<path d="M17.32 5H6.68a4 4 0 0 0-3.978 3.59c-.006.052-.01.101-.017.152C2.604 9.416 2 14.456 2 16a3 3 0 0 0 3 3c1 0 1.5-.5 2-1l1.414-1.414A2 2 0 0 1 9.828 16h4.344a2 2 0 0 1 1.414.586L17 18c.5.5 1 1 2 1a3 3 0 0 0 3-3c0-1.545-.604-6.584-.685-7.258-.007-.05-.011-.1-.017-.151A4 4 0 0 0 17.32 5z" />
</svg>

After

Width:  |  Height:  |  Size: 761 B

View File

@@ -0,0 +1,16 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-gauge"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m12 14 4-4" />
<path d="M3.34 19a10 10 0 1 1 17.32 0" />
</svg>

After

Width:  |  Height:  |  Size: 355 B

View File

@@ -0,0 +1,17 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-globe"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
<path d="M2 12h20" />
</svg>

After

Width:  |  Height:  |  Size: 408 B

View File

@@ -0,0 +1,19 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-grid-3x3"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M3 9h18" />
<path d="M3 15h18" />
<path d="M9 3v18" />
<path d="M15 3v18" />
</svg>

After

Width:  |  Height:  |  Size: 435 B

View File

@@ -0,0 +1,19 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-handshake"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m11 17 2 2a1 1 0 1 0 3-3" />
<path d="m14 14 2.5 2.5a1 1 0 1 0 3-3l-3.88-3.88a3 3 0 0 0-4.24 0l-.88.88a1 1 0 1 1-3-3l2.81-2.81a5.79 5.79 0 0 1 7.06-.87l.47.28a2 2 0 0 0 1.42.25L21 4" />
<path d="m21 3 1 11h-2" />
<path d="M3 3 2 14l6.5 6.5a1 1 0 1 0 3-3" />
<path d="M3 4h8" />
</svg>

After

Width:  |  Height:  |  Size: 586 B

View File

@@ -0,0 +1,16 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-heart-crack"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12.409 5.824c-.702.792-1.15 1.496-1.415 2.166l2.153 2.156a.5.5 0 0 1 0 .707l-2.293 2.293a.5.5 0 0 0 0 .707L12 15" />
<path d="M13.508 20.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5a5.5 5.5 0 0 1 9.591-3.677.6.6 0 0 0 .818.001A5.5 5.5 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5z" />
</svg>

After

Width:  |  Height:  |  Size: 579 B

View File

@@ -0,0 +1,16 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-heart-pulse"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5" />
<path d="M3.22 13H9.5l.5-1 2 4.5 2-7 1.5 3.5h5.27" />
</svg>

After

Width:  |  Height:  |  Size: 512 B

View File

@@ -0,0 +1,16 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-house"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" />
<path d="M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
</svg>

After

Width:  |  Height:  |  Size: 460 B

View File

@@ -0,0 +1,16 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-lock"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" />
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg>

After

Width:  |  Height:  |  Size: 385 B

View File

@@ -0,0 +1,17 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-map-pinned"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18 8c0 3.613-3.869 7.429-5.393 8.795a1 1 0 0 1-1.214 0C9.87 15.429 6 11.613 6 8a6 6 0 0 1 12 0" />
<circle cx="12" cy="8" r="2" />
<path d="M8.714 14h-3.71a1 1 0 0 0-.948.683l-2.004 6A1 1 0 0 0 3 22h18a1 1 0 0 0 .948-1.316l-2-6a1 1 0 0 0-.949-.684h-3.712" />
</svg>

After

Width:  |  Height:  |  Size: 565 B

View File

@@ -0,0 +1,19 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-network"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="16" y="16" width="6" height="6" rx="1" />
<rect x="2" y="16" width="6" height="6" rx="1" />
<rect x="9" y="2" width="6" height="6" rx="1" />
<path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3" />
<path d="M12 12V8" />
</svg>

After

Width:  |  Height:  |  Size: 524 B

View File

@@ -0,0 +1,19 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-palette"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z" />
<circle cx="13.5" cy="6.5" r=".5" fill="currentColor" />
<circle cx="17.5" cy="10.5" r=".5" fill="currentColor" />
<circle cx="6.5" cy="12.5" r=".5" fill="currentColor" />
<circle cx="8.5" cy="7.5" r=".5" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 650 B

View File

@@ -0,0 +1,18 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-paw-print"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="11" cy="4" r="2" />
<circle cx="18" cy="8" r="2" />
<circle cx="20" cy="16" r="2" />
<path d="M9 10a5 5 0 0 1 5 5v3.5a3.5 3.5 0 0 1-6.84 1.045Q6.52 17.48 4.46 16.84A3.5 3.5 0 0 1 5.5 10Z" />
</svg>

After

Width:  |  Height:  |  Size: 500 B

View File

@@ -0,0 +1,18 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-pickaxe"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m14 13-8.381 8.38a1 1 0 0 1-3.001-3L11 9.999" />
<path d="M15.973 4.027A13 13 0 0 0 5.902 2.373c-1.398.342-1.092 2.158.277 2.601a19.9 19.9 0 0 1 5.822 3.024" />
<path d="M16.001 11.999a19.9 19.9 0 0 1 3.024 5.824c.444 1.369 2.26 1.676 2.603.278A13 13 0 0 0 20 8.069" />
<path d="M18.352 3.352a1.205 1.205 0 0 0-1.704 0l-5.296 5.296a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l5.296-5.296a1.205 1.205 0 0 0 0-1.704z" />
</svg>

After

Width:  |  Height:  |  Size: 743 B

View File

@@ -0,0 +1,18 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-refresh-ccw"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
<path d="M3 3v5h5" />
<path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" />
<path d="M16 16h5v5" />
</svg>

After

Width:  |  Height:  |  Size: 474 B

View File

@@ -0,0 +1,18 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-scroll-text"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M15 12h-5" />
<path d="M15 8h-5" />
<path d="M19 17V5a2 2 0 0 0-2-2H4" />
<path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3" />
</svg>

After

Width:  |  Height:  |  Size: 502 B

View File

@@ -0,0 +1,15 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-shield"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" />
</svg>

After

Width:  |  Height:  |  Size: 464 B

View File

@@ -0,0 +1,18 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-skull"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m12.5 17-.5-1-.5 1h1z" />
<path d="M15 22a1 1 0 0 0 1-1v-1a2 2 0 0 0 1.56-3.25 8 8 0 1 0-11.12 0A2 2 0 0 0 8 20v1a1 1 0 0 0 1 1z" />
<circle cx="15" cy="12" r="1" />
<circle cx="9" cy="12" r="1" />
</svg>

After

Width:  |  Height:  |  Size: 500 B

View File

@@ -0,0 +1,15 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-square"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="18" height="18" x="3" y="3" rx="2" />
</svg>

After

Width:  |  Height:  |  Size: 339 B

View File

@@ -0,0 +1,18 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-sword"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m11 19-6-6" />
<path d="m5 21-2-2" />
<path d="m8 16-4 4" />
<path d="M9.5 17.5 21 6V3h-3L6.5 14.5" />
</svg>

After

Width:  |  Height:  |  Size: 405 B

View File

@@ -0,0 +1,22 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-swords"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="14.5 17.5 3 6 3 3 6 3 17.5 14.5" />
<line x1="13" x2="19" y1="19" y2="13" />
<line x1="16" x2="20" y1="16" y2="20" />
<line x1="19" x2="21" y1="21" y2="19" />
<polyline points="14.5 6.5 18 3 21 3 21 6 17.5 9.5" />
<line x1="5" x2="9" y1="14" y2="18" />
<line x1="7" x2="4" y1="17" y2="20" />
<line x1="3" x2="5" y1="19" y2="21" />
</svg>

After

Width:  |  Height:  |  Size: 651 B

View File

@@ -0,0 +1,17 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-target"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<circle cx="12" cy="12" r="6" />
<circle cx="12" cy="12" r="2" />
</svg>

After

Width:  |  Height:  |  Size: 392 B

View File

@@ -0,0 +1,16 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-terminal"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 19h8" />
<path d="m4 17 6-6-6-6" />
</svg>

After

Width:  |  Height:  |  Size: 341 B

View File

@@ -0,0 +1,23 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-theater"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M2 10s3-3 3-8" />
<path d="M22 10s-3-3-3-8" />
<path d="M10 2c0 4.4-3.6 8-8 8" />
<path d="M14 2c0 4.4 3.6 8 8 8" />
<path d="M2 10s2 2 2 5" />
<path d="M22 10s-2 2-2 5" />
<path d="M8 15h8" />
<path d="M2 22v-1a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1" />
<path d="M14 22v-1a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1" />
</svg>

After

Width:  |  Height:  |  Size: 617 B

View File

@@ -0,0 +1,16 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-tree-pine"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m17 14 3 3.3a1 1 0 0 1-.7 1.7H4.7a1 1 0 0 1-.7-1.7L7 14h-.3a1 1 0 0 1-.7-1.7L9 9h-.2A1 1 0 0 1 8 7.3L12 3l4 4.3a1 1 0 0 1-.8 1.7H15l3 3.3a1 1 0 0 1-.7 1.7H17Z" />
<path d="M12 22v-3" />
</svg>

After

Width:  |  Height:  |  Size: 488 B

View File

@@ -0,0 +1,20 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-trophy"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M10 14.66v1.626a2 2 0 0 1-.976 1.696A5 5 0 0 0 7 21.978" />
<path d="M14 14.66v1.626a2 2 0 0 0 .976 1.696A5 5 0 0 1 17 21.978" />
<path d="M18 9h1.5a1 1 0 0 0 0-5H18" />
<path d="M4 22h16" />
<path d="M6 9a6 6 0 0 0 12 0V3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1z" />
<path d="M6 9H4.5a1 1 0 0 1 0-5H6" />
</svg>

After

Width:  |  Height:  |  Size: 603 B

View File

@@ -0,0 +1,18 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-users"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<path d="M16 3.128a4 4 0 0 1 0 7.744" />
<path d="M22 21v-2a4 4 0 0 0-3-3.87" />
<circle cx="9" cy="7" r="4" />
</svg>

After

Width:  |  Height:  |  Size: 460 B

View File

@@ -0,0 +1,22 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-wand-sparkles"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m21.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.72" />
<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: 614 B

View File

@@ -0,0 +1,21 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-wifi-off"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 20h.01" />
<path d="M8.5 16.429a5 5 0 0 1 7 0" />
<path d="M5 12.859a10 10 0 0 1 5.17-2.69" />
<path d="M19 12.859a10 10 0 0 0-2.007-1.523" />
<path d="M2 8.82a15 15 0 0 1 4.177-2.643" />
<path d="M22 8.82a15 15 0 0 0-11.288-3.764" />
<path d="m2 2 20 20" />
</svg>

After

Width:  |  Height:  |  Size: 574 B

View File

@@ -0,0 +1,15 @@
<!-- @license lucide-static v0.562.0 - ISC -->
<svg
class="lucide lucide-zap"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z" />
</svg>

After

Width:  |  Height:  |  Size: 454 B

View File

@@ -0,0 +1,17 @@
[package]
name = "labrinth-derive"
edition.workspace = true
rust-version.workspace = true
repository.workspace = true
[lib]
proc-macro = true
[dependencies]
darling = { workspace = true }
proc-macro2 = { workspace = true }
quote = { workspace = true }
syn = { workspace = true }
[lints]
workspace = true

View File

@@ -0,0 +1,161 @@
use darling::{FromDeriveInput, FromField};
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{Attribute, DeriveInput, Ident, Result, Type, Visibility};
#[derive(Debug, FromDeriveInput)]
#[darling(supports(struct_named))]
struct Component {
ident: Ident,
vis: Visibility,
data: darling::ast::Data<(), ComponentField>,
}
#[derive(Debug, FromField)]
#[darling(attributes(component), forward_attrs)]
struct ComponentField {
ident: Option<Ident>,
vis: Visibility,
ty: Type,
attrs: Vec<Attribute>,
#[darling(default)]
synthetic: bool,
}
pub fn derive(input: &DeriveInput) -> Result<TokenStream> {
let Component { ident, vis, data } = Component::from_derive_input(input)?;
let fields = data
.take_struct()
.expect("macro only works on structs with named fields");
let fields = &fields.fields;
let struct_serial = struct_serial(&vis, &ident, fields)?;
let struct_edit = struct_edit(&vis, &ident, fields)?;
Ok(quote! {
#struct_serial
#struct_edit
})
}
fn struct_serial(
vis: &Visibility,
ident: &Ident,
fields: &[ComponentField],
) -> Result<TokenStream> {
let ident_serial = format_ident!("{ident}Serial");
let fields = fields
.iter()
.filter_map(|field| {
if field.synthetic {
return None;
}
let ident = &field
.ident
.as_ref()
.expect("macro only works on structs with named fields");
let vis = &field.vis;
let ty = &field.ty;
let attrs = &field.attrs;
Some(quote! {
#(#attrs)*
#vis #ident: #ty
})
})
.collect::<Vec<_>>();
Ok(quote! {
#[derive(
Debug,
Clone,
::serde::Serialize,
::serde::Deserialize,
::validator::Validate,
::utoipa::ToSchema,
)]
#vis struct #ident_serial {
#(#fields),*
}
})
}
fn struct_edit(
vis: &Visibility,
ident: &Ident,
fields: &[ComponentField],
) -> Result<TokenStream> {
let ident_edit = format_ident!("{ident}Edit");
let (fields, apply_fields): (Vec<_>, Vec<_>) = fields
.iter()
.filter_map(|field| {
if field.synthetic {
return None;
}
let ident = &field
.ident
.as_ref()
.expect("macro only works on structs with named fields");
let vis = &field.vis;
let ty = &field.ty;
let attrs = &field.attrs;
let serde_attr = if let Type::Path(path) = ty
&& let Some(root_ident) = path.path.segments.first()
&& root_ident.ident == "Option"
{
quote! {
#[serde(
default,
skip_serializing_if = "::core::option::Option::is_none",
with = "::serde_with::rust::double_option"
)]
}
} else {
quote! {
#[serde(default)]
}
};
Some((
quote! {
#(#attrs)*
#serde_attr
#vis #ident: ::core::option::Option<#ty>
},
quote! {
if let Some(t) = self.#ident {
component.#ident = t;
}
},
))
})
.unzip();
Ok(quote! {
#[derive(
Debug,
Clone,
::serde::Serialize,
::serde::Deserialize,
::validator::Validate,
::utoipa::ToSchema,
)]
#vis struct #ident_edit {
#(#fields),*
}
impl #ident_edit {
pub fn apply_to(
self,
component: &mut #ident,
) {
#(#apply_fields)*
}
}
})
}

View File

@@ -0,0 +1,15 @@
//! This crate is currently unused, but will replace the `macro_rules!` component
//! logic in Labrinth experimental API.
use proc_macro::TokenStream;
use syn::{DeriveInput, parse_macro_input};
mod component;
#[proc_macro_derive(Component, attributes(component))]
pub fn component(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
component::derive(&input)
.unwrap_or_else(|err| err.to_compile_error())
.into()
}

View File

@@ -0,0 +1,4 @@
**Website:** %PROJECT_SITE_URL% \
**Store:** %PROJECT_STORE_URL% \
**Wiki:** %PROJECT_WIKI_URL% \
**Discord:** %PROJECT_DISCORD_URL%

View File

@@ -0,0 +1,5 @@
## Reuploads are forbidden
This server appears to have uploaded a Modpack by another creator.
Per section 4 of %RULES%, we ask that you provide proof of your permission to distribute this pack on Modrinth.
Either implicit permission abiding by the terms of the content's license(s) or explicit permission from the original creator of the pack.

View File

@@ -1,6 +1,6 @@
## Private Use
Under normal circumstances, your project would be rejected due to the issues listed below.
However, since your project is not intended for public use, these requirements will be waived and your project will be unlisted. This means it will remain accessible through a direct link without appearing in public search results, allowing you to share it privately.
If you're okay with this, or submitted your project to be unlisted already, than no further action is necessary.
Under normal circumstances, your project would be rejected due to the issues listed below.
However, since your project is not intended for public use, these requirements will be waived and your project will be unlisted. This means it will remain accessible through a direct link without appearing in public search results, allowing you to share it privately.
If you're okay with this, or submitted your project to be unlisted already, than no further action is necessary.
If you would like to publish your project publicly, please address all moderation concerns before resubmitting this project.

View File

@@ -0,0 +1,8 @@
## Modded servers on Modrinth
Since your project is intended for use on a specific Minecraft server, we strongly recommend [listing your server with Modrinth](https://modrinth.com/discover/servers).
When creating a server project, you can connect it to any Modpack that already exists on Modrinth, or upload your content as a custom modpack.
Adding a custom modpack to your server will save you time, only needing to manage one lightweight project page, and make finding, updating, and playing on your server much easier for your players.
We strongly encourage you to %LEARN_MORE_ABOUT_SERVERS_FLINK%.

View File

@@ -0,0 +1,7 @@
## Insufficient Summary
Currently, your summary includes your server's Address, this is redundant information that should instead be appropriately listed in your server's %PROJECT_SETTINGS_FLINK%.
Your server's summary should provide a brief overview of your server that informs and entices players.
This is the first thing most people will see about your listing other than the Icon, so it's important it be accurate, reasonably detailed, and exciting.

View File

@@ -4,4 +4,4 @@ Per section 5.3 of %RULES%, your Summary can not be the same as your project's T
Your project summary should provide a brief overview of your project that informs and entices users.
This is the first thing most people will see about your mod other than the Logo, so it's important it be accurate, reasonably detailed, and exciting.
This is the first thing most people will see about your mod other than the Icon, so it's important it be accurate, reasonably detailed, and exciting.

View File

@@ -11,7 +11,8 @@ export default {
// Replace me please.
guidance_url:
'https://www.notion.so/Content-Moderation-Cheat-Sheets-22d5ee711bf081a4920ef08879fe6bf5?source=copy_link#22d5ee711bf08116bd8bc1186f357062',
shouldShow: (project: Labrinth.Projects.v2.Project) => project.project_type === 'modpack',
shouldShow: (project: Labrinth.Projects.v2.Project, projectV3) =>
project.project_type === 'modpack' && !projectV3?.minecraft_server,
actions: [
{
id: 'button',

View File

@@ -2,6 +2,13 @@ import type { Nag } from '../types/nags'
import { coreNags } from './nags/core'
import { descriptionNags } from './nags/description'
import { linksNags } from './nags/links'
import { serverProjectsNags } from './nags/server-projects'
import { tagsNags } from './nags/tags'
export default [...coreNags, ...linksNags, ...descriptionNags, ...tagsNags] as Nag[]
export default [
...coreNags,
...linksNags,
...descriptionNags,
...tagsNags,
...serverProjectsNags,
] as Nag[]

View File

@@ -37,7 +37,8 @@ export const coreNags: Nag[] = [
defaultMessage: 'At least one version is required for a project to be submitted for review.',
}),
status: 'required',
shouldShow: (context: NagContext) => context.versions.length < 1,
shouldShow: (context: NagContext) =>
context.versions.length < 1 && !context.projectV3?.minecraft_server,
link: {
path: 'settings/versions',
title: defineMessage({
@@ -147,6 +148,7 @@ export const coreNags: Nag[] = [
}),
status: 'suggestion',
shouldShow: (context: NagContext) => {
if (!!context.projectV3?.minecraft_server) return false
const featuredGalleryImage = context.project.gallery?.find((img) => img.featured)
return context.project?.gallery?.length === 0 || !featuredGalleryImage
},
@@ -159,58 +161,6 @@ export const coreNags: Nag[] = [
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-gallery',
},
},
{
id: 'select-tags',
title: defineMessage({
id: 'nags.select-tags.title',
defaultMessage: 'Select tags',
}),
description: defineMessage({
id: 'nags.select-tags.description',
defaultMessage:
'Select the tags that correctly apply to your project to help the right users find it.',
}),
status: 'suggestion',
shouldShow: (context: NagContext) =>
context.project.versions.length > 0 && context.project.categories.length < 1,
link: {
path: 'settings/tags',
title: defineMessage({
id: 'nags.settings.tags.title',
defaultMessage: 'Visit tag settings',
}),
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
},
},
{
id: 'add-links',
title: defineMessage({
id: 'nags.add-links.title',
defaultMessage: 'Add external links',
}),
description: defineMessage({
id: 'nags.add-links.description',
defaultMessage:
'Add any relevant links targeted outside of Modrinth, such as source code, an issue tracker, or a Discord invite.',
}),
status: 'suggestion',
shouldShow: (context: NagContext) =>
!(
context.project.issues_url ||
context.project.source_url ||
context.project.wiki_url ||
context.project.discord_url ||
context.project.donation_urls?.length
),
link: {
path: 'settings/links',
title: defineMessage({
id: 'nags.settings.links.title',
defaultMessage: 'Visit links settings',
}),
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
},
},
{
id: 'select-license',
title: defineMessage({
@@ -232,7 +182,8 @@ export const coreNags: Nag[] = [
)
},
status: 'required',
shouldShow: (context: NagContext) => context.project.license.id === 'LicenseRef-Unknown',
shouldShow: (context: NagContext) =>
context.project.license.id === 'LicenseRef-Unknown' && !context.projectV3?.minecraft_server,
link: {
path: 'settings/license',
title: defineMessage({

View File

@@ -1,4 +1,5 @@
export * from './core'
export * from './description'
export * from './links'
export * from './server-projects'
export * from './tags'

View File

@@ -66,6 +66,65 @@ export function isUncommonLicenseUrl(url: string | null): boolean {
}
export const linksNags: Nag[] = [
{
id: 'add-links',
title: defineMessage({
id: 'nags.add-links.title',
defaultMessage: 'Add external links',
}),
description: defineMessage({
id: 'nags.add-links.description',
defaultMessage:
'Add any relevant links targeted outside of Modrinth, such as source code, an issue tracker, or a Discord invite.',
}),
status: 'suggestion',
shouldShow: (context: NagContext) =>
!context.projectV3?.minecraft_server &&
!(
context.project.issues_url ||
context.project.source_url ||
context.project.wiki_url ||
context.project.discord_url ||
context.project.donation_urls?.length
),
link: {
path: 'settings/links',
title: defineMessage({
id: 'nags.settings.links.title',
defaultMessage: 'Visit links settings',
}),
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
},
},
{
id: 'add-links-server',
title: defineMessage({
id: 'nags.add-links-server.title',
defaultMessage: 'Add external links',
}),
description: defineMessage({
id: 'nags.add-links-server.description',
defaultMessage:
'Add any relevant links targeted outside of Modrinth, such as a website, store, or a Discord invite.',
}),
status: 'suggestion',
shouldShow: (context: NagContext) => {
return !(
context.projectV3?.link_urls?.site?.url ||
context.projectV3?.link_urls?.store?.url ||
context.projectV3?.link_urls?.discord?.url ||
context.projectV3?.link_urls?.wiki?.url
)
},
link: {
path: 'settings/links',
title: defineMessage({
id: 'nags.settings.links.title',
defaultMessage: 'Visit links settings',
}),
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
},
},
{
id: 'verify-external-links',
title: defineMessage({
@@ -109,7 +168,9 @@ export const linksNags: Nag[] = [
shouldShow: (context: NagContext) =>
isDiscordUrl(context.project.source_url ?? null) ||
isDiscordUrl(context.project.issues_url ?? null) ||
isDiscordUrl(context.project.wiki_url ?? null),
isDiscordUrl(context.project.wiki_url ?? null) ||
isDiscordUrl(context.projectV3?.link_urls?.site?.url ?? null) ||
isDiscordUrl(context.projectV3?.link_urls?.store?.url ?? null),
link: {
path: 'settings/links',
title: defineMessage({
@@ -145,6 +206,8 @@ export const linksNags: Nag[] = [
isLinkShortener(context.project.issues_url ?? null) ||
isLinkShortener(context.project.wiki_url ?? null) ||
isLinkShortener(context.project.discord_url ?? null) ||
isLinkShortener(context.projectV3?.link_urls?.site?.url ?? null) ||
isLinkShortener(context.projectV3?.link_urls?.store?.url ?? null) ||
Boolean(context.project.license.url && isLinkShortener(context.project.license.url ?? null))
)
},

View File

@@ -0,0 +1,102 @@
import { defineMessage } from '@modrinth/ui'
import type { Nag, NagContext } from '../../types/nags'
export const serverProjectsNags: Nag[] = [
{
id: 'select-country',
title: defineMessage({
id: 'nags.select-country.title',
defaultMessage: 'Select a country',
}),
description: defineMessage({
id: 'nags.select-country.description',
defaultMessage: 'Let players know what country your server is located in.',
}),
status: 'required',
shouldShow: (context: NagContext) =>
!!context.projectV3?.minecraft_server && !context.projectV3?.minecraft_server.country,
link: {
path: 'settings/server',
title: defineMessage({
id: 'nags.server.title',
defaultMessage: 'Visit server settings',
}),
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-server',
},
},
{
id: 'select-language',
title: defineMessage({
id: 'nags.select-language.title',
defaultMessage: 'Select a language',
}),
description: defineMessage({
id: 'nags.select-language.description',
defaultMessage: 'List the language or languages supported by your server.',
}),
status: 'suggestion',
shouldShow: (context: NagContext) =>
!!context.projectV3?.minecraft_server &&
context.projectV3?.minecraft_server?.languages?.length === 0,
link: {
path: 'settings/server',
title: defineMessage({
id: 'nags.server.title',
defaultMessage: 'Visit server settings',
}),
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-server',
},
},
{
id: 'add-java-address',
title: defineMessage({
id: 'nags.add-java-address.title',
defaultMessage: 'Add a Java address',
}),
description: defineMessage({
id: 'nags.add-java-address.description',
defaultMessage:
'Add the IP address and port Java Edition players can use to join your server.',
}),
status: 'required',
shouldShow: (context: NagContext) =>
!!context.projectV3?.minecraft_server && !context.projectV3?.minecraft_java_server?.address,
link: {
path: 'settings/server',
title: defineMessage({
id: 'nags.server.title',
defaultMessage: 'Visit server settings',
}),
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-server',
},
},
{
id: 'select-compatibility',
title: defineMessage({
id: 'nags.select-compatibility.title',
defaultMessage: 'Select compatibility',
}),
description: defineMessage({
id: 'nags.select-compatibility.description',
defaultMessage:
'Select what versions your server supports, choose a Modpack, or upload your own.',
}),
status: 'required',
shouldShow: (context: NagContext) => {
if (
context.projectV3?.minecraft_java_server?.content?.kind === 'vanilla' &&
!context.projectV3?.minecraft_java_server?.content?.recommended_game_version
)
return true
return false
},
link: {
path: 'settings/server',
title: defineMessage({
id: 'nags.server.title',
defaultMessage: 'Visit server settings',
}),
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-server',
},
},
]

View File

@@ -6,6 +6,7 @@ import type { Nag, NagContext } from '../../types/nags'
const allResolutionTags = ['8x-', '16x', '32x', '48x', '64x', '128x', '256x', '512x+']
const MAX_TAG_COUNT = 8
const MAX_TAG_COUNT_SERVER = 18
function getCategories(
project: Labrinth.Projects.v2.Project & { actualProjectType: string },
@@ -23,6 +24,29 @@ function getCategories(
}
export const tagsNags: Nag[] = [
{
id: 'select-tags',
title: defineMessage({
id: 'nags.select-tags.title',
defaultMessage: 'Select tags',
}),
description: defineMessage({
id: 'nags.select-tags.description',
defaultMessage:
'Select the tags that correctly apply to your project to help the right users find it.',
}),
status: 'suggestion',
shouldShow: (context: NagContext) =>
context.project.versions.length > 0 && context.project.categories.length < 1,
link: {
path: 'settings/tags',
title: defineMessage({
id: 'nags.settings.tags.title',
defaultMessage: 'Visit tag settings',
}),
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
},
},
{
id: 'too-many-tags',
title: defineMessage({
@@ -49,9 +73,49 @@ export const tagsNags: Nag[] = [
},
status: 'warning',
shouldShow: (context: NagContext) => {
if (context.projectV3?.minecraft_java_server) return false
const tagCount =
context.project.categories.length + (context.project.additional_categories?.length || 0)
return tagCount > MAX_TAG_COUNT
return tagCount > MAX_TAG_COUNT && !context.projectV3?.minecraft_server
},
link: {
path: 'settings/tags',
title: defineMessage({
id: 'nags.edit-tags.title',
defaultMessage: 'Edit tags',
}),
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
},
},
{
id: 'too-many-tags-server',
title: defineMessage({
id: 'nags.too-many-tags-server.title',
defaultMessage: 'Select accurate tags',
}),
description: (context: NagContext) => {
const { formatMessage } = useVIntl()
const tagCount =
context.project.categories.length + (context.project.additional_categories?.length || 0)
const maxTagCount = MAX_TAG_COUNT_SERVER
return formatMessage(
defineMessage({
id: 'nags.too-many-tags-server.description',
defaultMessage:
"You've selected {tagCount, plural, one {# tag} other {# tags}}. Please reduce to {maxTagCount} or fewer to make sure your server appears in relevant search results.",
}),
{
tagCount,
maxTagCount,
},
)
},
status: 'required',
shouldShow: (context: NagContext) => {
const tagCount =
context.project.categories.length + (context.project.additional_categories?.length || 0)
return tagCount > MAX_TAG_COUNT_SERVER && context.projectV3?.minecraft_server != null
},
link: {
path: 'settings/tags',

View File

@@ -11,7 +11,8 @@ const environmentMultiple: Stage = {
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
text: async () =>
(await import('../../messages/checklist-text/environment/environment-multiple.md?raw')).default,
shouldShow: (project, projectV3) => (projectV3?.environment?.length ?? 0) !== 1,
shouldShow: (project, projectV3) =>
(projectV3?.environment?.length ?? 0) !== 1 && !projectV3?.minecraft_server,
actions: [
{
id: 'side_types_inaccurate',

View File

@@ -11,7 +11,8 @@ const environment: Stage = {
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
text: async () =>
(await import('../../messages/checklist-text/environment/environment.md?raw')).default,
shouldShow: (project, projectV3) => (projectV3?.environment?.length ?? 0) === 1,
shouldShow: (project, projectV3) =>
(projectV3?.environment?.length ?? 0) === 1 && !projectV3?.minecraft_server,
actions: [
{
id: 'side_types_inaccurate',

View File

@@ -17,6 +17,7 @@ const gallery: Stage = {
weight: 900,
suggestedStatus: 'flagged',
severity: 'low',
shouldShow: (project, projectV3) => !projectV3?.minecraft_server,
message: async () => (await import('../messages/gallery/insufficient.md?raw')).default,
} as ButtonAction,
{

View File

@@ -26,6 +26,9 @@ const licenseStage: Stage = {
icon: BookTextIcon,
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
navigate: '/settings/license',
shouldShow(project, projectV3) {
return !projectV3?.minecraft_server
},
actions: [
{
id: 'license_invalid_link',

View File

@@ -9,18 +9,23 @@ const links: Stage = {
icon: LinkIcon,
guidance_url: 'https://modrinth.com/legal/rules',
navigate: '/settings/links',
shouldShow: (project) =>
shouldShow: (project, projectV3) =>
Boolean(
project.issues_url ||
project.source_url ||
project.wiki_url ||
project.discord_url ||
projectV3?.link_urls?.site ||
projectV3?.link_urls?.store ||
project.donation_urls.length > 0,
),
text: async (project) => {
let text = (await import('../messages/checklist-text/links/base.md?raw')).default
text: async (project, projectV3) => {
let text
if (!!projectV3?.minecraft_server)
text = (await import('../messages/checklist-text/links/server.md?raw')).default
else text = (await import('../messages/checklist-text/links/base.md?raw')).default
if (project.donation_urls.length > 0) {
if (project.donation_urls && project.donation_urls.length > 0) {
text += (await import('../messages/checklist-text/links/donation/donations.md?raw')).default
for (const donation of project.donation_urls) {

View File

@@ -16,12 +16,14 @@ const reupload: Stage = {
weight: 1100,
suggestedStatus: 'rejected',
severity: 'high',
shouldShow: (project, projectV3) => !projectV3?.minecraft_server,
message: async () => (await import('../messages/reupload/reupload.md?raw')).default,
disablesActions: [
'reupload_unclear_fork',
'reupload_insufficient_fork',
'reupload_request_proof',
'reupload_identity_verification',
'reupload_request_proof_server',
],
relevantExtraInput: [
{
@@ -45,12 +47,14 @@ const reupload: Stage = {
weight: 1100,
suggestedStatus: 'rejected',
severity: 'high',
shouldShow: (project, projectV3) => !projectV3?.minecraft_server,
message: async () => (await import('../messages/reupload/fork.md?raw')).default,
disablesActions: [
'reupload_reupload',
'reupload_insufficient_fork',
'reupload_request_proof',
'reupload_identity_verification',
'reupload_request_proof_server',
],
} as ButtonAction,
{
@@ -60,12 +64,14 @@ const reupload: Stage = {
weight: 1100,
suggestedStatus: 'rejected',
severity: 'high',
shouldShow: (project, projectV3) => !projectV3?.minecraft_server,
message: async () => (await import('../messages/reupload/insufficient_fork.md?raw')).default,
disablesActions: [
'reupload_unclear_fork',
'reupload_reupload',
'reupload_request_proof',
'reupload_identity_verification',
'reupload_request_proof_server',
],
} as ButtonAction,
{
@@ -82,6 +88,7 @@ const reupload: Stage = {
'reupload_unclear_fork',
'reupload_insufficient_fork',
'reupload_identity_verification',
'reupload_request_proof_server',
],
},
{
@@ -91,6 +98,7 @@ const reupload: Stage = {
weight: 1100,
suggestedStatus: 'rejected',
severity: 'high',
shouldShow: (project, projectV3) => !projectV3?.minecraft_server,
message: async () =>
(await import('../messages/reupload/identity_verification.md?raw')).default,
relevantExtraInput: [
@@ -104,6 +112,27 @@ const reupload: Stage = {
'reupload_reupload',
'reupload_insufficient_fork',
'reupload_request_proof',
'reupload_request_proof_server',
],
},
{
id: 'reupload_request_proof_server',
type: 'button',
label: 'Custom modpack permissions',
weight: 1100,
suggestedStatus: 'rejected',
severity: 'high',
shouldShow: (project, projectV3) =>
!!projectV3?.minecraft_server &&
projectV3?.minecraft_java_server?.content?.kind === 'modpack' &&
projectV3?.minecraft_java_server?.content?.['project_id'] === project.id,
message: async () => (await import('../messages/reupload/custom_server.md?raw')).default,
disablesActions: [
'reupload_reupload',
'reupload_unclear_fork',
'reupload_insufficient_fork',
'reupload_identity_verification',
'reupload_request_proof',
],
},
],

View File

@@ -40,8 +40,19 @@ const statusAlerts: Stage = {
weight: -999999,
suggestedStatus: 'flagged',
disablesActions: ['status_corrections_applied', 'status_account_issues'],
shouldShow: (project, projectV3) => !projectV3?.minecraft_server,
message: async () => (await import('../messages/status-alerts/private.md?raw')).default,
} as ButtonAction,
{
id: 'status_server_use',
type: 'button',
label: 'Server use',
weight: -999999,
suggestedStatus: 'flagged',
disablesActions: ['status_corrections_applied', 'status_account_issues'],
shouldShow: (project) => project.project_type === 'modpack',
message: async () => (await import('../messages/status-alerts/serverpack.md?raw')).default,
} as ButtonAction,
{
id: 'status_account_issues',
type: 'button',
@@ -95,6 +106,14 @@ const statusAlerts: Stage = {
message: async () =>
(await import('../messages/status-alerts/automod_confusion.md?raw')).default,
} as ButtonAction,
{
id: 'status_serverpack',
type: 'button',
label: `Serverpack`,
weight: -999999,
shouldShow: (project) => project.project_type === 'modpack',
message: async () => (await import('../messages/status-alerts/serverpack.md?raw')).default,
} as ButtonAction,
],
}

View File

@@ -48,6 +48,16 @@ const summary: Stage = {
severity: 'medium',
message: async () => (await import('../messages/summary/non-english.md?raw')).default,
} as ButtonAction,
{
id: 'summary_repeat_ip',
type: 'button',
label: 'Repeat of IP',
weight: 303,
suggestedStatus: 'flagged',
severity: 'medium',
shouldShow: (project, projectV3) => !!projectV3?.minecraft_server,
message: async () => (await import('../messages/summary/repeat-ip.md?raw')).default,
},
],
}

View File

@@ -69,6 +69,7 @@ const titleSlug: Stage = {
{
label: 'Forked project',
weight: 112,
shouldShow: (project, projectV3) => !projectV3?.minecraft_server,
message: async () =>
(await import('../messages/title/similarities-fork.md?raw')).default,
},

View File

@@ -8,7 +8,7 @@ const undefinedProjectStage: Stage = {
icon: XIcon,
guidance_url: 'https://modrinth.com/legal/rules#miscellaneous',
navigate: '/versions',
shouldShow: (project) => project.versions.length === 0,
shouldShow: (project, projectV3) => project.versions.length === 0 && !projectV3?.minecraft_server,
actions: [
{
id: 'undefined_no_versions',

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