diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index a744566aa3..396d9730de 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) { - - - - - - + + + + + @@ -698,6 +710,9 @@ function handleAuxClick(e) { .app-grid-navbar { grid-area: nav; + + // Fixes SVG scaling issues + filter: brightness(1.00001); } .app-grid-statusbar { @@ -781,6 +796,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..72b72af920 100644 --- a/apps/app-frontend/src/components/ui/Instance.vue +++ b/apps/app-frontend/src/components/ui/Instance.vue @@ -2,14 +2,14 @@ import { computed, onMounted, onUnmounted, ref } from 'vue' import { useRouter } from 'vue-router' import { - DownloadIcon, - GameIcon, - PlayIcon, SpinnerIcon, - StopCircleIcon, + GameIcon, TimerIcon, + StopCircleIcon, + PlayIcon, + DownloadIcon, } from '@modrinth/assets' -import { Avatar, ButtonStyled } from '@modrinth/ui' +import { ButtonStyled, Avatar, SmartClickable } from '@modrinth/ui' import { convertFileSrc } from '@tauri-apps/api/core' import { finish_install, kill, run } from '@/helpers/profile' import { get_by_profile_path } from '@/helpers/process' @@ -134,22 +134,26 @@ onUnmounted(() => unlisten()) diff --git a/apps/app-frontend/src/components/ui/InstanceIndicator.vue b/apps/app-frontend/src/components/ui/InstanceIndicator.vue index 61adcfa2ea..8b131909ee 100644 --- a/apps/app-frontend/src/components/ui/InstanceIndicator.vue +++ b/apps/app-frontend/src/components/ui/InstanceIndicator.vue @@ -3,23 +3,18 @@ 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..35f9846f87 100644 --- a/apps/app-frontend/src/components/ui/ProjectCard.vue +++ b/apps/app-frontend/src/components/ui/ProjectCard.vue @@ -1,16 +1,13 @@ 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..20b7c04eba --- /dev/null +++ b/apps/app-frontend/src/components/ui/ProjectCardActions.vue @@ -0,0 +1,194 @@ + + + diff --git a/apps/app-frontend/src/components/ui/SearchCard.vue b/apps/app-frontend/src/components/ui/SearchCard.vue index b513169525..9bbb87ab9d 100644 --- a/apps/app-frontend/src/components/ui/SearchCard.vue +++ b/apps/app-frontend/src/components/ui/SearchCard.vue @@ -1,157 +1,72 @@ - 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..6778591185 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..a98ea05e22 --- /dev/null +++ b/apps/app-frontend/src/composables/instance-context.ts @@ -0,0 +1,41 @@ +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..54541d8a6a 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..9020ad67d0 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, @@ -19,14 +26,14 @@ import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags' import type { LocationQuery } from 'vue-router' import { useRoute, useRouter } from 'vue-router' 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 { useInstanceContext } from '@/composables/instance-context.ts' +import type { SearchResult } from '@modrinth/utils' const { formatMessage } = useVIntl() @@ -38,62 +45,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 +109,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 +163,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 +194,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 +205,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 +267,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 +283,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 +349,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 +361,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 +474,26 @@ await refreshSearch()
-
+
You are currently offline. Connect to the internet to browse Modrinth!
-
+
diff --git a/apps/app-frontend/src/pages/collection/Index.vue b/apps/app-frontend/src/pages/collection/Index.vue new file mode 100644 index 0000000000..8eaf861ea7 --- /dev/null +++ b/apps/app-frontend/src/pages/collection/Index.vue @@ -0,0 +1,136 @@ + + + + diff --git a/apps/app-frontend/src/pages/collection/index.js b/apps/app-frontend/src/pages/collection/index.js new file mode 100644 index 0000000000..b06d36585f --- /dev/null +++ b/apps/app-frontend/src/pages/collection/index.js @@ -0,0 +1,3 @@ +import Index from './Index.vue' + +export { Index } 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/instance/Mods.vue b/apps/app-frontend/src/pages/instance/Mods.vue index b70f013989..3e38981c32 100644 --- a/apps/app-frontend/src/pages/instance/Mods.vue +++ b/apps/app-frontend/src/pages/instance/Mods.vue @@ -69,7 +69,10 @@ name: x.author.name, type: x.author.type, id: x.author.slug, - link: `https://modrinth.com/${x.author.type}/${x.author.slug}`, + link: { + path: `/${x.author.type}/${x.author.slug}`, + query: { i: props.instance.path }, + }, linkProps: { target: '_blank' }, } } 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({