diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 412ce1d3a..ee83d2237 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 080301de3..644348e2f 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) + if (rows.value && rows.value[0]) { + // Calculate how many cards fit in one row + const containerWidth = rows.value[0].clientWidth + // Convert container width from pixels to rem + const containerWidthInRem = + containerWidth / parseFloat(getComputedStyle(document.documentElement).fontSize) - maxInstancesPerCompactRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75) - maxInstancesPerRow.value = Math.floor((containerWidthInRem + 0.75) / 20.75) - maxProjectsPerRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75) + maxInstancesPerCompactRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75) + maxInstancesPerRow.value = Math.floor((containerWidthInRem + 0.75) / 20.75) + maxProjectsPerRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75) - if (maxInstancesPerRow.value < 5) { - maxInstancesPerRow.value *= 2 - } - if (maxInstancesPerCompactRow.value < 5) { - maxInstancesPerCompactRow.value *= 2 - } - if (maxProjectsPerRow.value < 3) { - maxProjectsPerRow.value *= 2 + if (maxInstancesPerRow.value < 5) { + maxInstancesPerRow.value *= 2 + } + if (maxInstancesPerCompactRow.value < 5) { + maxInstancesPerCompactRow.value *= 2 + } + if (maxProjectsPerRow.value < 3) { + maxProjectsPerRow.value *= 2 + } } } @@ -207,13 +209,17 @@ const resizeObserver = ref(null) onMounted(() => { calculateCardsPerRow() resizeObserver.value = new ResizeObserver(calculateCardsPerRow) - resizeObserver.value.observe(rowContainer.value) + if (rowContainer.value) { + resizeObserver.value.observe(rowContainer.value) + } window.addEventListener('resize', calculateCardsPerRow) }) onUnmounted(() => { window.removeEventListener('resize', calculateCardsPerRow) - resizeObserver.value.unobserve(rowContainer.value) + if (rowContainer.value) { + resizeObserver.value.unobserve(rowContainer.value) + } }) diff --git a/apps/app-frontend/src/components/ui/Instance.vue b/apps/app-frontend/src/components/ui/Instance.vue index 2b954f2a3..aef062c0e 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 61adcfa2e..450962a33 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 e78f3fb22..2d64ef7d2 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 000000000..b03c6cce8 --- /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 b51316952..2f02a41b1 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 071b0e9b6..020e34296 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 24d27feb6..9ce64785d 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 000000000..b637dceb5 --- /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 1007744d0..3669a068d 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 515e4e71a..e139543d0 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 a57b2fddd..1bdd7fcb8 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 d2f3d52e0..af9b4374c 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 619b41113..338de4241 100644 --- a/apps/app-frontend/src/pages/library/Custom.vue +++ b/apps/app-frontend/src/pages/library/Custom.vue @@ -10,7 +10,7 @@ defineProps({