diff --git a/apps/app-frontend/package.json b/apps/app-frontend/package.json index c7561a588b..2352839df3 100644 --- a/apps/app-frontend/package.json +++ b/apps/app-frontend/package.json @@ -28,17 +28,18 @@ "@tauri-apps/plugin-updater": "^2.7.1", "@tauri-apps/plugin-window-state": "^2.2.2", "@types/three": "^0.172.0", - "intl-messageformat": "^10.7.7", - "vue-i18n": "^10.0.0", "@vueuse/core": "^11.1.0", "dayjs": "^1.11.10", "floating-vue": "^5.2.2", + "fuse.js": "^6.6.2", + "intl-messageformat": "^10.7.7", "ofetch": "^1.3.4", "pinia": "^3.0.0", "posthog-js": "^1.158.2", "three": "^0.172.0", "vite-svg-loader": "^5.1.0", "vue": "^3.5.13", + "vue-i18n": "^10.0.0", "vue-multiselect": "3.0.0", "vue-router": "^4.6.0", "vue-virtual-scroller": "v2.0.0-beta.8" diff --git a/apps/app-frontend/src/pages/instance/Mods.vue b/apps/app-frontend/src/pages/instance/Mods.vue index e91b43f0bd..6c915c63a4 100644 --- a/apps/app-frontend/src/pages/instance/Mods.vue +++ b/apps/app-frontend/src/pages/instance/Mods.vue @@ -1,241 +1,228 @@ + + isBulkOperating.value = true + bulkOperation.value = 'enable' + bulkTotal.value = itemsToToggle.length + bulkProgress.value = 0 - - +onUnmounted(() => { + unlisten() + unlistenProfiles() + if (refreshInterval) { + clearInterval(refreshInterval) + } +}) + diff --git a/apps/app-frontend/src/pages/instance/Mods_old.vue b/apps/app-frontend/src/pages/instance/Mods_old.vue new file mode 100644 index 0000000000..e91b43f0bd --- /dev/null +++ b/apps/app-frontend/src/pages/instance/Mods_old.vue @@ -0,0 +1,1183 @@ + + + + + + diff --git a/apps/frontend/src/assets/styles/components.scss b/apps/frontend/src/assets/styles/components.scss index 357e33a3c1..21a5ccbf3d 100644 --- a/apps/frontend/src/assets/styles/components.scss +++ b/apps/frontend/src/assets/styles/components.scss @@ -223,10 +223,7 @@ :where(input) { box-sizing: border-box; max-height: 40px; - - &:not(.stylized-toggle) { - max-width: 100%; - } + max-width: 100%; } :where(.adjacent-input, &.adjacent-input) { @@ -271,10 +268,6 @@ &:not(&.small) { flex-direction: column; align-items: flex-start; - - .stylized-toggle { - flex-basis: 0; - } } } } @@ -650,64 +643,6 @@ tr.button-transparent { } } -.switch { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - -webkit-tap-highlight-color: transparent; - cursor: pointer; - - &:focus { - //outline: 0; Bad for accessibility - } -} - -.stylized-toggle { - @extend .button-base; - - box-sizing: content-box; - min-height: 32px; - height: 32px; - width: 52px; - max-width: 52px; - border-radius: var(--size-rounded-max); - display: inline-block; - position: relative; - margin: 0; - transition: all 0.2s ease; - background: var(--color-button-bg); - - &:after { - content: ''; - position: absolute; - top: 7px; - left: 7px; - width: 18px; - height: 18px; - border-radius: 50%; - background: var(--color-toggle-handle); - transition: all 0.2s cubic-bezier(0.5, 0.1, 0.75, 1.35); - outline: 2px solid transparent; - - @media (prefers-reduced-motion) { - transition: none; - } - } - - &:checked { - background-color: var(--color-brand); - - &:after { - transform: translatex(20px); - background: var(--color-brand-inverted); - } - } - - &:hover &:focus { - background: var(--color-button-bg); - } -} - .textarea-wrapper { display: flex; flex-direction: column; diff --git a/apps/frontend/src/assets/styles/global.scss b/apps/frontend/src/assets/styles/global.scss index 51c554d8d7..a01337a9a4 100644 --- a/apps/frontend/src/assets/styles/global.scss +++ b/apps/frontend/src/assets/styles/global.scss @@ -60,8 +60,6 @@ html { --color-button-bg-active: #c3c6cb; --color-button-text-active: var(--color-button-text-hover); - --color-toggle-handle: var(--color-icon); - --color-dropdown-bg: var(--color-button-bg); --color-dropdown-text: var(--color-button-text); @@ -177,8 +175,6 @@ html { --color-button-bg-active: #616570; --color-button-text-active: var(--color-button-text-hover); - --color-toggle-handle: var(--color-button-text); - --color-dropdown-bg: var(--color-button-bg); --color-dropdown-text: var(--color-button-text); diff --git a/apps/frontend/src/components/ui/servers/PlatformChangeModpackVersionModal.vue b/apps/frontend/src/components/ui/servers/PlatformChangeModpackVersionModal.vue index 0b31fce762..2c9072a731 100644 --- a/apps/frontend/src/components/ui/servers/PlatformChangeModpackVersionModal.vue +++ b/apps/frontend/src/components/ui/servers/PlatformChangeModpackVersionModal.vue @@ -32,12 +32,7 @@ - +
If enabled, existing mods, worlds, and configurations, will be deleted before installing @@ -69,7 +64,7 @@ - - diff --git a/apps/frontend/src/components/ui/servers/PlatformMrpackModal.vue b/apps/frontend/src/components/ui/servers/PlatformMrpackModal.vue index 436b938f80..d3d76f60f6 100644 --- a/apps/frontend/src/components/ui/servers/PlatformMrpackModal.vue +++ b/apps/frontend/src/components/ui/servers/PlatformMrpackModal.vue @@ -53,12 +53,7 @@ - +
Removes all data on your server, including your worlds, mods, and configuration @@ -128,6 +123,7 @@ import { ButtonStyled, injectNotificationManager, NewModal, + Toggle, } from '@modrinth/ui' import { ModrinthServersFetchError } from '@modrinth/utils' import { onMounted, onUnmounted } from 'vue' @@ -255,9 +251,3 @@ const hide = () => mrpackModal.value?.hide() defineExpose({ show, hide }) - - diff --git a/apps/frontend/src/components/ui/servers/PlatformVersionSelectModal.vue b/apps/frontend/src/components/ui/servers/PlatformVersionSelectModal.vue index 71b5609f89..c465180bb2 100644 --- a/apps/frontend/src/components/ui/servers/PlatformVersionSelectModal.vue +++ b/apps/frontend/src/components/ui/servers/PlatformVersionSelectModal.vue @@ -143,12 +143,7 @@ - +
Removes all data on your server, including your worlds, mods, and configuration files, @@ -542,9 +537,3 @@ const hide = () => versionSelectModal.value?.hide() defineExpose({ show, hide }) - - diff --git a/apps/frontend/src/components/ui/servers/ServerInstallation.vue b/apps/frontend/src/components/ui/servers/ServerInstallation.vue index 56ac895f4d..88a7db3ffe 100644 --- a/apps/frontend/src/components/ui/servers/ServerInstallation.vue +++ b/apps/frontend/src/components/ui/servers/ServerInstallation.vue @@ -274,10 +274,6 @@ watch( diff --git a/apps/frontend/src/pages/flags.vue b/apps/frontend/src/pages/flags.vue index 8c7376c253..18536bc08a 100644 --- a/apps/frontend/src/pages/flags.vue +++ b/apps/frontend/src/pages/flags.vue @@ -1,67 +1,76 @@ - - diff --git a/apps/frontend/src/pages/hosting/manage/[id]/content.vue b/apps/frontend/src/pages/hosting/manage/[id]/content.vue index 5a0bb5d922..2cf5866be1 100644 --- a/apps/frontend/src/pages/hosting/manage/[id]/content.vue +++ b/apps/frontend/src/pages/hosting/manage/[id]/content.vue @@ -1,21 +1,13 @@ - - + + diff --git a/apps/frontend/src/pages/hosting/manage/[id]/content/index.vue b/apps/frontend/src/pages/hosting/manage/[id]/content/index.vue deleted file mode 100644 index f4bdc02862..0000000000 --- a/apps/frontend/src/pages/hosting/manage/[id]/content/index.vue +++ /dev/null @@ -1,706 +0,0 @@ - - - - - diff --git a/apps/frontend/src/pages/hosting/manage/[id]/content_old.vue b/apps/frontend/src/pages/hosting/manage/[id]/content_old.vue new file mode 100644 index 0000000000..6e3098125a --- /dev/null +++ b/apps/frontend/src/pages/hosting/manage/[id]/content_old.vue @@ -0,0 +1,21 @@ + + + diff --git a/apps/frontend/src/pages/hosting/manage/[id]/options/preferences.vue b/apps/frontend/src/pages/hosting/manage/[id]/options/preferences.vue index bb81742597..4102a6b5c7 100644 --- a/apps/frontend/src/pages/hosting/manage/[id]/options/preferences.vue +++ b/apps/frontend/src/pages/hosting/manage/[id]/options/preferences.vue @@ -21,11 +21,10 @@
{{ prefConfig.description }} - @@ -42,7 +41,7 @@ - - diff --git a/apps/frontend/src/pages/hosting/manage/[id]/options/properties.vue b/apps/frontend/src/pages/hosting/manage/[id]/options/properties.vue index 73f552282a..16c57e7ace 100644 --- a/apps/frontend/src/pages/hosting/manage/[id]/options/properties.vue +++ b/apps/frontend/src/pages/hosting/manage/[id]/options/properties.vue @@ -62,11 +62,9 @@ />
-
@@ -134,7 +132,7 @@ - - diff --git a/apps/frontend/src/pages/hosting/manage/[id]/options/startup.vue b/apps/frontend/src/pages/hosting/manage/[id]/options/startup.vue index c9699c2f28..50ff0fb78b 100644 --- a/apps/frontend/src/pages/hosting/manage/[id]/options/startup.vue +++ b/apps/frontend/src/pages/hosting/manage/[id]/options/startup.vue @@ -69,12 +69,7 @@
- +
import { IssuesIcon, UpdatedIcon } from '@modrinth/assets' -import { ButtonStyled, Combobox, injectNotificationManager } from '@modrinth/ui' +import { ButtonStyled, Combobox, injectNotificationManager, Toggle } from '@modrinth/ui' import SaveBanner from '~/components/ui/servers/SaveBanner.vue' import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts' @@ -232,9 +227,3 @@ function resetToDefault() { invocation.value = originalInvocation.value ?? '' } - - diff --git a/apps/frontend/src/pages/settings/index.vue b/apps/frontend/src/pages/settings/index.vue index ad93ccbec6..a737dbc3bc 100644 --- a/apps/frontend/src/pages/settings/index.vue +++ b/apps/frontend/src/pages/settings/index.vue @@ -119,85 +119,78 @@

{{ formatMessage(toggleFeatures.title) }}

{{ formatMessage(toggleFeatures.description) }}

-
- - -
-
- - -
-
- - -
-
- - -
-
- - +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
@@ -212,6 +205,7 @@ import { IntlFormatted, normalizeChildren, ThemeSelector, + Toggle, useVIntl, } from '@modrinth/ui' import { formatProjectType } from '@modrinth/utils' diff --git a/packages/api-client/src/modules/index.ts b/packages/api-client/src/modules/index.ts index 82880a682d..7827bbcf4f 100644 --- a/packages/api-client/src/modules/index.ts +++ b/packages/api-client/src/modules/index.ts @@ -7,7 +7,7 @@ import { ArchonServersV0Module } from './archon/servers/v0' import { ArchonServersV1Module } from './archon/servers/v1' import { ISO3166Module } from './iso3166' import { KyrosFilesV0Module } from './kyros/files/v0' -import { LabrinthVersionsV3Module } from './labrinth' +import { LabrinthVersionsV2Module, LabrinthVersionsV3Module } from './labrinth' import { LabrinthBillingInternalModule } from './labrinth/billing/internal' import { LabrinthCollectionsModule } from './labrinth/collections' import { LabrinthProjectsV2Module } from './labrinth/projects/v2' @@ -40,6 +40,7 @@ export const MODULE_REGISTRY = { labrinth_projects_v3: LabrinthProjectsV3Module, labrinth_state: LabrinthStateModule, labrinth_tech_review_internal: LabrinthTechReviewInternalModule, + labrinth_versions_v2: LabrinthVersionsV2Module, labrinth_versions_v3: LabrinthVersionsV3Module, } as const satisfies Record diff --git a/packages/api-client/src/modules/labrinth/index.ts b/packages/api-client/src/modules/labrinth/index.ts index 38bc3f22b6..c11216dfe6 100644 --- a/packages/api-client/src/modules/labrinth/index.ts +++ b/packages/api-client/src/modules/labrinth/index.ts @@ -4,4 +4,5 @@ export * from './projects/v2' export * from './projects/v3' export * from './state' export * from './tech-review/internal' +export * from './versions/v2' export * from './versions/v3' diff --git a/packages/api-client/src/modules/labrinth/types.ts b/packages/api-client/src/modules/labrinth/types.ts index c36e1cb1c1..16546fea30 100644 --- a/packages/api-client/src/modules/labrinth/types.ts +++ b/packages/api-client/src/modules/labrinth/types.ts @@ -423,6 +423,12 @@ export namespace Labrinth { game_versions: string[] loaders: string[] } + + export interface GetProjectVersionsParams { + game_versions?: string[] + loaders?: string[] + include_changelog?: boolean + } } // TODO: consolidate duplicated types between v2 and v3 versions @@ -437,6 +443,7 @@ export namespace Labrinth { export interface GetProjectVersionsParams { game_versions?: string[] loaders?: string[] + include_changelog?: boolean } export type VersionChannel = 'release' | 'beta' | 'alpha' diff --git a/packages/api-client/src/modules/labrinth/versions/v2.ts b/packages/api-client/src/modules/labrinth/versions/v2.ts new file mode 100644 index 0000000000..750d91d53d --- /dev/null +++ b/packages/api-client/src/modules/labrinth/versions/v2.ts @@ -0,0 +1,135 @@ +import { AbstractModule } from '../../../core/abstract-module' +import type { Labrinth } from '../types' + +export class LabrinthVersionsV2Module extends AbstractModule { + public getModuleID(): string { + return 'labrinth_versions_v2' + } + + /** + * Get versions for a project (v2) + * + * @param id - Project ID or slug (e.g., 'sodium' or 'AANobbMI') + * @param options - Optional query parameters to filter versions + * @returns Promise resolving to an array of v2 versions + * + * @example + * ```typescript + * const versions = await client.labrinth.versions_v2.getProjectVersions('sodium') + * const filteredVersions = await client.labrinth.versions_v2.getProjectVersions('sodium', { + * game_versions: ['1.20.1'], + * loaders: ['fabric'], + * include_changelog: false + * }) + * console.log(versions[0].version_number) + * ``` + */ + public async getProjectVersions( + id: string, + options?: Labrinth.Versions.v2.GetProjectVersionsParams, + ): Promise { + const params: Record = {} + if (options?.game_versions?.length) { + params.game_versions = JSON.stringify(options.game_versions) + } + if (options?.loaders?.length) { + params.loaders = JSON.stringify(options.loaders) + } + if (options?.include_changelog === false) { + params.include_changelog = 'false' + } + + return this.client.request(`/project/${id}/version`, { + api: 'labrinth', + version: 2, + method: 'GET', + params: Object.keys(params).length > 0 ? params : undefined, + }) + } + + /** + * Get a specific version by ID (v2) + * + * @param id - Version ID + * @returns Promise resolving to the v2 version data + * + * @example + * ```typescript + * const version = await client.labrinth.versions_v2.getVersion('DXtmvS8i') + * console.log(version.version_number) + * ``` + */ + public async getVersion(id: string): Promise { + return this.client.request(`/version/${id}`, { + api: 'labrinth', + version: 2, + method: 'GET', + }) + } + + /** + * Get multiple versions by IDs (v2) + * + * @param ids - Array of version IDs + * @returns Promise resolving to an array of v2 versions + * + * @example + * ```typescript + * const versions = await client.labrinth.versions_v2.getVersions(['DXtmvS8i', 'abc123']) + * console.log(versions[0].version_number) + * ``` + */ + public async getVersions(ids: string[]): Promise { + return this.client.request(`/versions`, { + api: 'labrinth', + version: 2, + method: 'GET', + params: { ids: JSON.stringify(ids) }, + }) + } + + /** + * Get a version from a project by version ID or number (v2) + * + * @param projectId - Project ID or slug + * @param versionId - Version ID or version number + * @returns Promise resolving to the v2 version data + * + * @example + * ```typescript + * const version = await client.labrinth.versions_v2.getVersionFromIdOrNumber('sodium', 'DXtmvS8i') + * const versionByNumber = await client.labrinth.versions_v2.getVersionFromIdOrNumber('sodium', '0.4.12') + * ``` + */ + public async getVersionFromIdOrNumber( + projectId: string, + versionId: string, + ): Promise { + return this.client.request( + `/project/${projectId}/version/${versionId}`, + { + api: 'labrinth', + version: 2, + method: 'GET', + }, + ) + } + + /** + * Delete a version by ID (v2) + * + * @param versionId - Version ID + * + * @example + * ```typescript + * await client.labrinth.versions_v2.deleteVersion('DXtmvS8i') + * ``` + */ + public async deleteVersion(versionId: string): Promise { + return this.client.request(`/version/${versionId}`, { + api: 'labrinth', + version: 2, + method: 'DELETE', + }) + } +} diff --git a/packages/api-client/src/modules/labrinth/versions/v3.ts b/packages/api-client/src/modules/labrinth/versions/v3.ts index 914b030de7..e3ef64abd5 100644 --- a/packages/api-client/src/modules/labrinth/versions/v3.ts +++ b/packages/api-client/src/modules/labrinth/versions/v3.ts @@ -35,6 +35,9 @@ export class LabrinthVersionsV3Module extends AbstractModule { if (options?.loaders?.length) { params.loaders = JSON.stringify(options.loaders) } + if (options?.include_changelog === false) { + params.include_changelog = 'false' + } return this.client.request(`/project/${id}/version`, { api: 'labrinth', diff --git a/packages/api-client/src/platform/nuxt.ts b/packages/api-client/src/platform/nuxt.ts index 15a3613399..b74fd0bd4f 100644 --- a/packages/api-client/src/platform/nuxt.ts +++ b/packages/api-client/src/platform/nuxt.ts @@ -13,27 +13,32 @@ import { XHRUploadClient } from './xhr-upload-client' * * This provides cross-request persistence in SSR while also working in client-side. * State is shared between requests in the same Nuxt context. + * + * Note: useState must be called during initialization (in setup context) and cached, + * as it won't work during async operations when the Nuxt context may be lost. */ export class NuxtCircuitBreakerStorage implements CircuitBreakerStorage { - private getState(): Map { + private state: Map + + constructor() { // @ts-expect-error - useState is provided by Nuxt runtime - const state = useState>( + const stateRef = useState>( 'circuit-breaker-state', () => new Map(), ) - return state.value + this.state = stateRef.value } get(key: string): CircuitBreakerState | undefined { - return this.getState().get(key) + return this.state.get(key) } set(key: string, state: CircuitBreakerState): void { - this.getState().set(key, state) + this.state.set(key, state) } clear(key: string): void { - this.getState().delete(key) + this.state.delete(key) } } diff --git a/packages/assets/styles/classes.scss b/packages/assets/styles/classes.scss index d4d6a72c0a..a0d62bf970 100644 --- a/packages/assets/styles/classes.scss +++ b/packages/assets/styles/classes.scss @@ -70,10 +70,7 @@ :where(input) { box-sizing: border-box; max-height: 40px; - - &:not(.stylized-toggle) { - max-width: 100%; - } + max-width: 100%; } :where(.adjacent-input, &.adjacent-input) { @@ -118,10 +115,6 @@ &:not(&.small) { flex-direction: column; align-items: start; - - .stylized-toggle { - flex-basis: 0; - } } } } @@ -766,60 +759,6 @@ a, } } -.switch { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - -webkit-tap-highlight-color: transparent; - cursor: pointer; -} - -.stylized-toggle { - @extend .button-base; - - box-sizing: content-box; - min-height: 32px; - height: 32px; - min-width: 52px; - max-width: 52px; - border-radius: var(--radius-max); - display: inline-block; - position: relative; - margin: 0; - transition: all 0.2s ease; - background: var(--color-button-bg); - - &:after { - content: ''; - position: absolute; - top: 7px; - left: 7px; - width: 18px; - height: 18px; - border-radius: 50%; - background: var(--color-gray); - transition: all 0.2s cubic-bezier(0.5, 0.1, 0.75, 1.35); - outline: 2px solid transparent; - - @media (prefers-reduced-motion) { - transition: none; - } - } - - &:checked { - background-color: var(--color-brand); - - &:after { - transform: translatex(20px); - background: var(--color-accent-contrast); - } - } - - &:hover &:focus { - background: var(--color-button-bg); - } -} - // TOOLTIPS .v-popper--theme-dropdown, diff --git a/packages/tooling-config/tailwind/tailwind-preset.ts b/packages/tooling-config/tailwind/tailwind-preset.ts index 3c6ca2a69e..e566b3f8ac 100644 --- a/packages/tooling-config/tailwind/tailwind-preset.ts +++ b/packages/tooling-config/tailwind/tailwind-preset.ts @@ -224,7 +224,7 @@ const config: Config = { hr: 'var(--color-hr)', table: { border: 'var(--color-table-border)', - alternateRow: ' var(--color-table-alternate-row)', + alternateRow: 'var(--color-table-alternate-row)', }, }, backgroundImage: { diff --git a/packages/ui/.storybook/preview.ts b/packages/ui/.storybook/preview.ts index 2c3f329bcb..53ae7e64c1 100644 --- a/packages/ui/.storybook/preview.ts +++ b/packages/ui/.storybook/preview.ts @@ -2,6 +2,7 @@ import '@modrinth/assets/omorphia.scss' import 'floating-vue/dist/style.css' import '../src/styles/tailwind.css' +import { GenericModrinthClient } from '@modrinth/api-client' import { withThemeByClassName } from '@storybook/addon-themes' import type { Preview } from '@storybook/vue3-vite' import { setup } from '@storybook/vue3-vite' @@ -17,7 +18,10 @@ import { } from '../src/composables/i18n' import { AbstractWebNotificationManager, + I18N_INJECTION_KEY, + type I18nContext, type NotificationPanelLocation, + provideModrinthClient, provideNotificationManager, type WebNotification, } from '../src/providers' @@ -77,6 +81,17 @@ class StorybookNotificationManager extends AbstractWebNotificationManager { setup((app) => { app.use(i18n) + + // Provide the custom I18nContext for components using injectI18n() + const i18nContext: I18nContext = { + locale: i18n.global.locale, + t: (key, values) => i18n.global.t(key, values ?? {}) as string, + setLocale: (newLocale) => { + i18n.global.locale.value = newLocale + }, + } + app.provide(I18N_INJECTION_KEY, i18nContext) + app.use(FloatingVue, { themes: { 'ribbit-popout': { @@ -100,10 +115,15 @@ setup((app) => { } }) -// Wrapper component that provides notification manager context -const NotificationManagerProvider = defineComponent({ +const StorybookProvider = defineComponent({ setup(_, { slots }) { provideNotificationManager(new StorybookNotificationManager()) + + const modrinthClient = new GenericModrinthClient({ + userAgent: 'modrinth-storybook/1.0.0', + }) + provideModrinthClient(modrinthClient) + return () => slots.default?.() }, }) @@ -126,14 +146,13 @@ const preview: Preview = { }, defaultTheme: 'dark', }), - // Wrap stories with notification manager provider (story) => ({ - components: { story, NotificationManagerProvider, NotificationPanel }, + components: { story, StorybookProvider, NotificationPanel }, template: /*html*/ ` - + - + `, }), ], diff --git a/packages/ui/src/components/base/Avatar.vue b/packages/ui/src/components/base/Avatar.vue index 06c42f8775..90a2bd40e6 100644 --- a/packages/ui/src/components/base/Avatar.vue +++ b/packages/ui/src/components/base/Avatar.vue @@ -1,6 +1,6 @@