diff --git a/apps/frontend/AGENTS.md b/apps/frontend/AGENTS.md index 681311eb9c..ceb2b988dc 120000 --- a/apps/frontend/AGENTS.md +++ b/apps/frontend/AGENTS.md @@ -1 +1 @@ -CLAUDE.md \ No newline at end of file +CLAUDE.md diff --git a/packages/app-lib/src/state/cache.rs b/packages/app-lib/src/state/cache.rs index 867fc1b624..e066e4e5f1 100644 --- a/packages/app-lib/src/state/cache.rs +++ b/packages/app-lib/src/state/cache.rs @@ -510,7 +510,7 @@ pub struct DonationLink { pub url: String, } -#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Copy, Debug, Eq, PartialEq)] #[serde(rename_all = "kebab-case")] pub enum SideType { Required, diff --git a/packages/app-lib/src/state/instances/commands/list_content.rs b/packages/app-lib/src/state/instances/commands/list_content.rs index 69c2d86468..b6a8681cbc 100644 --- a/packages/app-lib/src/state/instances/commands/list_content.rs +++ b/packages/app-lib/src/state/instances/commands/list_content.rs @@ -529,6 +529,8 @@ pub(crate) async fn dependencies_to_content_items( update_version_id: None, date_added: None, source_kind: None, + client_side: Some(project.client_side), + server_side: Some(project.server_side), }) }) .collect::>(); @@ -906,6 +908,8 @@ async fn content_files_to_content_items( update_version_id: file.update_version_id.clone(), date_added: modification_times[index].clone(), source_kind: file.source_kind, + client_side: project.map(|project| project.client_side), + server_side: project.map(|project| project.server_side), } }) .collect::>(); diff --git a/packages/app-lib/src/state/instances/content.rs b/packages/app-lib/src/state/instances/content.rs index 6a0838ab6e..f7c490a85c 100644 --- a/packages/app-lib/src/state/instances/content.rs +++ b/packages/app-lib/src/state/instances/content.rs @@ -1,5 +1,5 @@ use super::ContentSourceKind; -use crate::state::{Project, ProjectType, Version}; +use crate::state::{Project, ProjectType, SideType, Version}; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, Clone)] @@ -17,6 +17,8 @@ pub struct ContentItem { pub update_version_id: Option, pub date_added: Option, pub source_kind: Option, + pub client_side: Option, + pub server_side: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/packages/ui/src/layouts/shared/content-tab/composables/content-filtering.ts b/packages/ui/src/layouts/shared/content-tab/composables/content-filtering.ts index 77b25aedc6..7bca6ed7a2 100644 --- a/packages/ui/src/layouts/shared/content-tab/composables/content-filtering.ts +++ b/packages/ui/src/layouts/shared/content-tab/composables/content-filtering.ts @@ -5,9 +5,10 @@ import { computed, ref, watch } from 'vue' import { defineMessages, useVIntl } from '#ui/composables/i18n' import { commonProjectTypeCategoryMessages, normalizeProjectType } from '#ui/utils/common-messages' -import type { ClientWarningType, ContentItem } from '../types' +import type { ClientWarningType, ContentItem, ContentSideType } from '../types' const CLIENT_ONLY_ENVIRONMENTS = new Set(['client_only', 'singleplayer_only']) +const SERVER_ONLY_ENVIRONMENTS = new Set(['server_only', 'dedicated_server_only']) export function isClientOnlyEnvironment(env?: string | null): boolean { return !!env && CLIENT_ONLY_ENVIRONMENTS.has(env) @@ -20,6 +21,20 @@ export function getClientWarningType(item: ContentItem): ClientWarningType | nul return null } +function isSideActive(side?: ContentSideType | null): boolean { + return side === 'required' || side === 'optional' +} + +export function matchesClientFilter(item: ContentItem): boolean { + if (getClientWarningType(item) !== null) return true + return isSideActive(item.client_side) && !isSideActive(item.server_side) +} + +export function matchesServerFilter(item: ContentItem): boolean { + if (item.environment && SERVER_ONLY_ENVIRONMENTS.has(item.environment)) return true + return isSideActive(item.server_side) && !isSideActive(item.client_side) +} + export interface ContentFilterOption { id: string label: string @@ -29,6 +44,8 @@ export interface ContentFilterConfig { showTypeFilters?: boolean showUpdateFilter?: boolean showWarningsFilter?: boolean + showClientFilter?: boolean + showServerFilter?: boolean isPackLocked?: Ref persistKey?: string } @@ -50,6 +67,14 @@ const messages = defineMessages({ id: 'content.filter.disabled', defaultMessage: 'Disabled', }, + client: { + id: 'content.filter.client-only', + defaultMessage: 'Client-Only', + }, + server: { + id: 'content.filter.server-only', + defaultMessage: 'Server-Only', + }, }) export function useContentFilters(items: Ref, config?: ContentFilterConfig) { @@ -99,6 +124,14 @@ export function useContentFilters(items: Ref, config?: ContentFil }) } + if (config?.showClientFilter && items.value.some((m) => matchesClientFilter(m))) { + options.push({ id: 'client', label: formatMessage(messages.client) }) + } + + if (config?.showServerFilter && items.value.some((m) => matchesServerFilter(m))) { + options.push({ id: 'server', label: formatMessage(messages.server) }) + } + return options }) @@ -138,7 +171,14 @@ export function useContentFilters(items: Ref, config?: ContentFil function applyFilters(source: ContentItem[]): ContentItem[] { if (selectedFilters.value.length === 0) return source - const attributeFilters = new Set(['updates', 'enabled', 'disabled', 'warnings']) + const attributeFilters = new Set([ + 'updates', + 'enabled', + 'disabled', + 'warnings', + 'client', + 'server', + ]) const typeFilters = selectedFilters.value.filter((f) => !attributeFilters.has(f)) const activeAttributes = selectedFilters.value.filter((f) => attributeFilters.has(f)) @@ -155,6 +195,8 @@ export function useContentFilters(items: Ref, config?: ContentFil if (filter === 'enabled' && !item.enabled) return false if (filter === 'disabled' && item.enabled) return false if (filter === 'warnings' && getClientWarningType(item) === null) return false + if (filter === 'client' && !matchesClientFilter(item)) return false + if (filter === 'server' && !matchesServerFilter(item)) return false } return true diff --git a/packages/ui/src/layouts/shared/content-tab/layout.vue b/packages/ui/src/layouts/shared/content-tab/layout.vue index bdee7c24a8..b47e1ecd9a 100644 --- a/packages/ui/src/layouts/shared/content-tab/layout.vue +++ b/packages/ui/src/layouts/shared/content-tab/layout.vue @@ -232,6 +232,8 @@ const { selectedFilters, filterOptions, toggleFilter, applyFilters } = useConten showTypeFilters: true, showUpdateFilter: ctx.hasUpdateSupport, showWarningsFilter: true, + showClientFilter: true, + showServerFilter: true, isPackLocked: ctx.isPackLocked, persistKey: ctx.filterPersistKey, }, diff --git a/packages/ui/src/layouts/shared/content-tab/types.ts b/packages/ui/src/layouts/shared/content-tab/types.ts index c92f8216a9..996c8eb140 100644 --- a/packages/ui/src/layouts/shared/content-tab/types.ts +++ b/packages/ui/src/layouts/shared/content-tab/types.ts @@ -31,6 +31,8 @@ export type ContentSourceKind = | 'imported_modpack' | 'shared_instance' +export type ContentSideType = 'required' | 'optional' | 'unsupported' | 'unknown' + export interface ContentActionWarning { admonitionHeader: string admonitionBody: string @@ -84,6 +86,8 @@ export interface ContentItem extends Omit< environment?: string pack_client_retained?: boolean pack_client_depends?: boolean + client_side?: ContentSideType + server_side?: ContentSideType installing?: boolean source_kind?: ContentSourceKind | null external?: boolean diff --git a/packages/ui/src/locales/en-US/index.json b/packages/ui/src/locales/en-US/index.json index f515082ecb..a730906986 100644 --- a/packages/ui/src/locales/en-US/index.json +++ b/packages/ui/src/locales/en-US/index.json @@ -524,12 +524,18 @@ "content.diff-modal.updated-count": { "defaultMessage": "{count} updated" }, + "content.filter.client-only": { + "defaultMessage": "Client-Only" + }, "content.filter.disabled": { "defaultMessage": "Disabled" }, "content.filter.enabled": { "defaultMessage": "Enabled" }, + "content.filter.server-only": { + "defaultMessage": "Server-Only" + }, "content.filter.updates": { "defaultMessage": "Updates" },