App redesign (#2946)

* Start of app redesign

* format

* continue progress

* Content page nearly done

* Fix recursion issues with content page

* Fix update all alignment

* Discover page progress

* Settings progress

* Removed unlocked-size hack that breaks web

* Revamp project page, refactor web project page to share code with app, fixed loading bar, misc UI/UX enhancements, update ko-fi logo, update arrow icons, fix web issues caused by floating-vue migration, fix tooltip issues, update web tooltips, clean up web hydration issues

* Ads + run prettier

* Begin auth refactor, move common messages to ui lib, add i18n extraction to all apps, begin Library refactor

* fix ads not hiding when plus log in

* rev lockfile changes/conflicts

* Fix sign in page

* Add generated

* (mostly) Data driven search

* Fix search mobile issue

* profile fixes

* Project versions page, fix typescript on UI lib and misc fixes

* Remove unused gallery component

* Fix linkfunction err

* Search filter controls at top, localization for locked filters

* Fix provided filter names

* Fix navigating from instance browse to main browse

* Friends frontend (#2995)

* Friends system frontend

* (almost) finish frontend

* finish friends, fix lint

* Fix lint

---------

Signed-off-by: Geometrically <18202329+Geometrically@users.noreply.github.com>

* Refresh macOS app icon

* Update web search UI more

* Fix link opens

* Fix frontend build

---------

Signed-off-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
Co-authored-by: Jai A <jaiagr+gpg@pm.me>
Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
This commit is contained in:
Prospector
2024-12-11 19:54:18 -08:00
committed by GitHub
parent 6ec1dcf088
commit c39bb78e38
257 changed files with 15713 additions and 9475 deletions

View File

@@ -1,12 +1,12 @@
<script setup lang="ts">
import { useVIntl, defineMessages } from '@vintl/vintl'
import { computed } from 'vue'
import type { VersionChannel } from '@modrinth/utils'
const { formatMessage } = useVIntl()
const props = withDefaults(
withDefaults(
defineProps<{
channel: 'release' | 'beta' | 'alpha'
channel: VersionChannel
large?: boolean
}>(),
{

View File

@@ -0,0 +1,212 @@
<template>
<div class="experimental-styles-within flex flex-col gap-3">
<div class="flex flex-wrap items-center gap-2">
<ManySelect
v-model="selectedPlatforms"
:options="filterOptions.platform"
:dropdown-id="`${baseId}-platform`"
@change="updateFilters"
>
<FilterIcon class="h-5 w-5 text-secondary" />
Platform
<template #option="{ option }">
{{ formatCategory(option) }}
</template>
</ManySelect>
<ManySelect
v-model="selectedGameVersions"
:options="filterOptions.gameVersion"
:dropdown-id="`${baseId}-game-version`"
search
@change="updateFilters"
>
<FilterIcon class="h-5 w-5 text-secondary" />
Game versions
<template #footer>
<Checkbox v-model="showSnapshots" class="mx-1" :label="`Show all versions`" />
</template>
</ManySelect>
<ManySelect
v-model="selectedChannels"
:options="filterOptions.channel"
:dropdown-id="`${baseId}-channel`"
@change="updateFilters"
>
<FilterIcon class="h-5 w-5 text-secondary" />
Channels
<template #option="{ option }">
{{ option === 'release' ? 'Release' : option === 'beta' ? 'Beta' : 'Alpha' }}
</template>
</ManySelect>
</div>
<div class="flex flex-wrap items-center gap-1 empty:hidden">
<TagItem
v-if="selectedChannels.length + selectedGameVersions.length + selectedPlatforms.length > 1"
class="transition-transform active:scale-[0.95]"
:action="clearFilters"
>
<XCircleIcon />
Clear all filters
</TagItem>
<TagItem
v-for="channel in selectedChannels"
:key="`remove-filter-${channel}`"
:style="`--_color: var(--color-${channel === 'alpha' ? 'red' : channel === 'beta' ? 'orange' : 'green'});--_bg-color: var(--color-${channel === 'alpha' ? 'red' : channel === 'beta' ? 'orange' : 'green'}-highlight)`"
:action="() =>toggleFilter('channel', channel)"
>
<XIcon />
{{ channel.slice(0, 1).toUpperCase() + channel.slice(1) }}
</TagItem>
<TagItem
v-for="version in selectedGameVersions"
:key="`remove-filter-${version}`"
:action="() =>toggleFilter('gameVersion', version)"
>
<XIcon />
{{ version }}
</TagItem>
<TagItem
v-for="platform in selectedPlatforms"
:key="`remove-filter-${platform}`"
:style="`--_color: var(--color-platform-${platform})`"
:action="() => toggleFilter('platform', platform)"
>
<XIcon />
{{ formatCategory(platform) }}
</TagItem>
</div>
</div>
</template>
<script setup lang="ts">
import { FilterIcon, XCircleIcon, XIcon } from "@modrinth/assets";
import { ManySelect, Checkbox } from "../index";
import { type Version , formatCategory, type GameVersionTag } from '@modrinth/utils';
import { ref, computed } from "vue";
import { useRoute } from "vue-router";
import TagItem from '../base/TagItem.vue'
const props = defineProps<{
versions: Version[]
gameVersions: GameVersionTag[]
baseId?: string
}>();
const emit = defineEmits(["update:query"]);
const allChannels = ref(["release", "beta", "alpha"]);
const route = useRoute();
const showSnapshots = ref(false);
type FilterType = "channel" | "gameVersion" | "platform";
type Filter = string;
const filterOptions = computed(() => {
const filters: Record<FilterType, Filter[]> = {
channel: [],
gameVersion: [],
platform: [],
};
const platformSet = new Set();
const gameVersionSet = new Set();
const channelSet = new Set();
for (const version of props.versions) {
for (const loader of version.loaders) {
platformSet.add(loader);
}
for (const gameVersion of version.game_versions) {
gameVersionSet.add(gameVersion);
}
channelSet.add(version.version_type);
}
if (channelSet.size > 0) {
filters.channel = Array.from(channelSet) as Filter[];
filters.channel.sort((a, b) => allChannels.value.indexOf(a) - allChannels.value.indexOf(b));
}
if (gameVersionSet.size > 0) {
const gameVersions = props.gameVersions.filter((x) => gameVersionSet.has(x.version));
filters.gameVersion = gameVersions
.filter((x) => (showSnapshots.value ? true : x.version_type === "release"))
.map((x) => x.version);
}
if (platformSet.size > 0) {
filters.platform = Array.from(platformSet) as Filter[];
}
return filters;
});
const selectedChannels = ref<string[]>([]);
const selectedGameVersions = ref<string[]>([]);
const selectedPlatforms = ref<string[]>([]);
selectedChannels.value = route.query.c ? getArrayOrString(route.query.c) : [];
selectedGameVersions.value = route.query.g ? getArrayOrString(route.query.g) : [];
selectedPlatforms.value = route.query.l ? getArrayOrString(route.query.l) : [];
async function toggleFilters(type: FilterType, filters: Filter[]) {
for (const filter of filters) {
await toggleFilter(type, filter, true);
}
updateFilters();
}
async function toggleFilter(type: FilterType, filter: Filter, bulk = false) {
if (type === "channel") {
selectedChannels.value = selectedChannels.value.includes(filter)
? selectedChannels.value.filter((x) => x !== filter)
: [...selectedChannels.value, filter];
} else if (type === "gameVersion") {
selectedGameVersions.value = selectedGameVersions.value.includes(filter)
? selectedGameVersions.value.filter((x) => x !== filter)
: [...selectedGameVersions.value, filter];
} else if (type === "platform") {
selectedPlatforms.value = selectedPlatforms.value.includes(filter)
? selectedPlatforms.value.filter((x) => x !== filter)
: [...selectedPlatforms.value, filter];
}
if (!bulk) {
updateFilters();
}
}
async function clearFilters() {
selectedChannels.value = [];
selectedGameVersions.value = [];
selectedPlatforms.value = [];
updateFilters();
}
function updateFilters() {
emit("update:query", {
c: selectedChannels.value,
g: selectedGameVersions.value,
l: selectedPlatforms.value,
page: undefined,
});
}
defineExpose({
toggleFilter,
toggleFilters,
selectedChannels,
selectedGameVersions,
selectedPlatforms,
});
function getArrayOrString(x: string | string[]): string[] {
if (typeof x === "string") {
return [x];
} else {
return x;
}
}
</script>

View File

@@ -0,0 +1,47 @@
<template>
<div
class="grid grid-cols-[min-content_auto_min-content_min-content] items-center gap-2 rounded-2xl border-[1px] border-divider bg-bg p-2"
>
<VersionChannelIndicator :channel="version.version_type" />
<div class="flex min-w-0 flex-col gap-1">
<h1 class="my-0 truncate text-nowrap text-base font-extrabold leading-none text-contrast">
{{ version.version_number }}
</h1>
<p class="m-0 truncate text-nowrap text-xs font-semibold text-secondary">
{{ version.name }}
</p>
</div>
<ButtonStyled color="brand">
<a :href="downloadUrl" class="min-w-0" @click="emit('onDownload')">
<DownloadIcon aria-hidden="true" /> Download
</a>
</ButtonStyled>
<ButtonStyled circular>
<nuxt-link
:to="`/project/${props.version.project_id}/version/${props.version.id}`"
class="min-w-0"
aria-label="Open project page"
@click="emit('onNavigate')"
>
<ExternalIcon aria-hidden="true" />
</nuxt-link>
</ButtonStyled>
</div>
</template>
<script setup lang="ts">
import { ButtonStyled, VersionChannelIndicator } from "../index";
import { DownloadIcon, ExternalIcon } from "@modrinth/assets";
import { computed } from "vue";
const props = defineProps<{
version: Version;
}>();
const downloadUrl = computed(() => {
const primary: VersionFile = props.version.files.find((x) => x.primary) || props.version.files[0];
return primary.url;
});
const emit = defineEmits(["onDownload", "onNavigate"]);
</script>