From c82c4ddc5b7b3aca52aeab4e11c0c4ffc1c6b53b Mon Sep 17 00:00:00 2001 From: Prospector Date: Mon, 6 Jan 2025 16:49:18 -0800 Subject: [PATCH 1/8] Add users, orgs, and new project cards to App --- apps/app-frontend/src/App.vue | 56 +- .../src/components/RowDisplay.vue | 46 +- .../src/components/ui/Instance.vue | 35 +- .../src/components/ui/InstanceIndicator.vue | 16 +- .../src/components/ui/ProjectCard.vue | 42 +- .../src/components/ui/ProjectCardActions.vue | 182 ++++++ .../src/components/ui/SearchCard.vue | 199 ++---- .../components/ui/modal/AppSettingsModal.vue | 4 - .../ui/settings/FeatureFlagSettings.vue | 45 +- .../src/composables/instance-context.ts | 42 ++ apps/app-frontend/src/helpers/types.d.ts | 12 + .../app-frontend/src/locales/en-US/index.json | 12 + apps/app-frontend/src/pages/Browse.vue | 150 ++--- .../app-frontend/src/pages/instance/Index.vue | 4 +- .../app-frontend/src/pages/library/Custom.vue | 2 +- .../src/pages/library/Downloaded.vue | 2 +- .../src/pages/library/Overview.vue | 2 +- .../src/pages/organization/Index.vue | 155 +++++ .../src/pages/organization/index.js | 3 + apps/app-frontend/src/pages/project/Index.vue | 32 +- apps/app-frontend/src/pages/user/Index.vue | 129 ++++ apps/app-frontend/src/pages/user/index.js | 3 + apps/app-frontend/src/routes.js | 22 + apps/app-frontend/src/store/theme.js | 8 +- apps/frontend/src/assets/styles/global.scss | 3 +- apps/frontend/src/components/ui/Badge.vue | 2 +- apps/frontend/src/pages/[type]/[id].vue | 4 +- apps/frontend/src/pages/organization/[id].vue | 6 +- apps/frontend/src/pages/plus.vue | 2 +- .../src/pages/search/[searchProjectType].vue | 14 +- apps/frontend/src/pages/user/[id].vue | 135 ++-- apps/frontend/src/utils/permissions.ts | 7 +- packages/app-lib/src/state/settings.rs | 1 + .../assets}/badges/10m-club.svg | 0 .../assets}/badges/alpha-tester.svg | 0 .../assets}/badges/beta-tester.svg | 0 packages/assets/badges/contributor.svg | 8 + .../assets}/badges/early-adopter.svg | 0 .../images => packages/assets}/badges/mod.svg | 0 .../assets}/badges/plus.svg | 0 .../assets}/badges/staff.svg | 0 packages/assets/badges/translator.svg | 14 + packages/assets/icons/badge-check.svg | 1 + packages/assets/icons/category/adventure.svg | 1 + packages/assets/icons/category/atmosphere.svg | 1 + packages/assets/icons/category/audio.svg | 1 + packages/assets/icons/category/blocks.svg | 1 + packages/assets/icons/category/bloom.svg | 1 + packages/assets/icons/category/cartoon.svg | 1 + .../assets/icons/category/challenging.svg | 1 + .../icons/category/colored-lighting.svg | 1 + packages/assets/icons/category/combat.svg | 1 + .../assets/icons/category/core-shaders.svg | 1 + packages/assets/icons/category/cursed.svg | 1 + packages/assets/icons/category/decoration.svg | 1 + packages/assets/icons/category/economy.svg | 1 + packages/assets/icons/category/entities.svg | 5 + .../assets/icons/category/environment.svg | 1 + packages/assets/icons/category/equipment.svg | 1 + packages/assets/icons/category/fantasy.svg | 1 + packages/assets/icons/category/foliage.svg | 1 + packages/assets/icons/category/fonts.svg | 1 + packages/assets/icons/category/food.svg | 1 + .../assets/icons/category/game-mechanics.svg | 1 + packages/assets/icons/category/gui.svg | 1 + packages/assets/icons/category/high.svg | 1 + packages/assets/icons/category/items.svg | 1 + .../assets/icons/category/kitchen-sink.svg | 1 + .../assets/icons/category/lightweight.svg | 1 + packages/assets/icons/category/locale.svg | 1 + packages/assets/icons/category/low.svg | 1 + packages/assets/icons/category/magic.svg | 1 + packages/assets/icons/category/management.svg | 1 + packages/assets/icons/category/medium.svg | 1 + packages/assets/icons/category/minigame.svg | 1 + packages/assets/icons/category/mobs.svg | 1 + packages/assets/icons/category/modded.svg | 1 + packages/assets/icons/category/models.svg | 1 + .../assets/icons/category/multiplayer.svg | 1 + .../assets/icons/category/optimization.svg | 1 + .../assets/icons/category/path-tracing.svg | 1 + packages/assets/icons/category/pbr.svg | 1 + packages/assets/icons/category/potato.svg | 1 + packages/assets/icons/category/quests.svg | 1 + packages/assets/icons/category/realistic.svg | 1 + .../assets/icons/category/reflections.svg | 1 + packages/assets/icons/category/screenshot.svg | 1 + .../assets/icons/category/semi-realistic.svg | 1 + packages/assets/icons/category/shadows.svg | 1 + packages/assets/icons/category/simplistic.svg | 1 + packages/assets/icons/category/social.svg | 1 + packages/assets/icons/category/storage.svg | 1 + packages/assets/icons/category/technology.svg | 1 + packages/assets/icons/category/themed.svg | 1 + .../assets/icons/category/transportation.svg | 1 + packages/assets/icons/category/tweaks.svg | 1 + packages/assets/icons/category/utility.svg | 1 + .../assets/icons/category/vanilla-like.svg | 1 + packages/assets/icons/category/worldgen.svg | 1 + packages/assets/icons/platform/bukkit.svg | 1 + packages/assets/icons/platform/bungeecord.svg | 7 + packages/assets/icons/platform/canvas.svg | 1 + packages/assets/icons/platform/fabric.svg | 4 + packages/assets/icons/platform/folia.svg | 1 + packages/assets/icons/platform/forge.svg | 4 + packages/assets/icons/platform/iris.svg | 1 + packages/assets/icons/platform/liteloader.svg | 1 + packages/assets/icons/platform/modloader.svg | 1 + packages/assets/icons/platform/neoforge.svg | 1 + packages/assets/icons/platform/optifine.svg | 1 + packages/assets/icons/platform/paper.svg | 6 + packages/assets/icons/platform/purpur.svg | 9 + packages/assets/icons/platform/quilt.svg | 10 + packages/assets/icons/platform/rift.svg | 1 + packages/assets/icons/platform/spigot.svg | 1 + packages/assets/icons/platform/sponge.svg | 1 + packages/assets/icons/platform/vanilla.svg | 1 + packages/assets/icons/platform/velocity.svg | 1 + packages/assets/icons/platform/waterfall.svg | 1 + packages/assets/index.ts | 178 ++++++ packages/assets/styles/classes.scss | 5 +- packages/assets/styles/defaults.scss | 3 +- packages/assets/styles/variables.scss | 15 +- packages/ui/index.ts | 3 + packages/ui/src/components/base/AutoLink.vue | 28 +- packages/ui/src/components/base/Badge.vue | 40 +- .../ui/src/components/base/ButtonStyled.vue | 9 +- .../src/components/base/ContentPageHeader.vue | 2 +- packages/ui/src/components/base/NavTabs.vue | 208 ++++++ .../ui/src/components/base/OverflowMenu.vue | 8 +- .../ui/src/components/base/PopoutMenu.vue | 16 +- .../ui/src/components/base/SmartClickable.vue | 58 ++ packages/ui/src/components/base/StatItem.vue | 2 +- packages/ui/src/components/base/TagItem.vue | 4 +- packages/ui/src/components/index.ts | 13 + .../ui/src/components/modal/TabbedModal.vue | 16 +- .../organization/OrganizationHeader.vue | 71 ++ .../OrganizationSidebarMembers.vue | 73 +++ .../src/components/project/NewProjectCard.vue | 342 ++++++++-- .../project/ProjectBackgroundGradient.vue | 26 +- .../src/components/project/ProjectHeader.vue | 6 +- .../project/ProjectSidebarCreators.vue | 34 +- .../src/components/project/ProjectsList.vue | 173 +++++ packages/ui/src/components/user/UserBadge.vue | 100 +++ .../ui/src/components/user/UserHeader.vue | 92 +++ .../src/components/user/UserSidebarBadges.vue | 82 +++ .../user/UserSidebarOrganizations.vue | 39 ++ packages/ui/src/locales/en-US/index.json | 506 ++++++++++++++- packages/ui/src/utils/common-messages.ts | 48 ++ packages/ui/src/utils/link.ts | 36 ++ packages/ui/src/utils/project-types.ts | 77 +++ packages/ui/src/utils/search.ts | 12 +- packages/ui/src/utils/tags.ts | 605 ++++++++++++++++++ packages/utils/color.ts | 36 ++ packages/utils/index.ts | 2 + packages/utils/permissions.ts | 4 + packages/utils/types.ts | 115 +++- 157 files changed, 3932 insertions(+), 669 deletions(-) create mode 100644 apps/app-frontend/src/components/ui/ProjectCardActions.vue create mode 100644 apps/app-frontend/src/composables/instance-context.ts create mode 100644 apps/app-frontend/src/pages/organization/Index.vue create mode 100644 apps/app-frontend/src/pages/organization/index.js create mode 100644 apps/app-frontend/src/pages/user/Index.vue create mode 100644 apps/app-frontend/src/pages/user/index.js rename {apps/frontend/src/assets/images => packages/assets}/badges/10m-club.svg (100%) rename {apps/frontend/src/assets/images => packages/assets}/badges/alpha-tester.svg (100%) rename {apps/frontend/src/assets/images => packages/assets}/badges/beta-tester.svg (100%) create mode 100644 packages/assets/badges/contributor.svg rename {apps/frontend/src/assets/images => packages/assets}/badges/early-adopter.svg (100%) rename {apps/frontend/src/assets/images => packages/assets}/badges/mod.svg (100%) rename {apps/frontend/src/assets/images => packages/assets}/badges/plus.svg (100%) rename {apps/frontend/src/assets/images => packages/assets}/badges/staff.svg (100%) create mode 100644 packages/assets/badges/translator.svg create mode 100644 packages/assets/icons/badge-check.svg create mode 100644 packages/assets/icons/category/adventure.svg create mode 100644 packages/assets/icons/category/atmosphere.svg create mode 100644 packages/assets/icons/category/audio.svg create mode 100644 packages/assets/icons/category/blocks.svg create mode 100644 packages/assets/icons/category/bloom.svg create mode 100644 packages/assets/icons/category/cartoon.svg create mode 100644 packages/assets/icons/category/challenging.svg create mode 100644 packages/assets/icons/category/colored-lighting.svg create mode 100644 packages/assets/icons/category/combat.svg create mode 100644 packages/assets/icons/category/core-shaders.svg create mode 100644 packages/assets/icons/category/cursed.svg create mode 100644 packages/assets/icons/category/decoration.svg create mode 100644 packages/assets/icons/category/economy.svg create mode 100644 packages/assets/icons/category/entities.svg create mode 100644 packages/assets/icons/category/environment.svg create mode 100644 packages/assets/icons/category/equipment.svg create mode 100644 packages/assets/icons/category/fantasy.svg create mode 100644 packages/assets/icons/category/foliage.svg create mode 100644 packages/assets/icons/category/fonts.svg create mode 100644 packages/assets/icons/category/food.svg create mode 100644 packages/assets/icons/category/game-mechanics.svg create mode 100644 packages/assets/icons/category/gui.svg create mode 100644 packages/assets/icons/category/high.svg create mode 100644 packages/assets/icons/category/items.svg create mode 100644 packages/assets/icons/category/kitchen-sink.svg create mode 100644 packages/assets/icons/category/lightweight.svg create mode 100644 packages/assets/icons/category/locale.svg create mode 100644 packages/assets/icons/category/low.svg create mode 100644 packages/assets/icons/category/magic.svg create mode 100644 packages/assets/icons/category/management.svg create mode 100644 packages/assets/icons/category/medium.svg create mode 100644 packages/assets/icons/category/minigame.svg create mode 100644 packages/assets/icons/category/mobs.svg create mode 100644 packages/assets/icons/category/modded.svg create mode 100644 packages/assets/icons/category/models.svg create mode 100644 packages/assets/icons/category/multiplayer.svg create mode 100644 packages/assets/icons/category/optimization.svg create mode 100644 packages/assets/icons/category/path-tracing.svg create mode 100644 packages/assets/icons/category/pbr.svg create mode 100644 packages/assets/icons/category/potato.svg create mode 100644 packages/assets/icons/category/quests.svg create mode 100644 packages/assets/icons/category/realistic.svg create mode 100644 packages/assets/icons/category/reflections.svg create mode 100644 packages/assets/icons/category/screenshot.svg create mode 100644 packages/assets/icons/category/semi-realistic.svg create mode 100644 packages/assets/icons/category/shadows.svg create mode 100644 packages/assets/icons/category/simplistic.svg create mode 100644 packages/assets/icons/category/social.svg create mode 100644 packages/assets/icons/category/storage.svg create mode 100644 packages/assets/icons/category/technology.svg create mode 100644 packages/assets/icons/category/themed.svg create mode 100644 packages/assets/icons/category/transportation.svg create mode 100644 packages/assets/icons/category/tweaks.svg create mode 100644 packages/assets/icons/category/utility.svg create mode 100644 packages/assets/icons/category/vanilla-like.svg create mode 100644 packages/assets/icons/category/worldgen.svg create mode 100644 packages/assets/icons/platform/bukkit.svg create mode 100644 packages/assets/icons/platform/bungeecord.svg create mode 100644 packages/assets/icons/platform/canvas.svg create mode 100644 packages/assets/icons/platform/fabric.svg create mode 100644 packages/assets/icons/platform/folia.svg create mode 100644 packages/assets/icons/platform/forge.svg create mode 100644 packages/assets/icons/platform/iris.svg create mode 100644 packages/assets/icons/platform/liteloader.svg create mode 100644 packages/assets/icons/platform/modloader.svg create mode 100644 packages/assets/icons/platform/neoforge.svg create mode 100644 packages/assets/icons/platform/optifine.svg create mode 100644 packages/assets/icons/platform/paper.svg create mode 100644 packages/assets/icons/platform/purpur.svg create mode 100644 packages/assets/icons/platform/quilt.svg create mode 100644 packages/assets/icons/platform/rift.svg create mode 100644 packages/assets/icons/platform/spigot.svg create mode 100644 packages/assets/icons/platform/sponge.svg create mode 100644 packages/assets/icons/platform/vanilla.svg create mode 100644 packages/assets/icons/platform/velocity.svg create mode 100644 packages/assets/icons/platform/waterfall.svg create mode 100644 packages/ui/src/components/base/NavTabs.vue create mode 100644 packages/ui/src/components/base/SmartClickable.vue create mode 100644 packages/ui/src/components/organization/OrganizationHeader.vue create mode 100644 packages/ui/src/components/organization/OrganizationSidebarMembers.vue create mode 100644 packages/ui/src/components/project/ProjectsList.vue create mode 100644 packages/ui/src/components/user/UserBadge.vue create mode 100644 packages/ui/src/components/user/UserHeader.vue create mode 100644 packages/ui/src/components/user/UserSidebarBadges.vue create mode 100644 packages/ui/src/components/user/UserSidebarOrganizations.vue create mode 100644 packages/ui/src/utils/link.ts create mode 100644 packages/ui/src/utils/project-types.ts create mode 100644 packages/ui/src/utils/tags.ts create mode 100644 packages/utils/color.ts create mode 100644 packages/utils/permissions.ts diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 412ce1d3a4..ee83d22377 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -2,6 +2,7 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue' import { RouterView, useRoute, useRouter } from 'vue-router' import { + UserIcon, ArrowBigUpDashIcon, CompassIcon, DownloadIcon, @@ -233,6 +234,9 @@ async function fetchCredentials() { credentials.value = creds } +const profileMenu = ref() +const isProfileMenuOpen = computed(() => profileMenu.value?.isOpen) + async function signIn() { await login().catch(handleError) await fetchCredentials() @@ -410,26 +414,34 @@ function handleAuxClick(e) { - - - - - - + + + + + @@ -694,6 +706,9 @@ function handleAuxClick(e) { .app-grid-navbar { grid-area: nav; + + // Fixes SVG scaling issues + filter: brightness(1.00001); } .app-grid-statusbar { @@ -769,6 +784,7 @@ function handleAuxClick(e) { height: 100%; overflow: auto; overflow-x: hidden; + scrollbar-gutter: stable; } .app-contents::before { diff --git a/apps/app-frontend/src/components/RowDisplay.vue b/apps/app-frontend/src/components/RowDisplay.vue index 080301de3c..644348e2fc 100644 --- a/apps/app-frontend/src/components/RowDisplay.vue +++ b/apps/app-frontend/src/components/RowDisplay.vue @@ -181,24 +181,26 @@ const maxInstancesPerRow = ref(1) const maxProjectsPerRow = ref(1) const calculateCardsPerRow = () => { - // Calculate how many cards fit in one row - const containerWidth = rows.value[0].clientWidth - // Convert container width from pixels to rem - const containerWidthInRem = - containerWidth / parseFloat(getComputedStyle(document.documentElement).fontSize) - - maxInstancesPerCompactRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75) - maxInstancesPerRow.value = Math.floor((containerWidthInRem + 0.75) / 20.75) - maxProjectsPerRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75) - - if (maxInstancesPerRow.value < 5) { - maxInstancesPerRow.value *= 2 - } - if (maxInstancesPerCompactRow.value < 5) { - maxInstancesPerCompactRow.value *= 2 - } - if (maxProjectsPerRow.value < 3) { - maxProjectsPerRow.value *= 2 + if (rows.value && rows.value[0]) { + // Calculate how many cards fit in one row + const containerWidth = rows.value[0].clientWidth + // Convert container width from pixels to rem + const containerWidthInRem = + containerWidth / parseFloat(getComputedStyle(document.documentElement).fontSize) + + maxInstancesPerCompactRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75) + maxInstancesPerRow.value = Math.floor((containerWidthInRem + 0.75) / 20.75) + maxProjectsPerRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75) + + if (maxInstancesPerRow.value < 5) { + maxInstancesPerRow.value *= 2 + } + if (maxInstancesPerCompactRow.value < 5) { + maxInstancesPerCompactRow.value *= 2 + } + if (maxProjectsPerRow.value < 3) { + maxProjectsPerRow.value *= 2 + } } } @@ -207,13 +209,17 @@ const resizeObserver = ref(null) onMounted(() => { calculateCardsPerRow() resizeObserver.value = new ResizeObserver(calculateCardsPerRow) - resizeObserver.value.observe(rowContainer.value) + if (rowContainer.value) { + resizeObserver.value.observe(rowContainer.value) + } window.addEventListener('resize', calculateCardsPerRow) }) onUnmounted(() => { window.removeEventListener('resize', calculateCardsPerRow) - resizeObserver.value.unobserve(rowContainer.value) + if (rowContainer.value) { + resizeObserver.value.unobserve(rowContainer.value) + } }) diff --git a/apps/app-frontend/src/components/ui/Instance.vue b/apps/app-frontend/src/components/ui/Instance.vue index 2b954f2a39..aef062c0e9 100644 --- a/apps/app-frontend/src/components/ui/Instance.vue +++ b/apps/app-frontend/src/components/ui/Instance.vue @@ -1,15 +1,8 @@ diff --git a/apps/app-frontend/src/components/ui/InstanceIndicator.vue b/apps/app-frontend/src/components/ui/InstanceIndicator.vue index 61adcfa2ea..450962a33f 100644 --- a/apps/app-frontend/src/components/ui/InstanceIndicator.vue +++ b/apps/app-frontend/src/components/ui/InstanceIndicator.vue @@ -3,23 +3,15 @@ import { convertFileSrc } from '@tauri-apps/api/core' import { formatCategory } from '@modrinth/utils' import { GameIcon, LeftArrowIcon } from '@modrinth/assets' import { Avatar, ButtonStyled } from '@modrinth/ui' - -type Instance = { - game_version: string - loader: string - path: string - install_stage: string - icon_path?: string - name: string -} +import type { GameInstance } from '@/helpers/types' defineProps<{ - instance: Instance + instance?: GameInstance }>() - - diff --git a/apps/app-frontend/src/components/ui/ProjectCard.vue b/apps/app-frontend/src/components/ui/ProjectCard.vue index e78f3fb221..2d64ef7d28 100644 --- a/apps/app-frontend/src/components/ui/ProjectCard.vue +++ b/apps/app-frontend/src/components/ui/ProjectCard.vue @@ -1,5 +1,5 @@ diff --git a/apps/app-frontend/src/components/ui/ProjectCardActions.vue b/apps/app-frontend/src/components/ui/ProjectCardActions.vue new file mode 100644 index 0000000000..b03c6cce8a --- /dev/null +++ b/apps/app-frontend/src/components/ui/ProjectCardActions.vue @@ -0,0 +1,182 @@ + + + diff --git a/apps/app-frontend/src/components/ui/SearchCard.vue b/apps/app-frontend/src/components/ui/SearchCard.vue index b513169525..2f02a41b11 100644 --- a/apps/app-frontend/src/components/ui/SearchCard.vue +++ b/apps/app-frontend/src/components/ui/SearchCard.vue @@ -1,156 +1,70 @@ - diff --git a/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue b/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue index 071b0e9b6b..020e342964 100644 --- a/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue +++ b/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue @@ -116,10 +116,6 @@ function devModeCount() { themeStore.devMode = !themeStore.devMode settings.value.developer_mode = !!themeStore.devMode devModeCounter.value = 0 - - if (!themeStore.devMode && tabs[modal.value.selectedTab].developerOnly) { - modal.value.setTab(0) - } } } diff --git a/apps/app-frontend/src/components/ui/settings/FeatureFlagSettings.vue b/apps/app-frontend/src/components/ui/settings/FeatureFlagSettings.vue index 24d27feb67..9ce64785dc 100644 --- a/apps/app-frontend/src/components/ui/settings/FeatureFlagSettings.vue +++ b/apps/app-frontend/src/components/ui/settings/FeatureFlagSettings.vue @@ -1,19 +1,22 @@ diff --git a/apps/app-frontend/src/composables/instance-context.ts b/apps/app-frontend/src/composables/instance-context.ts new file mode 100644 index 0000000000..b637dceb55 --- /dev/null +++ b/apps/app-frontend/src/composables/instance-context.ts @@ -0,0 +1,42 @@ +import { useRoute } from 'vue-router' +import { ref, computed, type Ref, watch } from 'vue' +import { handleError } from '@/store/notifications' +import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile' +import type { GameInstance, InstanceContent } from '@/helpers/types' + +export type InstanceContentMap = Record + +export async function useInstanceContext() { + const route = useRoute() + + const instance: Ref = ref() + const instanceContent: Ref = ref() + + await loadInstance(); + + watch(route, () => { + loadInstance() + }) + + async function loadInstance() { + [instance.value, instanceContent.value] = + await Promise.all([ + route.query.i ? getInstance(route.query.i).catch(handleError) : Promise.resolve(), + route.query.i ? getInstanceProjects(route.query.i).catch(handleError) : Promise.resolve(), + ]) + } + + const instanceQueryAppendage = computed(() => { + if (instance.value) { + return `?i=${instance.value.path}` + } else { + return '' + } + }) + + return { + instance, + instanceContent, + instanceQueryAppendage, + } +} diff --git a/apps/app-frontend/src/helpers/types.d.ts b/apps/app-frontend/src/helpers/types.d.ts index 1007744d0f..3669a068df 100644 --- a/apps/app-frontend/src/helpers/types.d.ts +++ b/apps/app-frontend/src/helpers/types.d.ts @@ -32,6 +32,18 @@ type GameInstance = { hooks: Hooks } +type InstanceContent = { + hash: string + file_name: string + size: number + metadata?: { + project_id: ModrinthId, + version_id: ModrinthId + } + update_version_id: string + project_type: 'mod' | 'resourcepack' | 'datapack' | 'shaderpack' +} + type InstallStage = | 'installed' | 'minecraft_installing' diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json index 515e4e71ac..e139543d0f 100644 --- a/apps/app-frontend/src/locales/en-US/index.json +++ b/apps/app-frontend/src/locales/en-US/index.json @@ -308,6 +308,18 @@ "instance.settings.title": { "message": "Settings" }, + "project.card.actions.installed.tooltip": { + "message": "This project is already installed" + }, + "project.card.actions.installing.tooltip": { + "message": "This project is being installed" + }, + "project.card.actions.view-gallery": { + "message": "View gallery" + }, + "project.card.actions.view-versions": { + "message": "View versions" + }, "search.filter.locked.instance": { "message": "Provided by the instance" }, diff --git a/apps/app-frontend/src/pages/Browse.vue b/apps/app-frontend/src/pages/Browse.vue index a57b2fddd2..1bdd7fcb8b 100644 --- a/apps/app-frontend/src/pages/Browse.vue +++ b/apps/app-frontend/src/pages/Browse.vue @@ -2,7 +2,14 @@ import { computed, nextTick, ref, shallowRef, watch } from 'vue' import type { Ref } from 'vue' import { SearchIcon, XIcon, ClipboardCopyIcon, GlobeIcon, ExternalIcon } from '@modrinth/assets' -import type { Category, GameVersion, Platform, ProjectType, SortType, Tags } from '@modrinth/ui' +import type { + CategoryTag, + GameVersionTag, + PlatformTag, + ProjectType, + SortType, + Tags, +} from '@modrinth/ui' import { SearchFilterControl, SearchSidebarFilter, @@ -22,11 +29,13 @@ import SearchCard from '@/components/ui/SearchCard.vue' import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile.js' import { get_search_results } from '@/helpers/cache.js' import NavTabs from '@/components/ui/NavTabs.vue' -import type Instance from '@/components/ui/Instance.vue' import InstanceIndicator from '@/components/ui/InstanceIndicator.vue' import { defineMessages, useVIntl } from '@vintl/vintl' import ContextMenu from '@/components/ui/ContextMenu.vue' import { openUrl } from '@tauri-apps/plugin-opener' +import type { GameInstance } from '@/helpers/types' +import { InstanceContentMap, useInstanceContext } from '@/composables/instance-context.ts' +import type { SearchResult } from '@modrinth/utils' const { formatMessage } = useVIntl() @@ -38,62 +47,45 @@ const projectTypes = computed(() => { }) const [categories, loaders, availableGameVersions] = await Promise.all([ - get_categories().catch(handleError).then(ref), - get_loaders().catch(handleError).then(ref), - get_game_versions().catch(handleError).then(ref), + get_categories() + .catch(handleError) + .then((x: CategoryTag[]) => ref(x)), + get_loaders() + .catch(handleError) + .then((x: PlatformTag[]) => ref(x)), + get_game_versions() + .catch(handleError) + .then((x: GameVersionTag[]) => ref(x)), ]) const tags: Ref = computed(() => ({ - gameVersions: availableGameVersions.value as GameVersion[], - loaders: loaders.value as Platform[], - categories: categories.value as Category[], + gameVersions: availableGameVersions.value as GameVersionTag[], + loaders: loaders.value as PlatformTag[], + categories: categories.value as CategoryTag[], })) -type Instance = { - game_version: string - loader: string - path: string - install_stage: string - icon_path?: string - name: string -} - -type InstanceProject = { - metadata: { - project_id: string - } -} - -const instance: Ref = ref(null) -const instanceProjects: Ref = ref(null) const instanceHideInstalled = ref(false) -const newlyInstalled = ref([]) +const newlyInstalled: Ref = ref([]) + +const { instance, instanceContent } = await useInstanceContext() const PERSISTENT_QUERY_PARAMS = ['i', 'ai'] -await updateInstanceContext() +await checkHideInstalledQuery() -watch(route, () => { - updateInstanceContext() +watch(instance, () => { + checkHideInstalledQuery() }) -async function updateInstanceContext() { - if (route.query.i) { - ;[instance.value, instanceProjects.value] = await Promise.all([ - getInstance(route.query.i).catch(handleError), - getInstanceProjects(route.query.i).catch(handleError), - ]) - newlyInstalled.value = [] - } - +async function checkHideInstalledQuery() { if (route.query.ai && !(projectTypes.value.length === 1 && projectTypes.value[0] === 'modpack')) { instanceHideInstalled.value = route.query.ai === 'true' } - if (instance.value && instance.value.path !== route.query.i && route.path.startsWith('/browse')) { - instance.value = null - instanceHideInstalled.value = false - } + // if (instance.value && instance.value.path !== route.query.i && route.path.startsWith('/browse')) { + // instance.value = undefined + // instanceHideInstalled.value = false + // } } const instanceFilters = computed(() => { @@ -119,10 +111,10 @@ const instanceFilters = computed(() => { }) } - if (instanceHideInstalled.value && instanceProjects.value) { - const installedMods = Object.values(instanceProjects.value) + if (instanceHideInstalled.value && instanceContent.value) { + const installedMods: string[] = Object.values(instanceContent.value) .filter((x) => x.metadata) - .map((x) => x.metadata.project_id) + .map((x) => x.metadata!.project_id) installedMods.push(...newlyInstalled.value) @@ -173,23 +165,27 @@ breadcrumbs.setContext({ name: 'Discover content', link: route.path, query: rout const loading = ref(true) -const projectType = ref(route.params.projectType) +const projectType: Ref = ref( + typeof route.params.projectType === 'string' + ? (route.params.projectType as ProjectType) + : undefined, +) watch(projectType, () => { loading.value = true }) -type SearchResult = { - project_id: string +type ExtendedSearchResult = SearchResult & { + installed?: boolean } type SearchResults = { total_hits: number limit: number - hits: SearchResult[] + hits: ExtendedSearchResult[] } -const results: Ref = shallowRef(null) +const results: Ref = shallowRef() const pageCount = computed(() => results.value ? Math.ceil(results.value.total_hits / results.value.limit) : 1, ) @@ -200,7 +196,7 @@ watch(requestParams, () => { }) async function refreshSearch() { - let rawResults = await get_search_results(requestParams.value) + let rawResults = (await get_search_results(requestParams.value)) as { result: SearchResults } if (!rawResults) { rawResults = { result: { @@ -211,13 +207,15 @@ async function refreshSearch() { } } if (instance.value) { - for (const val of rawResults.result.hits) { - val.installed = - newlyInstalled.value.includes(val.project_id) || - Object.values(instanceProjects.value).some( - (x) => x.metadata && x.metadata.project_id === val.project_id, - ) - } + rawResults.result.hits.map((x) => ({ + ...x, + installed: + newlyInstalled.value.includes(x.project_id) || + (instanceContent.value && + Object.values(instanceContent.value).some( + (content) => content.metadata && content.metadata.project_id === x.project_id, + )), + })) } results.value = rawResults.result @@ -271,9 +269,9 @@ watch( () => route.params.projectType, async (newType) => { // Check if the newType is not the same as the current value - if (!newType || newType === projectType.value) return + if (!newType || newType === projectType.value || typeof newType !== 'string') return - projectType.value = newType + projectType.value = newType as ProjectType currentSortType.value = { display: 'Relevance', name: 'relevance' } query.value = '' @@ -287,7 +285,7 @@ const selectableProjectTypes = computed(() => { if (instance.value) { if ( - availableGameVersions.value.findIndex((x) => x.version === instance.value.game_version) <= + availableGameVersions.value.findIndex((x) => x.version === instance.value?.game_version) <= availableGameVersions.value.findIndex((x) => x.version === '1.13') ) { dataPacks = true @@ -353,9 +351,10 @@ const messages = defineMessages({ }, }) -const options = ref(null) -const handleRightClick = (event, result) => { - options.value.showMenu(event, result, [ +const options: Ref | null> = ref(null) + +const handleRightClick = (event: MouseEvent, result: ExtendedSearchResult) => { + options.value?.showMenu(event, result, [ { name: 'open_link', }, @@ -364,7 +363,7 @@ const handleRightClick = (event, result) => { }, ]) } -const handleOptionsClick = (args) => { +const handleOptionsClick = (args: { item: ExtendedSearchResult; option: string }) => { switch (args.option) { case 'open_link': openUrl(`https://modrinth.com/${args.item.project_type}/${args.item.slug}`) @@ -477,33 +476,26 @@ await refreshSearch()
-
+
You are currently offline. Connect to the internet to browse Modrinth!
-
+
diff --git a/apps/app-frontend/src/pages/instance/Index.vue b/apps/app-frontend/src/pages/instance/Index.vue index d2f3d52e08..af9b4374c8 100644 --- a/apps/app-frontend/src/pages/instance/Index.vue +++ b/apps/app-frontend/src/pages/instance/Index.vue @@ -17,11 +17,11 @@
- + {{ instance.loader }} {{ instance.game_version }}
- + diff --git a/apps/app-frontend/src/pages/library/Custom.vue b/apps/app-frontend/src/pages/library/Custom.vue index 619b411130..338de42414 100644 --- a/apps/app-frontend/src/pages/library/Custom.vue +++ b/apps/app-frontend/src/pages/library/Custom.vue @@ -10,7 +10,7 @@ defineProps({