You've already forked AstralRinth
forked from didirus/AstralRinth
Sidebar refinements (#2306)
* Begin sidebar refinement, change back to left as default * New filters proof of concept * Hide if only one option * Version filters * Update changelog page * Use new cosmetic variable for sidebar position * Fix safari issue and change defaults to left filters, right sidebars * Fix download modal on safari and firefox * Add date published tooltip to versions page * Improve selection consistency * Fix lint and extract i18n * Remove unnecessary observer options
This commit is contained in:
@@ -1147,7 +1147,6 @@ svg.inline-svg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// START STUFF FOR OMORPHIA
|
// START STUFF FOR OMORPHIA
|
||||||
|
|
||||||
.experimental-styles-within {
|
.experimental-styles-within {
|
||||||
.tag-list {
|
.tag-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1156,7 +1155,7 @@ svg.inline-svg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tag-list__item {
|
.tag-list__item {
|
||||||
background-color: var(--color-button-bg);
|
background-color: var(--_bg-color, var(--color-button-bg));
|
||||||
padding: var(--gap-4) var(--gap-8);
|
padding: var(--gap-4) var(--gap-8);
|
||||||
border-radius: var(--radius-max);
|
border-radius: var(--radius-max);
|
||||||
font-weight: var(--weight-bold);
|
font-weight: var(--weight-bold);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="accordion-wrapper">
|
<div class="accordion-wrapper" :class="{ 'has-content': hasContent }">
|
||||||
<div class="accordion-content">
|
<div class="accordion-content">
|
||||||
<div>
|
<div>
|
||||||
<div class="content-container" v-bind="$attrs">
|
<div v-bind="$attrs" ref="slotContainer" class="content-container">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -14,11 +14,39 @@
|
|||||||
defineOptions({
|
defineOptions({
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const slotContainer = ref();
|
||||||
|
|
||||||
|
const hasContent = ref(false);
|
||||||
|
|
||||||
|
const mutationObserver = ref<MutationObserver | null>(null);
|
||||||
|
|
||||||
|
function updateContent() {
|
||||||
|
if (!slotContainer.value) return false;
|
||||||
|
|
||||||
|
hasContent.value = slotContainer.value ? slotContainer.value.children.length > 0 : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
mutationObserver.value = new MutationObserver(updateContent);
|
||||||
|
|
||||||
|
mutationObserver.value.observe(slotContainer.value, {
|
||||||
|
childList: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
updateContent();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (mutationObserver.value) {
|
||||||
|
mutationObserver.value.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.accordion-content {
|
.accordion-content {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 0fr;
|
||||||
transition: grid-template-rows 0.3s ease-in-out;
|
transition: grid-template-rows 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,15 +56,15 @@ defineOptions({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.accordion-content:has(* .content-container:empty) {
|
.has-content .accordion-content {
|
||||||
grid-template-rows: 0fr;
|
grid-template-rows: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.accordion-content > div {
|
.accordion-content > div {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.accordion-wrapper:has(* .content-container:empty) {
|
.accordion-wrapper.has-content {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,164 +1,153 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card flex-card experimental-styles-within">
|
<div class="experimental-styles-within flex flex-col gap-3">
|
||||||
<span class="text-lg font-bold text-contrast">Filter</span>
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<div class="flex items-center gap-2">
|
<ManySelect
|
||||||
<div class="iconified-input w-full">
|
v-model="selectedPlatforms"
|
||||||
<label class="hidden" for="search">Search</label>
|
:options="filterOptions.platform"
|
||||||
<SearchIcon aria-hidden="true" />
|
@change="updateFilters"
|
||||||
<input
|
>
|
||||||
id="search"
|
<FilterIcon class="h-5 w-5 text-secondary" />
|
||||||
v-model="queryFilter"
|
Platform
|
||||||
name="search"
|
<template #option="{ option }">
|
||||||
type="search"
|
{{ formatCategory(option) }}
|
||||||
placeholder="Search filters..."
|
</template>
|
||||||
autocomplete="off"
|
</ManySelect>
|
||||||
/>
|
<ManySelect
|
||||||
</div>
|
v-model="selectedGameVersions"
|
||||||
|
:options="filterOptions.gameVersion"
|
||||||
|
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"
|
||||||
|
@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">
|
||||||
<button
|
<button
|
||||||
v-if="Object.keys(selectedFilters).length !== 0"
|
v-if="selectedChannels.length + selectedGameVersions.length + selectedPlatforms.length > 1"
|
||||||
class="btn icon-only"
|
class="tag-list__item text-contrast transition-transform active:scale-[0.95]"
|
||||||
@click="clearFilters"
|
@click="clearFilters"
|
||||||
>
|
>
|
||||||
<FilterXIcon />
|
<XCircleIcon />
|
||||||
|
Clear all filters
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-for="(value, key, index) in filters"
|
|
||||||
:key="key"
|
|
||||||
:class="`border-0 border-b border-solid border-button-bg py-2 last:border-b-0`"
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
class="flex !w-full bg-transparent px-0 py-2 font-extrabold text-contrast transition-all active:scale-[0.98]"
|
v-for="channel in selectedChannels"
|
||||||
@click="
|
:key="`remove-filter-${channel}`"
|
||||||
() => {
|
class="tag-list__item transition-transform active:scale-[0.95]"
|
||||||
filterAccordions[index].isOpen
|
:style="`--_color: var(--color-${channel === 'alpha' ? 'red' : channel === 'beta' ? 'orange' : 'green'});--_bg-color: var(--color-${channel === 'alpha' ? 'red' : channel === 'beta' ? 'orange' : 'green'}-highlight)`"
|
||||||
? filterAccordions[index].close()
|
@click="toggleFilter('channel', channel)"
|
||||||
: filterAccordions[index].open();
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<template v-if="key === 'gameVersion'"> Game versions </template>
|
<XIcon />
|
||||||
<template v-else>
|
{{ channel.slice(0, 1).toUpperCase() + channel.slice(1) }}
|
||||||
{{ $capitalizeString(key) }}
|
</button>
|
||||||
</template>
|
<button
|
||||||
<DropdownIcon
|
v-for="version in selectedGameVersions"
|
||||||
class="ml-auto h-5 w-5 transition-transform"
|
:key="`remove-filter-${version}`"
|
||||||
:class="{ 'rotate-180': filterAccordions[index]?.isOpen }"
|
class="tag-list__item transition-transform active:scale-[0.95]"
|
||||||
/>
|
@click="toggleFilter('gameVersion', version)"
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
{{ version }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-for="platform in selectedPlatforms"
|
||||||
|
:key="`remove-filter-${platform}`"
|
||||||
|
class="tag-list__item transition-transform active:scale-[0.95]"
|
||||||
|
:style="`--_color: var(--color-platform-${platform})`"
|
||||||
|
@click="toggleFilter('platform', platform)"
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
{{ formatCategory(platform) }}
|
||||||
</button>
|
</button>
|
||||||
<Accordion ref="filterAccordions" :open-by-default="true">
|
|
||||||
<ScrollablePanel
|
|
||||||
:class="{ 'h-[18rem]': value.length >= 8 && key === 'gameVersion' }"
|
|
||||||
:no-max-height="key !== 'gameVersion'"
|
|
||||||
>
|
|
||||||
<div class="mr-1 flex flex-col gap-1">
|
|
||||||
<div v-for="filter in value" :key="filter" class="group flex gap-1">
|
|
||||||
<button
|
|
||||||
:class="`flex !w-full items-center gap-2 truncate rounded-xl px-2 py-1 text-sm font-semibold transition-all active:scale-[0.98] ${selectedFilters[key]?.includes(filter) ? 'bg-brand-highlight text-contrast hover:brightness-125' : 'bg-transparent text-secondary hover:bg-button-bg'}`"
|
|
||||||
@click="toggleFilter(key, filter)"
|
|
||||||
>
|
|
||||||
<span v-if="filter === 'release'" class="h-2 w-2 rounded-full bg-brand" />
|
|
||||||
<span v-else-if="filter === 'beta'" class="h-2 w-2 rounded-full bg-orange" />
|
|
||||||
<span v-else-if="filter === 'alpha'" class="h-2 w-2 rounded-full bg-red" />
|
|
||||||
<span class="truncate text-sm">{{ $formatCategory(filter) }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ScrollablePanel>
|
|
||||||
<Checkbox
|
|
||||||
v-if="key === 'gameVersion'"
|
|
||||||
v-model="showSnapshots"
|
|
||||||
class="mx-2"
|
|
||||||
:label="`Show all versions`"
|
|
||||||
/>
|
|
||||||
</Accordion>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { DropdownIcon, FilterXIcon, SearchIcon } from "@modrinth/assets";
|
import { FilterIcon, XCircleIcon, XIcon } from "@modrinth/assets";
|
||||||
import { ScrollablePanel, Checkbox } from "@modrinth/ui";
|
import { ManySelect, Checkbox } from "@modrinth/ui";
|
||||||
import Accordion from "~/components/ui/Accordion.vue";
|
import { formatCategory } from "@modrinth/utils";
|
||||||
|
import type { ModrinthVersion } from "@modrinth/utils";
|
||||||
|
|
||||||
|
const props = defineProps<{ versions: ModrinthVersion[] }>();
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
versions: {
|
|
||||||
type: Array,
|
|
||||||
default() {
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const emit = defineEmits(["switch-page"]);
|
const emit = defineEmits(["switch-page"]);
|
||||||
|
|
||||||
|
const allChannels = ref(["release", "beta", "alpha"]);
|
||||||
|
|
||||||
const route = useNativeRoute();
|
const route = useNativeRoute();
|
||||||
const router = useNativeRouter();
|
const router = useNativeRouter();
|
||||||
|
|
||||||
const tags = useTags();
|
const tags = useTags();
|
||||||
|
|
||||||
const filterAccordions = ref([]);
|
|
||||||
|
|
||||||
const queryFilter = ref("");
|
|
||||||
const showSnapshots = ref(false);
|
const showSnapshots = ref(false);
|
||||||
const filters = computed(() => {
|
|
||||||
const filters = {};
|
|
||||||
|
|
||||||
const tempLoaders = new Set();
|
type FilterType = "channel" | "gameVersion" | "platform";
|
||||||
const tempVersions = new Set();
|
type Filter = string;
|
||||||
const tempReleaseChannels = new Set();
|
|
||||||
|
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 version of props.versions) {
|
||||||
for (const loader of version.loaders) {
|
for (const loader of version.loaders) {
|
||||||
tempLoaders.add(loader);
|
platformSet.add(loader);
|
||||||
}
|
}
|
||||||
for (const gameVersion of version.game_versions) {
|
for (const gameVersion of version.game_versions) {
|
||||||
tempVersions.add(gameVersion);
|
gameVersionSet.add(gameVersion);
|
||||||
}
|
}
|
||||||
tempReleaseChannels.add(version.version_type);
|
channelSet.add(version.version_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tempReleaseChannels.size > 0) {
|
if (channelSet.size > 0) {
|
||||||
filters.type = Array.from(tempReleaseChannels);
|
filters.channel = Array.from(channelSet) as Filter[];
|
||||||
|
filters.channel.sort((a, b) => allChannels.value.indexOf(a) - allChannels.value.indexOf(b));
|
||||||
}
|
}
|
||||||
if (tempVersions.size > 0) {
|
if (gameVersionSet.size > 0) {
|
||||||
const gameVersions = tags.value.gameVersions.filter((x) => tempVersions.has(x.version));
|
const gameVersions = tags.value.gameVersions.filter((x) => gameVersionSet.has(x.version));
|
||||||
|
|
||||||
filters.gameVersion = gameVersions
|
filters.gameVersion = gameVersions
|
||||||
.filter((x) => (showSnapshots.value ? true : x.version_type === "release"))
|
.filter((x) => (showSnapshots.value ? true : x.version_type === "release"))
|
||||||
.map((x) => x.version);
|
.map((x) => x.version);
|
||||||
}
|
}
|
||||||
if (tempLoaders.size > 0) {
|
if (platformSet.size > 0) {
|
||||||
filters.platform = Array.from(tempLoaders);
|
filters.platform = Array.from(platformSet) as Filter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredObj = {};
|
return filters;
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(filters)) {
|
|
||||||
const filters = queryFilter.value
|
|
||||||
? value.filter((x) => x.toLowerCase().includes(queryFilter.value.toLowerCase()))
|
|
||||||
: value;
|
|
||||||
|
|
||||||
if (filters.length > 0) {
|
|
||||||
filteredObj[key] = filters;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filteredObj;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedFilters = ref({});
|
const selectedChannels = ref<string[]>([]);
|
||||||
|
const selectedGameVersions = ref<string[]>([]);
|
||||||
|
const selectedPlatforms = ref<string[]>([]);
|
||||||
|
|
||||||
if (route.query.type) {
|
selectedChannels.value = route.query.c ? getArrayOrString(route.query.c) : [];
|
||||||
selectedFilters.value.type = getArrayOrString(route.query.type);
|
selectedGameVersions.value = route.query.g ? getArrayOrString(route.query.g) : [];
|
||||||
}
|
selectedPlatforms.value = route.query.l ? getArrayOrString(route.query.l) : [];
|
||||||
if (route.query.gameVersion) {
|
|
||||||
selectedFilters.value.gameVersion = getArrayOrString(route.query.gameVersion);
|
|
||||||
}
|
|
||||||
if (route.query.platform) {
|
|
||||||
selectedFilters.value.platform = getArrayOrString(route.query.platform);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function toggleFilters(type, filters) {
|
async function toggleFilters(type: FilterType, filters: Filter[]) {
|
||||||
for (const filter of filters) {
|
for (const filter of filters) {
|
||||||
await toggleFilter(type, filter);
|
await toggleFilter(type, filter);
|
||||||
}
|
}
|
||||||
@@ -166,54 +155,58 @@ async function toggleFilters(type, filters) {
|
|||||||
await router.replace({
|
await router.replace({
|
||||||
query: {
|
query: {
|
||||||
...route.query,
|
...route.query,
|
||||||
type: selectedFilters.value.type,
|
c: selectedChannels.value,
|
||||||
gameVersion: selectedFilters.value.gameVersion,
|
g: selectedGameVersions.value,
|
||||||
platform: selectedFilters.value.platform,
|
l: selectedPlatforms.value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
emit("switch-page", 1);
|
emit("switch-page", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleFilter(type, filter, skipRouter) {
|
async function toggleFilter(type: FilterType, filter: Filter, skipRouter = false) {
|
||||||
if (!selectedFilters.value[type]) {
|
if (type === "channel") {
|
||||||
selectedFilters.value[type] = [];
|
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];
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = selectedFilters.value[type].indexOf(filter);
|
|
||||||
if (index !== -1) {
|
|
||||||
selectedFilters.value[type].splice(index, 1);
|
|
||||||
} else {
|
|
||||||
selectedFilters.value[type].push(filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedFilters.value[type].length === 0) {
|
|
||||||
delete selectedFilters.value[type];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!skipRouter) {
|
if (!skipRouter) {
|
||||||
await router.replace({
|
await updateFilters();
|
||||||
query: {
|
|
||||||
...route.query,
|
|
||||||
type: selectedFilters.value.type,
|
|
||||||
gameVersion: selectedFilters.value.gameVersion,
|
|
||||||
platform: selectedFilters.value.platform,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
emit("switch-page", 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateFilters() {
|
||||||
|
await router.replace({
|
||||||
|
query: {
|
||||||
|
...route.query,
|
||||||
|
c: selectedChannels.value,
|
||||||
|
g: selectedGameVersions.value,
|
||||||
|
l: selectedPlatforms.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
emit("switch-page", 1);
|
||||||
|
}
|
||||||
|
|
||||||
async function clearFilters() {
|
async function clearFilters() {
|
||||||
selectedFilters.value = {};
|
selectedChannels.value = [];
|
||||||
|
selectedGameVersions.value = [];
|
||||||
|
selectedPlatforms.value = [];
|
||||||
|
|
||||||
await router.replace({
|
await router.replace({
|
||||||
query: {
|
query: {
|
||||||
...route.query,
|
...route.query,
|
||||||
type: undefined,
|
c: undefined,
|
||||||
gameVersion: undefined,
|
g: undefined,
|
||||||
platform: undefined,
|
l: undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,17 +19,8 @@ const validateValues = <K extends PropertyKey>(flags: Record<K, FlagValue>) => f
|
|||||||
export const DEFAULT_FEATURE_FLAGS = validateValues({
|
export const DEFAULT_FEATURE_FLAGS = validateValues({
|
||||||
// Developer flags
|
// Developer flags
|
||||||
developerMode: false,
|
developerMode: false,
|
||||||
|
showVersionFilesInTable: false,
|
||||||
// In-development features, flags will be removed over time
|
showAdsWithPlus: false,
|
||||||
newProjectLinks: true,
|
|
||||||
newProjectMembers: false,
|
|
||||||
newProjectDetails: true,
|
|
||||||
projectCompatibility: false,
|
|
||||||
removeFeaturedVersions: false,
|
|
||||||
|
|
||||||
// Alt layouts
|
|
||||||
// searchSidebarRight: false,
|
|
||||||
// projectSidebarRight: false,
|
|
||||||
|
|
||||||
// Feature toggles
|
// Feature toggles
|
||||||
// advancedRendering: true,
|
// advancedRendering: true,
|
||||||
|
|||||||
@@ -611,8 +611,8 @@
|
|||||||
"project.about.links.wiki": {
|
"project.about.links.wiki": {
|
||||||
"message": "Visit wiki"
|
"message": "Visit wiki"
|
||||||
},
|
},
|
||||||
"project.about.title": {
|
"project.description.title": {
|
||||||
"message": "About"
|
"message": "Description"
|
||||||
},
|
},
|
||||||
"project.gallery.title": {
|
"project.gallery.title": {
|
||||||
"message": "Gallery"
|
"message": "Gallery"
|
||||||
@@ -1028,9 +1028,6 @@
|
|||||||
"settings.display.project-list.layouts.collection": {
|
"settings.display.project-list.layouts.collection": {
|
||||||
"message": "Collection"
|
"message": "Collection"
|
||||||
},
|
},
|
||||||
"settings.display.sidebar.Left-aligned-search-sidebar.title": {
|
|
||||||
"message": "Left-aligned search sidebar"
|
|
||||||
},
|
|
||||||
"settings.display.sidebar.advanced-rendering.description": {
|
"settings.display.sidebar.advanced-rendering.description": {
|
||||||
"message": "Enables advanced rendering such as blur effects that may cause performance issues without hardware-accelerated rendering."
|
"message": "Enables advanced rendering such as blur effects that may cause performance issues without hardware-accelerated rendering."
|
||||||
},
|
},
|
||||||
@@ -1049,14 +1046,17 @@
|
|||||||
"settings.display.sidebar.hide-app-promos.title": {
|
"settings.display.sidebar.hide-app-promos.title": {
|
||||||
"message": "Hide Modrinth App promotions"
|
"message": "Hide Modrinth App promotions"
|
||||||
},
|
},
|
||||||
"settings.display.sidebar.left-aligned-project-sidebar.description": {
|
"settings.display.sidebar.left-aligned-content-sidebar.title": {
|
||||||
"message": "Aligns the project details sidebar to the left of the page's content."
|
"message": "Left-aligned sidebar on content pages"
|
||||||
},
|
},
|
||||||
"settings.display.sidebar.left-aligned-project-sidebar.title": {
|
"settings.display.sidebar.right-aligned-content-sidebar.description": {
|
||||||
"message": "Left-aligned project sidebar"
|
"message": "Aligns the sidebar to the left of the page's content."
|
||||||
},
|
},
|
||||||
"settings.display.sidebar.left-aligned-search-sidebar.description": {
|
"settings.display.sidebar.right-aligned-filters-sidebar.description": {
|
||||||
"message": "Aligns the search filters sidebar to the left of the search results."
|
"message": "Aligns the filters sidebar to the right of the search results."
|
||||||
|
},
|
||||||
|
"settings.display.sidebar.right-aligned-filters-sidebar.title": {
|
||||||
|
"message": "Right-aligned filters sidebar on search pages"
|
||||||
},
|
},
|
||||||
"settings.display.theme.dark": {
|
"settings.display.theme.dark": {
|
||||||
"message": "Dark"
|
"message": "Dark"
|
||||||
|
|||||||
@@ -149,6 +149,20 @@
|
|||||||
<span class="text-lg font-extrabold text-contrast"> Settings </span>
|
<span class="text-lg font-extrabold text-contrast"> Settings </span>
|
||||||
</template>
|
</template>
|
||||||
</NewModal>
|
</NewModal>
|
||||||
|
<NewModal ref="modalLicense" :header="project.license.name ? project.license.name : 'License'">
|
||||||
|
<template #title>
|
||||||
|
<Avatar :src="project.icon_url" :alt="project.title" class="icon" size="32px" no-shadow />
|
||||||
|
<span class="text-lg font-extrabold text-contrast">
|
||||||
|
{{ project.license.name ? project.license.name : "License" }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
class="markdown-body"
|
||||||
|
v-html="
|
||||||
|
renderString(licenseText).isEmpty ? 'Loading license text...' : renderString(licenseText)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</NewModal>
|
||||||
<div
|
<div
|
||||||
class="over-the-top-download-animation"
|
class="over-the-top-download-animation"
|
||||||
:class="{ 'animation-hidden': !overTheTopDownloadAnimation }"
|
:class="{ 'animation-hidden': !overTheTopDownloadAnimation }"
|
||||||
@@ -232,7 +246,6 @@
|
|||||||
class="accordion-with-bg"
|
class="accordion-with-bg"
|
||||||
@on-open="
|
@on-open="
|
||||||
() => {
|
() => {
|
||||||
gameVersionFilterInput.focus();
|
|
||||||
if (platformAccordion) {
|
if (platformAccordion) {
|
||||||
platformAccordion.close();
|
platformAccordion.close();
|
||||||
}
|
}
|
||||||
@@ -402,7 +415,8 @@
|
|||||||
!filteredAlpha
|
!filteredAlpha
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
No versions available for {{ currentGameVersion }} and {{ currentPlatform }}.
|
No versions available for {{ currentGameVersion }} and
|
||||||
|
{{ formatCategory(currentPlatform) }}.
|
||||||
</p>
|
</p>
|
||||||
</AutomaticAccordion>
|
</AutomaticAccordion>
|
||||||
</div>
|
</div>
|
||||||
@@ -410,10 +424,9 @@
|
|||||||
</NewModal>
|
</NewModal>
|
||||||
<CollectionCreateModal ref="modal_collection" :project-ids="[project.id]" />
|
<CollectionCreateModal ref="modal_collection" :project-ids="[project.id]" />
|
||||||
<div
|
<div
|
||||||
class="new-page"
|
class="new-page sidebar"
|
||||||
:class="{
|
:class="{
|
||||||
sidebar: !route.name.endsWith('gallery') && !route.name.endsWith('moderation'),
|
'alt-layout': cosmetics.leftContentLayout,
|
||||||
'alt-layout': cosmetics.projectLayout,
|
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="normal-page__header relative my-4">
|
<div class="normal-page__header relative my-4">
|
||||||
@@ -438,7 +451,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<div class="mt-auto flex flex-wrap gap-4">
|
<div class="mt-auto flex flex-wrap gap-4">
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-3 border-0 border-r border-solid border-button-bg pr-4"
|
class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4"
|
||||||
>
|
>
|
||||||
<DownloadIcon class="h-6 w-6 text-secondary" />
|
<DownloadIcon class="h-6 w-6 text-secondary" />
|
||||||
<span class="font-semibold">
|
<span class="font-semibold">
|
||||||
@@ -446,14 +459,14 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-3 border-0 border-solid border-button-bg pr-4 md:border-r"
|
class="flex items-center gap-2 border-0 border-solid border-button-bg pr-4 md:border-r"
|
||||||
>
|
>
|
||||||
<HeartIcon class="h-6 w-6 text-secondary" />
|
<HeartIcon class="h-6 w-6 text-secondary" />
|
||||||
<span class="font-semibold">
|
<span class="font-semibold">
|
||||||
{{ $formatNumber(project.followers) }}
|
{{ $formatNumber(project.followers) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden items-center gap-3 md:flex">
|
<div class="hidden items-center gap-2 md:flex">
|
||||||
<TagsIcon class="h-6 w-6 text-secondary" />
|
<TagsIcon class="h-6 w-6 text-secondary" />
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<div
|
<div
|
||||||
@@ -650,25 +663,327 @@
|
|||||||
{{ project.title }} has been archived. {{ project.title }} will not receive any further
|
{{ project.title }} has been archived. {{ project.title }} will not receive any further
|
||||||
updates unless the author decides to unarchive the project.
|
updates unless the author decides to unarchive the project.
|
||||||
</MessageBanner>
|
</MessageBanner>
|
||||||
<div class="overflow-x-auto">
|
</div>
|
||||||
<NavTabs :links="navLinks" class="mt-4" />
|
<div class="normal-page__sidebar">
|
||||||
|
<div v-if="versions.length > 0" class="card flex-card experimental-styles-within">
|
||||||
|
<h2>{{ formatMessage(compatibilityMessages.title) }}</h2>
|
||||||
|
<section>
|
||||||
|
<h3>{{ formatMessage(compatibilityMessages.minecraftJava) }}</h3>
|
||||||
|
<div class="tag-list">
|
||||||
|
<div
|
||||||
|
v-for="version in getVersionsToDisplay(project)"
|
||||||
|
:key="`version-tag-${version}`"
|
||||||
|
class="tag-list__item"
|
||||||
|
>
|
||||||
|
{{ version }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section v-if="project.project_type !== 'resourcepack'">
|
||||||
|
<h3>{{ formatMessage(compatibilityMessages.platforms) }}</h3>
|
||||||
|
<div class="tag-list">
|
||||||
|
<div
|
||||||
|
v-for="platform in project.loaders"
|
||||||
|
:key="`platform-tag-${platform}`"
|
||||||
|
:class="`tag-list__item`"
|
||||||
|
:style="`--_color: var(--color-platform-${platform})`"
|
||||||
|
>
|
||||||
|
<svg v-html="tags.loaders.find((x) => x.name === platform).icon"></svg>
|
||||||
|
{{ formatCategory(platform) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
|
v-if="
|
||||||
|
(project.actualProjectType === 'mod' || project.project_type === 'modpack') &&
|
||||||
|
!(project.client_side === 'unsupported' && project.server_side === 'unsupported') &&
|
||||||
|
!(project.client_side === 'unknown' && project.server_side === 'unknown')
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<h3>{{ formatMessage(compatibilityMessages.environments) }}</h3>
|
||||||
|
<div class="status-list">
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
(project.client_side === 'required' && project.server_side !== 'required') ||
|
||||||
|
(project.client_side === 'optional' && project.server_side === 'optional')
|
||||||
|
"
|
||||||
|
class="status-list__item"
|
||||||
|
>
|
||||||
|
<ClientIcon aria-hidden="true" />
|
||||||
|
Client-side
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
(project.server_side === 'required' && project.client_side !== 'required') ||
|
||||||
|
(project.client_side === 'optional' && project.server_side === 'optional')
|
||||||
|
"
|
||||||
|
class="status-list__item"
|
||||||
|
>
|
||||||
|
<ServerIcon aria-hidden="true" />
|
||||||
|
Server-side
|
||||||
|
</div>
|
||||||
|
<div v-if="false" class="status-list__item">
|
||||||
|
<UserIcon aria-hidden="true" />
|
||||||
|
Singleplayer
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="project.client_side === 'required' && project.server_side === 'required'"
|
||||||
|
class="status-list__item"
|
||||||
|
>
|
||||||
|
<MonitorSmartphoneIcon aria-hidden="true" />
|
||||||
|
Client and server
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="
|
||||||
|
project.client_side === 'optional' ||
|
||||||
|
(project.client_side === 'required' && project.server_side === 'optional') ||
|
||||||
|
project.server_side === 'optional' ||
|
||||||
|
(project.server_side === 'required' && project.client_side === 'optional')
|
||||||
|
"
|
||||||
|
class="status-list__item"
|
||||||
|
>
|
||||||
|
<MonitorSmartphoneIcon aria-hidden="true" />
|
||||||
|
Client and server <span class="text-sm">(optional)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<AdPlaceholder
|
||||||
|
v-if="
|
||||||
|
(!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus) &&
|
||||||
|
tags.approvedStatuses.includes(project.status)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
project.issues_url ||
|
||||||
|
project.source_url ||
|
||||||
|
project.wiki_url ||
|
||||||
|
project.discord_url ||
|
||||||
|
project.donation_urls.length > 0
|
||||||
|
"
|
||||||
|
class="card flex-card experimental-styles-within"
|
||||||
|
>
|
||||||
|
<h2>{{ formatMessage(linksMessages.title) }}</h2>
|
||||||
|
<div class="links-list">
|
||||||
|
<a
|
||||||
|
v-if="project.issues_url"
|
||||||
|
:href="project.issues_url"
|
||||||
|
:target="$external()"
|
||||||
|
rel="noopener nofollow ugc"
|
||||||
|
>
|
||||||
|
<IssuesIcon aria-hidden="true" />
|
||||||
|
{{ formatMessage(linksMessages.issues) }}
|
||||||
|
<ExternalIcon aria-hidden="true" class="external-icon" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
v-if="project.source_url"
|
||||||
|
:href="project.source_url"
|
||||||
|
:target="$external()"
|
||||||
|
rel="noopener nofollow ugc"
|
||||||
|
>
|
||||||
|
<CodeIcon aria-hidden="true" />
|
||||||
|
{{ formatMessage(linksMessages.source) }}
|
||||||
|
<ExternalIcon aria-hidden="true" class="external-icon" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
v-if="project.wiki_url"
|
||||||
|
:href="project.wiki_url"
|
||||||
|
:target="$external()"
|
||||||
|
rel="noopener nofollow ugc"
|
||||||
|
>
|
||||||
|
<WikiIcon aria-hidden="true" />
|
||||||
|
{{ formatMessage(linksMessages.wiki) }}
|
||||||
|
<ExternalIcon aria-hidden="true" class="external-icon" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
v-if="project.discord_url"
|
||||||
|
:href="project.discord_url"
|
||||||
|
:target="$external()"
|
||||||
|
rel="noopener nofollow ugc"
|
||||||
|
>
|
||||||
|
<DiscordIcon class="shrink" aria-hidden="true" />
|
||||||
|
{{ formatMessage(linksMessages.discord) }}
|
||||||
|
<ExternalIcon aria-hidden="true" class="external-icon" />
|
||||||
|
</a>
|
||||||
|
<hr
|
||||||
|
v-if="
|
||||||
|
(project.issues_url ||
|
||||||
|
project.source_url ||
|
||||||
|
project.wiki_url ||
|
||||||
|
project.discord_url) &&
|
||||||
|
project.donation_urls.length > 0
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
v-for="(donation, index) in project.donation_urls"
|
||||||
|
:key="index"
|
||||||
|
:href="donation.url"
|
||||||
|
:target="$external()"
|
||||||
|
rel="noopener nofollow ugc"
|
||||||
|
>
|
||||||
|
<BuyMeACoffeeIcon v-if="donation.id === 'bmac'" aria-hidden="true" />
|
||||||
|
<PatreonIcon v-else-if="donation.id === 'patreon'" aria-hidden="true" />
|
||||||
|
<KoFiIcon v-else-if="donation.id === 'ko-fi'" aria-hidden="true" />
|
||||||
|
<PayPalIcon v-else-if="donation.id === 'paypal'" aria-hidden="true" />
|
||||||
|
<OpenCollectiveIcon
|
||||||
|
v-else-if="donation.id === 'open-collective'"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<HeartIcon v-else-if="donation.id === 'github'" />
|
||||||
|
<CurrencyIcon v-else />
|
||||||
|
<span v-if="donation.id === 'bmac'">{{
|
||||||
|
formatMessage(linksMessages.donateBmac)
|
||||||
|
}}</span>
|
||||||
|
<span v-else-if="donation.id === 'patreon'">{{
|
||||||
|
formatMessage(linksMessages.donatePatreon)
|
||||||
|
}}</span>
|
||||||
|
<span v-else-if="donation.id === 'paypal'">{{
|
||||||
|
formatMessage(linksMessages.donatePayPal)
|
||||||
|
}}</span>
|
||||||
|
<span v-else-if="donation.id === 'ko-fi'">{{
|
||||||
|
formatMessage(linksMessages.donateKoFi)
|
||||||
|
}}</span>
|
||||||
|
<span v-else-if="donation.id === 'github'">{{
|
||||||
|
formatMessage(linksMessages.donateGithub)
|
||||||
|
}}</span>
|
||||||
|
<span v-else>{{ formatMessage(linksMessages.donateGeneric) }}</span>
|
||||||
|
<ExternalIcon aria-hidden="true" class="external-icon" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card flex-card experimental-styles-within">
|
||||||
|
<h2>{{ formatMessage(creatorsMessages.title) }}</h2>
|
||||||
|
<div class="details-list">
|
||||||
|
<template v-if="organization">
|
||||||
|
<nuxt-link
|
||||||
|
class="details-list__item details-list__item--type-large"
|
||||||
|
:to="`/organization/${organization.slug}`"
|
||||||
|
>
|
||||||
|
<Avatar :src="organization.icon_url" :alt="organization.name" size="32px" />
|
||||||
|
<div class="rows">
|
||||||
|
<span>
|
||||||
|
{{ organization.name }}
|
||||||
|
</span>
|
||||||
|
<span class="details-list__item__text--style-secondary">Organization</span>
|
||||||
|
</div>
|
||||||
|
</nuxt-link>
|
||||||
|
<hr v-if="members.length > 0" />
|
||||||
|
</template>
|
||||||
|
<nuxt-link
|
||||||
|
v-for="member in members"
|
||||||
|
:key="`member-${member.id}`"
|
||||||
|
class="details-list__item details-list__item--type-large"
|
||||||
|
:to="'/user/' + member.user.username"
|
||||||
|
>
|
||||||
|
<Avatar :src="member.avatar_url" :alt="member.name" size="32px" circle />
|
||||||
|
<div class="rows">
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
{{ member.name }}
|
||||||
|
<CrownIcon
|
||||||
|
v-if="member.is_owner"
|
||||||
|
v-tooltip="formatMessage(creatorsMessages.owner)"
|
||||||
|
class="text-brand-orange"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span class="details-list__item__text--style-secondary">{{ member.role }}</span>
|
||||||
|
</div>
|
||||||
|
</nuxt-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card flex-card experimental-styles-within">
|
||||||
|
<h2>{{ formatMessage(detailsMessages.title) }}</h2>
|
||||||
|
<div class="details-list">
|
||||||
|
<div class="details-list__item">
|
||||||
|
<BookTextIcon aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
Licensed
|
||||||
|
<a
|
||||||
|
v-if="project.license.url"
|
||||||
|
class="text-link hover:underline"
|
||||||
|
:href="project.license.url"
|
||||||
|
:target="$external()"
|
||||||
|
rel="noopener nofollow ugc"
|
||||||
|
>
|
||||||
|
{{ licenseIdDisplay }}
|
||||||
|
<ExternalIcon aria-hidden="true" class="external-icon ml-1 mt-[-1px] inline" />
|
||||||
|
</a>
|
||||||
|
<span
|
||||||
|
v-else-if="
|
||||||
|
project.license.id === 'LicenseRef-All-Rights-Reserved' ||
|
||||||
|
!project.license.id.includes('LicenseRef')
|
||||||
|
"
|
||||||
|
class="text-link hover:underline"
|
||||||
|
@click="(event) => getLicenseData(event)"
|
||||||
|
>
|
||||||
|
{{ licenseIdDisplay }}
|
||||||
|
</span>
|
||||||
|
<span v-else>{{ licenseIdDisplay }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="project.approved"
|
||||||
|
v-tooltip="$dayjs(project.approved).format('MMMM D, YYYY [at] h:mm A')"
|
||||||
|
class="details-list__item"
|
||||||
|
>
|
||||||
|
<CalendarIcon aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
{{ formatMessage(detailsMessages.published, { date: publishedDate }) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
v-tooltip="$dayjs(project.published).format('MMMM D, YYYY [at] h:mm A')"
|
||||||
|
class="details-list__item"
|
||||||
|
>
|
||||||
|
<CalendarIcon aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
{{ formatMessage(detailsMessages.created, { date: createdDate }) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="project.status === 'processing' && project.queued"
|
||||||
|
v-tooltip="$dayjs(project.queued).format('MMMM D, YYYY [at] h:mm A')"
|
||||||
|
class="details-list__item"
|
||||||
|
>
|
||||||
|
<ScaleIcon aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
{{ formatMessage(detailsMessages.submitted, { date: submittedDate }) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="versions.length > 0 && project.updated"
|
||||||
|
v-tooltip="$dayjs(project.updated).format('MMMM D, YYYY [at] h:mm A')"
|
||||||
|
class="details-list__item"
|
||||||
|
>
|
||||||
|
<VersionIcon aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
{{ formatMessage(detailsMessages.updated, { date: updatedDate }) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NuxtPage
|
<div class="normal-page__content">
|
||||||
v-model:project="project"
|
<div class="overflow-x-auto">
|
||||||
v-model:versions="versions"
|
<NavTabs :links="navLinks" class="mb-4" />
|
||||||
v-model:featured-versions="featuredVersions"
|
</div>
|
||||||
v-model:members="members"
|
<NuxtPage
|
||||||
v-model:all-members="allMembers"
|
v-model:project="project"
|
||||||
v-model:dependencies="dependencies"
|
v-model:versions="versions"
|
||||||
v-model:organization="organization"
|
v-model:featured-versions="featuredVersions"
|
||||||
:current-member="currentMember"
|
v-model:members="members"
|
||||||
:reset-project="resetProject"
|
v-model:all-members="allMembers"
|
||||||
:reset-organization="resetOrganization"
|
v-model:dependencies="dependencies"
|
||||||
:reset-members="resetMembers"
|
v-model:organization="organization"
|
||||||
:route="route"
|
:current-member="currentMember"
|
||||||
@on-download="triggerDownloadAnimation"
|
:reset-project="resetProject"
|
||||||
/>
|
:reset-organization="resetOrganization"
|
||||||
|
:reset-members="resetMembers"
|
||||||
|
:route="route"
|
||||||
|
@on-download="triggerDownloadAnimation"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ModerationChecklist
|
<ModerationChecklist
|
||||||
v-if="auth.user && tags.staffRoles.includes(auth.user.role) && showModerationChecklist"
|
v-if="auth.user && tags.staffRoles.includes(auth.user.role) && showModerationChecklist"
|
||||||
@@ -703,6 +1018,23 @@ import {
|
|||||||
UsersIcon,
|
UsersIcon,
|
||||||
VersionIcon,
|
VersionIcon,
|
||||||
WrenchIcon,
|
WrenchIcon,
|
||||||
|
ClientIcon,
|
||||||
|
BookTextIcon,
|
||||||
|
MonitorSmartphoneIcon,
|
||||||
|
WikiIcon,
|
||||||
|
DiscordIcon,
|
||||||
|
CalendarIcon,
|
||||||
|
KoFiIcon,
|
||||||
|
BuyMeACoffeeIcon,
|
||||||
|
IssuesIcon,
|
||||||
|
UserIcon,
|
||||||
|
PayPalIcon,
|
||||||
|
ServerIcon,
|
||||||
|
PatreonIcon,
|
||||||
|
CrownIcon,
|
||||||
|
OpenCollectiveIcon,
|
||||||
|
CodeIcon,
|
||||||
|
CurrencyIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
@@ -713,7 +1045,7 @@ import {
|
|||||||
PopoutMenu,
|
PopoutMenu,
|
||||||
ScrollablePanel,
|
ScrollablePanel,
|
||||||
} from "@modrinth/ui";
|
} from "@modrinth/ui";
|
||||||
import { formatCategory, isRejected, isStaff, isUnderReview } from "@modrinth/utils";
|
import { formatCategory, isRejected, isStaff, isUnderReview, renderString } from "@modrinth/utils";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import Badge from "~/components/ui/Badge.vue";
|
import Badge from "~/components/ui/Badge.vue";
|
||||||
import NavTabs from "~/components/ui/NavTabs.vue";
|
import NavTabs from "~/components/ui/NavTabs.vue";
|
||||||
@@ -730,6 +1062,8 @@ import Accordion from "~/components/ui/Accordion.vue";
|
|||||||
import ModrinthIcon from "~/assets/images/utils/modrinth.svg?component";
|
import ModrinthIcon from "~/assets/images/utils/modrinth.svg?component";
|
||||||
import VersionSummary from "~/components/ui/VersionSummary.vue";
|
import VersionSummary from "~/components/ui/VersionSummary.vue";
|
||||||
import AutomaticAccordion from "~/components/ui/AutomaticAccordion.vue";
|
import AutomaticAccordion from "~/components/ui/AutomaticAccordion.vue";
|
||||||
|
import { getVersionsToDisplay } from "~/helpers/projects.js";
|
||||||
|
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
||||||
|
|
||||||
const data = useNuxtApp();
|
const data = useNuxtApp();
|
||||||
const route = useNativeRoute();
|
const route = useNativeRoute();
|
||||||
@@ -738,6 +1072,7 @@ const auth = await useAuth();
|
|||||||
const user = await useUser();
|
const user = await useUser();
|
||||||
|
|
||||||
const tags = useTags();
|
const tags = useTags();
|
||||||
|
const flags = useFeatureFlags();
|
||||||
const cosmetics = useCosmetics();
|
const cosmetics = useCosmetics();
|
||||||
|
|
||||||
const { formatMessage } = useVIntl();
|
const { formatMessage } = useVIntl();
|
||||||
@@ -789,6 +1124,152 @@ const gameVersionAccordion = ref();
|
|||||||
const platformAccordion = ref();
|
const platformAccordion = ref();
|
||||||
const getModrinthAppAccordion = ref();
|
const getModrinthAppAccordion = ref();
|
||||||
|
|
||||||
|
const formatRelativeTime = useRelativeTime();
|
||||||
|
|
||||||
|
const compatibilityMessages = defineMessages({
|
||||||
|
title: {
|
||||||
|
id: "project.about.compatibility.title",
|
||||||
|
defaultMessage: "Compatibility",
|
||||||
|
},
|
||||||
|
minecraftJava: {
|
||||||
|
id: "project.about.compatibility.game.minecraftJava",
|
||||||
|
defaultMessage: "Minecraft: Java Edition",
|
||||||
|
},
|
||||||
|
platforms: {
|
||||||
|
id: "project.about.compatibility.platforms",
|
||||||
|
defaultMessage: "Platforms",
|
||||||
|
},
|
||||||
|
environments: {
|
||||||
|
id: "project.about.compatibility.environments",
|
||||||
|
defaultMessage: "Supported environments",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const linksMessages = defineMessages({
|
||||||
|
title: {
|
||||||
|
id: "project.about.links.title",
|
||||||
|
defaultMessage: "Links",
|
||||||
|
},
|
||||||
|
issues: {
|
||||||
|
id: "project.about.links.issues",
|
||||||
|
defaultMessage: "Report issues",
|
||||||
|
},
|
||||||
|
source: {
|
||||||
|
id: "project.about.links.source",
|
||||||
|
defaultMessage: "View source",
|
||||||
|
},
|
||||||
|
wiki: {
|
||||||
|
id: "project.about.links.wiki",
|
||||||
|
defaultMessage: "Visit wiki",
|
||||||
|
},
|
||||||
|
discord: {
|
||||||
|
id: "project.about.links.discord",
|
||||||
|
defaultMessage: "Join Discord server",
|
||||||
|
},
|
||||||
|
donateGeneric: {
|
||||||
|
id: "project.about.links.donate.generic",
|
||||||
|
defaultMessage: "Donate",
|
||||||
|
},
|
||||||
|
donateGitHub: {
|
||||||
|
id: "project.about.links.donate.github",
|
||||||
|
defaultMessage: "Sponsor on GitHub",
|
||||||
|
},
|
||||||
|
donateBmac: {
|
||||||
|
id: "project.about.links.donate.bmac",
|
||||||
|
defaultMessage: "Buy Me a Coffee",
|
||||||
|
},
|
||||||
|
donatePatreon: {
|
||||||
|
id: "project.about.links.donate.patreon",
|
||||||
|
defaultMessage: "Donate on Patreon",
|
||||||
|
},
|
||||||
|
donatePayPal: {
|
||||||
|
id: "project.about.links.donate.paypal",
|
||||||
|
defaultMessage: "Donate on PayPal",
|
||||||
|
},
|
||||||
|
donateKoFi: {
|
||||||
|
id: "project.about.links.donate.kofi",
|
||||||
|
defaultMessage: "Donate on Ko-fi",
|
||||||
|
},
|
||||||
|
donateGithub: {
|
||||||
|
id: "project.about.links.donate.github",
|
||||||
|
defaultMessage: "Sponsor on GitHub",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const creatorsMessages = defineMessages({
|
||||||
|
title: {
|
||||||
|
id: "project.about.creators.title",
|
||||||
|
defaultMessage: "Creators",
|
||||||
|
},
|
||||||
|
owner: {
|
||||||
|
id: "project.about.creators.owner",
|
||||||
|
defaultMessage: "Project owner",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const detailsMessages = defineMessages({
|
||||||
|
title: {
|
||||||
|
id: "project.about.details.title",
|
||||||
|
defaultMessage: "Details",
|
||||||
|
},
|
||||||
|
licensed: {
|
||||||
|
id: "project.about.details.licensed",
|
||||||
|
defaultMessage: "Licensed {license}",
|
||||||
|
},
|
||||||
|
created: {
|
||||||
|
id: "project.about.details.created",
|
||||||
|
defaultMessage: "Created {date}",
|
||||||
|
},
|
||||||
|
submitted: {
|
||||||
|
id: "project.about.details.submitted",
|
||||||
|
defaultMessage: "Submitted {date}",
|
||||||
|
},
|
||||||
|
published: {
|
||||||
|
id: "project.about.details.published",
|
||||||
|
defaultMessage: "Published {date}",
|
||||||
|
},
|
||||||
|
updated: {
|
||||||
|
id: "project.about.details.updated",
|
||||||
|
defaultMessage: "Updated {date}",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const modalLicense = ref(null);
|
||||||
|
const licenseText = ref("");
|
||||||
|
|
||||||
|
const createdDate = computed(() =>
|
||||||
|
project.value.published ? formatRelativeTime(project.value.published) : "unknown",
|
||||||
|
);
|
||||||
|
const submittedDate = computed(() =>
|
||||||
|
project.value.queued ? formatRelativeTime(project.value.queued) : "unknown",
|
||||||
|
);
|
||||||
|
const publishedDate = computed(() =>
|
||||||
|
project.value.approved ? formatRelativeTime(project.value.approved) : "unknown",
|
||||||
|
);
|
||||||
|
const updatedDate = computed(() =>
|
||||||
|
project.value.updated ? formatRelativeTime(project.value.updated) : "unknown",
|
||||||
|
);
|
||||||
|
|
||||||
|
const licenseIdDisplay = computed(() => {
|
||||||
|
const id = project.value.license.id;
|
||||||
|
|
||||||
|
if (id === "LicenseRef-All-Rights-Reserved") {
|
||||||
|
return "ARR";
|
||||||
|
} else if (id.includes("LicenseRef")) {
|
||||||
|
return id.replaceAll("LicenseRef-", "").replaceAll("-", " ");
|
||||||
|
} else {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function getLicenseData(event) {
|
||||||
|
modalLicense.value.show(event);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const text = await useBaseFetch(`tag/license/${project.value.license.id}`);
|
||||||
|
licenseText.value = text.body || "License text could not be retrieved.";
|
||||||
|
} catch {
|
||||||
|
licenseText.value = "License text could not be retrieved.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const filteredVersions = computed(() => {
|
const filteredVersions = computed(() => {
|
||||||
return versions.value.filter(
|
return versions.value.filter(
|
||||||
(x) =>
|
(x) =>
|
||||||
@@ -830,9 +1311,9 @@ const messages = defineMessages({
|
|||||||
id: "project.stats.followers-label",
|
id: "project.stats.followers-label",
|
||||||
defaultMessage: "follower{count, plural, one {} other {s}}",
|
defaultMessage: "follower{count, plural, one {} other {s}}",
|
||||||
},
|
},
|
||||||
aboutTab: {
|
descriptionTab: {
|
||||||
id: "project.about.title",
|
id: "project.description.title",
|
||||||
defaultMessage: "About",
|
defaultMessage: "Description",
|
||||||
},
|
},
|
||||||
galleryTab: {
|
galleryTab: {
|
||||||
id: "project.gallery.title",
|
id: "project.gallery.title",
|
||||||
@@ -1223,7 +1704,7 @@ const navLinks = computed(() => {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: formatMessage(messages.aboutTab),
|
label: formatMessage(messages.descriptionTab),
|
||||||
href: projectUrl,
|
href: projectUrl,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
<div class="mb-3 flex">
|
||||||
|
<VersionFilterControl :versions="props.versions" @switch-page="switchPage" />
|
||||||
|
<Pagination
|
||||||
|
:page="currentPage"
|
||||||
|
:count="Math.ceil(filteredVersions.length / 20)"
|
||||||
|
class="ml-auto mt-auto"
|
||||||
|
:link-function="(page) => `?page=${page}`"
|
||||||
|
@switch-page="switchPage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="card changelog-wrapper">
|
<div class="card changelog-wrapper">
|
||||||
<div
|
<div
|
||||||
v-for="version in filteredVersions.slice((currentPage - 1) * 20, currentPage * 20)"
|
v-for="version in filteredVersions.slice((currentPage - 1) * 20, currentPage * 20)"
|
||||||
@@ -57,15 +67,6 @@
|
|||||||
@switch-page="switchPage"
|
@switch-page="switchPage"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="normal-page__sidebar">
|
|
||||||
<AdPlaceholder
|
|
||||||
v-if="
|
|
||||||
(!auth.user || !isPermission(auth.user.badges, 1 << 0)) &&
|
|
||||||
tags.approvedStatuses.includes(project.status)
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<VersionFilterControl :versions="props.versions" @switch-page="switchPage" />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Pagination } from "@modrinth/ui";
|
import { Pagination } from "@modrinth/ui";
|
||||||
@@ -73,7 +74,6 @@ import { DownloadIcon } from "@modrinth/assets";
|
|||||||
|
|
||||||
import { renderHighlightedString } from "~/helpers/highlight.js";
|
import { renderHighlightedString } from "~/helpers/highlight.js";
|
||||||
import VersionFilterControl from "~/components/ui/VersionFilterControl.vue";
|
import VersionFilterControl from "~/components/ui/VersionFilterControl.vue";
|
||||||
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
project: {
|
project: {
|
||||||
@@ -96,9 +96,6 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const auth = await useAuth();
|
|
||||||
const tags = useTags();
|
|
||||||
|
|
||||||
const title = `${props.project.title} - Changelog`;
|
const title = `${props.project.title} - Changelog`;
|
||||||
const description = `View the changelog of ${props.project.title}'s ${props.versions.length} versions.`;
|
const description = `View the changelog of ${props.project.title}'s ${props.versions.length} versions.`;
|
||||||
|
|
||||||
@@ -114,9 +111,9 @@ const route = useNativeRoute();
|
|||||||
|
|
||||||
const currentPage = ref(Number(route.query.page ?? 1));
|
const currentPage = ref(Number(route.query.page ?? 1));
|
||||||
const filteredVersions = computed(() => {
|
const filteredVersions = computed(() => {
|
||||||
const selectedGameVersions = getArrayOrString(route.query.gameVersion) ?? [];
|
const selectedGameVersions = getArrayOrString(route.query.g) ?? [];
|
||||||
const selectedLoaders = getArrayOrString(route.query.platform) ?? [];
|
const selectedLoaders = getArrayOrString(route.query.l) ?? [];
|
||||||
const selectedVersionTypes = getArrayOrString(route.query.type) ?? [];
|
const selectedVersionTypes = getArrayOrString(route.query.c) ?? [];
|
||||||
|
|
||||||
return props.versions.filter(
|
return props.versions.filter(
|
||||||
(projectVersion) =>
|
(projectVersion) =>
|
||||||
|
|||||||
@@ -1,18 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<NewModal ref="modalLicense" :header="project.license.name ? project.license.name : 'License'">
|
|
||||||
<template #title>
|
|
||||||
<Avatar :src="project.icon_url" :alt="project.title" class="icon" size="32px" no-shadow />
|
|
||||||
<span class="text-lg font-extrabold text-contrast">
|
|
||||||
{{ project.license.name ? project.license.name : "License" }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<div
|
|
||||||
class="markdown-body"
|
|
||||||
v-html="
|
|
||||||
renderString(licenseText).isEmpty ? 'Loading license text...' : renderString(licenseText)
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</NewModal>
|
|
||||||
<section class="normal-page__content">
|
<section class="normal-page__content">
|
||||||
<div
|
<div
|
||||||
v-if="project.body"
|
v-if="project.body"
|
||||||
@@ -20,333 +6,12 @@
|
|||||||
v-html="renderHighlightedString(project.body || '')"
|
v-html="renderHighlightedString(project.body || '')"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
<div class="normal-page__sidebar">
|
|
||||||
<AdPlaceholder
|
|
||||||
v-if="
|
|
||||||
(!auth.user || !isPermission(auth.user.badges, 1 << 0)) &&
|
|
||||||
tags.approvedStatuses.includes(project.status)
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<div v-if="versions.length > 0" class="card flex-card experimental-styles-within">
|
|
||||||
<h2>{{ formatMessage(compatibilityMessages.title) }}</h2>
|
|
||||||
<section>
|
|
||||||
<h3>{{ formatMessage(compatibilityMessages.minecraftJava) }}</h3>
|
|
||||||
<div class="tag-list">
|
|
||||||
<div
|
|
||||||
v-for="version in getVersionsToDisplay(project)"
|
|
||||||
:key="`version-tag-${version}`"
|
|
||||||
class="tag-list__item"
|
|
||||||
>
|
|
||||||
{{ version }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section v-if="project.project_type !== 'resourcepack'">
|
|
||||||
<h3>{{ formatMessage(compatibilityMessages.platforms) }}</h3>
|
|
||||||
<div class="tag-list">
|
|
||||||
<div
|
|
||||||
v-for="platform in project.loaders"
|
|
||||||
:key="`platform-tag-${platform}`"
|
|
||||||
:class="`tag-list__item`"
|
|
||||||
:style="`--_color: var(--color-platform-${platform})`"
|
|
||||||
>
|
|
||||||
<svg v-html="tags.loaders.find((x) => x.name === platform).icon"></svg>
|
|
||||||
{{ formatCategory(platform) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section
|
|
||||||
v-if="
|
|
||||||
(project.actualProjectType === 'mod' || project.project_type === 'modpack') &&
|
|
||||||
!(project.client_side === 'unsupported' && project.server_side === 'unsupported') &&
|
|
||||||
!(project.client_side === 'unknown' && project.server_side === 'unknown')
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<h3>{{ formatMessage(compatibilityMessages.environments) }}</h3>
|
|
||||||
<div class="status-list">
|
|
||||||
<div
|
|
||||||
v-if="
|
|
||||||
(project.client_side === 'required' && project.server_side !== 'required') ||
|
|
||||||
(project.client_side === 'optional' && project.server_side === 'optional')
|
|
||||||
"
|
|
||||||
class="status-list__item"
|
|
||||||
>
|
|
||||||
<ClientIcon aria-hidden="true" />
|
|
||||||
Client-side
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="
|
|
||||||
(project.server_side === 'required' && project.client_side !== 'required') ||
|
|
||||||
(project.client_side === 'optional' && project.server_side === 'optional')
|
|
||||||
"
|
|
||||||
class="status-list__item"
|
|
||||||
>
|
|
||||||
<ServerIcon aria-hidden="true" />
|
|
||||||
Server-side
|
|
||||||
</div>
|
|
||||||
<div v-if="false" class="status-list__item">
|
|
||||||
<UserIcon aria-hidden="true" />
|
|
||||||
Singleplayer
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="project.client_side === 'required' && project.server_side === 'required'"
|
|
||||||
class="status-list__item"
|
|
||||||
>
|
|
||||||
<MonitorSmartphoneIcon aria-hidden="true" />
|
|
||||||
Client and server
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else-if="
|
|
||||||
project.client_side === 'optional' ||
|
|
||||||
(project.client_side === 'required' && project.server_side === 'optional') ||
|
|
||||||
project.server_side === 'optional' ||
|
|
||||||
(project.server_side === 'required' && project.client_side === 'optional')
|
|
||||||
"
|
|
||||||
class="status-list__item"
|
|
||||||
>
|
|
||||||
<MonitorSmartphoneIcon aria-hidden="true" />
|
|
||||||
Client and server <span class="text-sm">(optional)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="
|
|
||||||
project.issues_url ||
|
|
||||||
project.source_url ||
|
|
||||||
project.wiki_url ||
|
|
||||||
project.discord_url ||
|
|
||||||
project.donation_urls.length > 0
|
|
||||||
"
|
|
||||||
class="card flex-card experimental-styles-within"
|
|
||||||
>
|
|
||||||
<h2>{{ formatMessage(linksMessages.title) }}</h2>
|
|
||||||
<div class="links-list">
|
|
||||||
<a
|
|
||||||
v-if="project.issues_url"
|
|
||||||
:href="project.issues_url"
|
|
||||||
:target="$external()"
|
|
||||||
rel="noopener nofollow ugc"
|
|
||||||
>
|
|
||||||
<IssuesIcon aria-hidden="true" />
|
|
||||||
{{ formatMessage(linksMessages.issues) }}
|
|
||||||
<ExternalIcon aria-hidden="true" class="external-icon" />
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
v-if="project.source_url"
|
|
||||||
:href="project.source_url"
|
|
||||||
:target="$external()"
|
|
||||||
rel="noopener nofollow ugc"
|
|
||||||
>
|
|
||||||
<CodeIcon aria-hidden="true" />
|
|
||||||
{{ formatMessage(linksMessages.source) }}
|
|
||||||
<ExternalIcon aria-hidden="true" class="external-icon" />
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
v-if="project.wiki_url"
|
|
||||||
:href="project.wiki_url"
|
|
||||||
:target="$external()"
|
|
||||||
rel="noopener nofollow ugc"
|
|
||||||
>
|
|
||||||
<WikiIcon aria-hidden="true" />
|
|
||||||
{{ formatMessage(linksMessages.wiki) }}
|
|
||||||
<ExternalIcon aria-hidden="true" class="external-icon" />
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
v-if="project.discord_url"
|
|
||||||
:href="project.discord_url"
|
|
||||||
:target="$external()"
|
|
||||||
rel="noopener nofollow ugc"
|
|
||||||
>
|
|
||||||
<DiscordIcon class="shrink" aria-hidden="true" />
|
|
||||||
{{ formatMessage(linksMessages.discord) }}
|
|
||||||
<ExternalIcon aria-hidden="true" class="external-icon" />
|
|
||||||
</a>
|
|
||||||
<hr
|
|
||||||
v-if="
|
|
||||||
(project.issues_url || project.source_url || project.wiki_url || project.discord_url) &&
|
|
||||||
project.donation_urls.length > 0
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<a
|
|
||||||
v-for="(donation, index) in project.donation_urls"
|
|
||||||
:key="index"
|
|
||||||
:href="donation.url"
|
|
||||||
:target="$external()"
|
|
||||||
rel="noopener nofollow ugc"
|
|
||||||
>
|
|
||||||
<BuyMeACoffeeIcon v-if="donation.id === 'bmac'" aria-hidden="true" />
|
|
||||||
<PatreonIcon v-else-if="donation.id === 'patreon'" aria-hidden="true" />
|
|
||||||
<KoFiIcon v-else-if="donation.id === 'ko-fi'" aria-hidden="true" />
|
|
||||||
<PayPalIcon v-else-if="donation.id === 'paypal'" aria-hidden="true" />
|
|
||||||
<OpenCollectiveIcon v-else-if="donation.id === 'open-collective'" aria-hidden="true" />
|
|
||||||
<HeartIcon v-else-if="donation.id === 'github'" />
|
|
||||||
<CurrencyIcon v-else />
|
|
||||||
<span v-if="donation.id === 'bmac'">{{ formatMessage(linksMessages.donateBmac) }}</span>
|
|
||||||
<span v-else-if="donation.id === 'patreon'">{{
|
|
||||||
formatMessage(linksMessages.donatePatreon)
|
|
||||||
}}</span>
|
|
||||||
<span v-else-if="donation.id === 'paypal'">{{
|
|
||||||
formatMessage(linksMessages.donatePayPal)
|
|
||||||
}}</span>
|
|
||||||
<span v-else-if="donation.id === 'ko-fi'">{{
|
|
||||||
formatMessage(linksMessages.donateKoFi)
|
|
||||||
}}</span>
|
|
||||||
<span v-else-if="donation.id === 'github'">{{
|
|
||||||
formatMessage(linksMessages.donateGithub)
|
|
||||||
}}</span>
|
|
||||||
<span v-else>{{ formatMessage(linksMessages.donateGeneric) }}</span>
|
|
||||||
<ExternalIcon aria-hidden="true" class="external-icon" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card flex-card experimental-styles-within">
|
|
||||||
<h2>{{ formatMessage(creatorsMessages.title) }}</h2>
|
|
||||||
<div class="details-list">
|
|
||||||
<template v-if="organization">
|
|
||||||
<nuxt-link
|
|
||||||
class="details-list__item details-list__item--type-large"
|
|
||||||
:to="`/organization/${organization.slug}`"
|
|
||||||
>
|
|
||||||
<Avatar :src="organization.icon_url" :alt="organization.name" size="32px" />
|
|
||||||
<div class="rows">
|
|
||||||
<span>
|
|
||||||
{{ organization.name }}
|
|
||||||
</span>
|
|
||||||
<span class="details-list__item__text--style-secondary">Organization</span>
|
|
||||||
</div>
|
|
||||||
</nuxt-link>
|
|
||||||
<hr v-if="members.length > 0" />
|
|
||||||
</template>
|
|
||||||
<nuxt-link
|
|
||||||
v-for="member in members"
|
|
||||||
:key="`member-${member.id}`"
|
|
||||||
class="details-list__item details-list__item--type-large"
|
|
||||||
:to="'/user/' + member.user.username"
|
|
||||||
>
|
|
||||||
<Avatar :src="member.avatar_url" :alt="member.name" size="32px" circle />
|
|
||||||
<div class="rows">
|
|
||||||
<span class="flex items-center gap-1">
|
|
||||||
{{ member.name }}
|
|
||||||
<CrownIcon
|
|
||||||
v-if="member.is_owner"
|
|
||||||
v-tooltip="formatMessage(creatorsMessages.owner)"
|
|
||||||
class="text-brand-orange"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span class="details-list__item__text--style-secondary">{{ member.role }}</span>
|
|
||||||
</div>
|
|
||||||
</nuxt-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card flex-card experimental-styles-within">
|
|
||||||
<h2>{{ formatMessage(detailsMessages.title) }}</h2>
|
|
||||||
<div class="details-list">
|
|
||||||
<div class="details-list__item">
|
|
||||||
<BookTextIcon aria-hidden="true" />
|
|
||||||
<div>
|
|
||||||
Licensed
|
|
||||||
<a
|
|
||||||
v-if="project.license.url"
|
|
||||||
class="text-link hover:underline"
|
|
||||||
:href="project.license.url"
|
|
||||||
:target="$external()"
|
|
||||||
rel="noopener nofollow ugc"
|
|
||||||
>
|
|
||||||
{{ licenseIdDisplay }}
|
|
||||||
<ExternalIcon aria-hidden="true" class="external-icon ml-1 mt-[-1px] inline" />
|
|
||||||
</a>
|
|
||||||
<span
|
|
||||||
v-else-if="
|
|
||||||
project.license.id === 'LicenseRef-All-Rights-Reserved' ||
|
|
||||||
!project.license.id.includes('LicenseRef')
|
|
||||||
"
|
|
||||||
class="text-link hover:underline"
|
|
||||||
@click="(event) => getLicenseData(event)"
|
|
||||||
>
|
|
||||||
{{ licenseIdDisplay }}
|
|
||||||
</span>
|
|
||||||
<span v-else>{{ licenseIdDisplay }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="project.approved"
|
|
||||||
v-tooltip="$dayjs(project.approved).format('MMMM D, YYYY [at] h:mm A')"
|
|
||||||
class="details-list__item"
|
|
||||||
>
|
|
||||||
<CalendarIcon aria-hidden="true" />
|
|
||||||
<div>
|
|
||||||
{{ formatMessage(detailsMessages.published, { date: publishedDate }) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
v-tooltip="$dayjs(project.published).format('MMMM D, YYYY [at] h:mm A')"
|
|
||||||
class="details-list__item"
|
|
||||||
>
|
|
||||||
<CalendarIcon aria-hidden="true" />
|
|
||||||
<div>
|
|
||||||
{{ formatMessage(detailsMessages.created, { date: createdDate }) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="project.status === 'processing' && project.queued"
|
|
||||||
v-tooltip="$dayjs(project.queued).format('MMMM D, YYYY [at] h:mm A')"
|
|
||||||
class="details-list__item"
|
|
||||||
>
|
|
||||||
<ScaleIcon aria-hidden="true" />
|
|
||||||
<div>
|
|
||||||
{{ formatMessage(detailsMessages.submitted, { date: submittedDate }) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="versions.length > 0 && project.updated"
|
|
||||||
v-tooltip="$dayjs(project.updated).format('MMMM D, YYYY [at] h:mm A')"
|
|
||||||
class="details-list__item"
|
|
||||||
>
|
|
||||||
<VersionIcon aria-hidden="true" />
|
|
||||||
<div>
|
|
||||||
{{ formatMessage(detailsMessages.updated, { date: updatedDate }) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
|
||||||
CalendarIcon,
|
|
||||||
IssuesIcon,
|
|
||||||
WikiIcon,
|
|
||||||
OpenCollectiveIcon,
|
|
||||||
DiscordIcon,
|
|
||||||
ScaleIcon,
|
|
||||||
KoFiIcon,
|
|
||||||
BookTextIcon,
|
|
||||||
PayPalIcon,
|
|
||||||
CrownIcon,
|
|
||||||
BuyMeACoffeeIcon,
|
|
||||||
CurrencyIcon,
|
|
||||||
PatreonIcon,
|
|
||||||
HeartIcon,
|
|
||||||
VersionIcon,
|
|
||||||
ExternalIcon,
|
|
||||||
CodeIcon,
|
|
||||||
UserIcon,
|
|
||||||
ServerIcon,
|
|
||||||
ClientIcon,
|
|
||||||
MonitorSmartphoneIcon,
|
|
||||||
} from "@modrinth/assets";
|
|
||||||
|
|
||||||
import { NewModal, Avatar } from "@modrinth/ui";
|
|
||||||
|
|
||||||
import { formatCategory, renderString } from "@modrinth/utils";
|
|
||||||
import { renderHighlightedString } from "~/helpers/highlight.js";
|
import { renderHighlightedString } from "~/helpers/highlight.js";
|
||||||
import { getVersionsToDisplay } from "~/helpers/projects.js";
|
|
||||||
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
|
||||||
|
|
||||||
const props = defineProps({
|
defineProps({
|
||||||
project: {
|
project: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default() {
|
default() {
|
||||||
@@ -372,153 +37,4 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const auth = await useAuth();
|
|
||||||
const tags = useTags();
|
|
||||||
const { formatMessage } = useVIntl();
|
|
||||||
const formatRelativeTime = useRelativeTime();
|
|
||||||
|
|
||||||
const compatibilityMessages = defineMessages({
|
|
||||||
title: {
|
|
||||||
id: "project.about.compatibility.title",
|
|
||||||
defaultMessage: "Compatibility",
|
|
||||||
},
|
|
||||||
minecraftJava: {
|
|
||||||
id: "project.about.compatibility.game.minecraftJava",
|
|
||||||
defaultMessage: "Minecraft: Java Edition",
|
|
||||||
},
|
|
||||||
platforms: {
|
|
||||||
id: "project.about.compatibility.platforms",
|
|
||||||
defaultMessage: "Platforms",
|
|
||||||
},
|
|
||||||
environments: {
|
|
||||||
id: "project.about.compatibility.environments",
|
|
||||||
defaultMessage: "Supported environments",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const linksMessages = defineMessages({
|
|
||||||
title: {
|
|
||||||
id: "project.about.links.title",
|
|
||||||
defaultMessage: "Links",
|
|
||||||
},
|
|
||||||
issues: {
|
|
||||||
id: "project.about.links.issues",
|
|
||||||
defaultMessage: "Report issues",
|
|
||||||
},
|
|
||||||
source: {
|
|
||||||
id: "project.about.links.source",
|
|
||||||
defaultMessage: "View source",
|
|
||||||
},
|
|
||||||
wiki: {
|
|
||||||
id: "project.about.links.wiki",
|
|
||||||
defaultMessage: "Visit wiki",
|
|
||||||
},
|
|
||||||
discord: {
|
|
||||||
id: "project.about.links.discord",
|
|
||||||
defaultMessage: "Join Discord server",
|
|
||||||
},
|
|
||||||
donateGeneric: {
|
|
||||||
id: "project.about.links.donate.generic",
|
|
||||||
defaultMessage: "Donate",
|
|
||||||
},
|
|
||||||
donateGitHub: {
|
|
||||||
id: "project.about.links.donate.github",
|
|
||||||
defaultMessage: "Sponsor on GitHub",
|
|
||||||
},
|
|
||||||
donateBmac: {
|
|
||||||
id: "project.about.links.donate.bmac",
|
|
||||||
defaultMessage: "Buy Me a Coffee",
|
|
||||||
},
|
|
||||||
donatePatreon: {
|
|
||||||
id: "project.about.links.donate.patreon",
|
|
||||||
defaultMessage: "Donate on Patreon",
|
|
||||||
},
|
|
||||||
donatePayPal: {
|
|
||||||
id: "project.about.links.donate.paypal",
|
|
||||||
defaultMessage: "Donate on PayPal",
|
|
||||||
},
|
|
||||||
donateKoFi: {
|
|
||||||
id: "project.about.links.donate.kofi",
|
|
||||||
defaultMessage: "Donate on Ko-fi",
|
|
||||||
},
|
|
||||||
donateGithub: {
|
|
||||||
id: "project.about.links.donate.github",
|
|
||||||
defaultMessage: "Sponsor on GitHub",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const creatorsMessages = defineMessages({
|
|
||||||
title: {
|
|
||||||
id: "project.about.creators.title",
|
|
||||||
defaultMessage: "Creators",
|
|
||||||
},
|
|
||||||
owner: {
|
|
||||||
id: "project.about.creators.owner",
|
|
||||||
defaultMessage: "Project owner",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const detailsMessages = defineMessages({
|
|
||||||
title: {
|
|
||||||
id: "project.about.details.title",
|
|
||||||
defaultMessage: "Details",
|
|
||||||
},
|
|
||||||
licensed: {
|
|
||||||
id: "project.about.details.licensed",
|
|
||||||
defaultMessage: "Licensed {license}",
|
|
||||||
},
|
|
||||||
created: {
|
|
||||||
id: "project.about.details.created",
|
|
||||||
defaultMessage: "Created {date}",
|
|
||||||
},
|
|
||||||
submitted: {
|
|
||||||
id: "project.about.details.submitted",
|
|
||||||
defaultMessage: "Submitted {date}",
|
|
||||||
},
|
|
||||||
published: {
|
|
||||||
id: "project.about.details.published",
|
|
||||||
defaultMessage: "Published {date}",
|
|
||||||
},
|
|
||||||
updated: {
|
|
||||||
id: "project.about.details.updated",
|
|
||||||
defaultMessage: "Updated {date}",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const modalLicense = ref(null);
|
|
||||||
const licenseText = ref("");
|
|
||||||
|
|
||||||
const createdDate = computed(() =>
|
|
||||||
props.project.published ? formatRelativeTime(props.project.published) : "unknown",
|
|
||||||
);
|
|
||||||
const submittedDate = computed(() =>
|
|
||||||
props.project.queued ? formatRelativeTime(props.project.queued) : "unknown",
|
|
||||||
);
|
|
||||||
const publishedDate = computed(() =>
|
|
||||||
props.project.approved ? formatRelativeTime(props.project.approved) : "unknown",
|
|
||||||
);
|
|
||||||
const updatedDate = computed(() =>
|
|
||||||
props.project.updated ? formatRelativeTime(props.project.updated) : "unknown",
|
|
||||||
);
|
|
||||||
|
|
||||||
const licenseIdDisplay = computed(() => {
|
|
||||||
const id = props.project.license.id;
|
|
||||||
|
|
||||||
if (id === "LicenseRef-All-Rights-Reserved") {
|
|
||||||
return "ARR";
|
|
||||||
} else if (id.includes("LicenseRef")) {
|
|
||||||
return id.replaceAll("LicenseRef-", "").replaceAll("-", " ");
|
|
||||||
} else {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async function getLicenseData(event) {
|
|
||||||
modalLicense.value.show(event);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const text = await useBaseFetch(`tag/license/${props.project.license.id}`);
|
|
||||||
licenseText.value = text.body || "License text could not be retrieved.";
|
|
||||||
} catch {
|
|
||||||
licenseText.value = "License text could not be retrieved.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -471,7 +471,7 @@
|
|||||||
<div class="normal-page__sidebar version-page__metadata">
|
<div class="normal-page__sidebar version-page__metadata">
|
||||||
<AdPlaceholder
|
<AdPlaceholder
|
||||||
v-if="
|
v-if="
|
||||||
(!auth.user || !isPermission(auth.user.badges, 1 << 0)) &&
|
(!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus) &&
|
||||||
tags.approvedStatuses.includes(project.status)
|
tags.approvedStatuses.includes(project.status)
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
@@ -749,6 +749,7 @@ export default defineNuxtComponent({
|
|||||||
|
|
||||||
const auth = await useAuth();
|
const auth = await useAuth();
|
||||||
const tags = useTags();
|
const tags = useTags();
|
||||||
|
const flags = useFeatureFlags();
|
||||||
|
|
||||||
const path = route.name.split("-");
|
const path = route.name.split("-");
|
||||||
const mode = path[path.length - 1];
|
const mode = path[path.length - 1];
|
||||||
@@ -896,6 +897,7 @@ export default defineNuxtComponent({
|
|||||||
return {
|
return {
|
||||||
auth,
|
auth,
|
||||||
tags,
|
tags,
|
||||||
|
flags,
|
||||||
fileTypes: ref(fileTypes),
|
fileTypes: ref(fileTypes),
|
||||||
oldFileTypes: ref(oldFileTypes),
|
oldFileTypes: ref(oldFileTypes),
|
||||||
isCreating: ref(isCreating),
|
isCreating: ref(isCreating),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="normal-page__content experimental-styles-within overflow-visible">
|
<section class="experimental-styles-within overflow-visible">
|
||||||
<div
|
<div
|
||||||
v-if="currentMember && isPermission(currentMember?.permissions, 1 << 0)"
|
v-if="currentMember && isPermission(currentMember?.permissions, 1 << 0)"
|
||||||
class="card flex items-center gap-4"
|
class="card flex items-center gap-4"
|
||||||
@@ -19,6 +19,20 @@
|
|||||||
</span>
|
</span>
|
||||||
<DropArea :accept="acceptFileFromProjectType(project.project_type)" @change="handleFiles" />
|
<DropArea :accept="acceptFileFromProjectType(project.project_type)" @change="handleFiles" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3 flex flex-wrap gap-2">
|
||||||
|
<VersionFilterControl
|
||||||
|
ref="versionFilters"
|
||||||
|
:versions="props.versions"
|
||||||
|
@switch-page="switchPage"
|
||||||
|
/>
|
||||||
|
<Pagination
|
||||||
|
:page="currentPage"
|
||||||
|
class="ml-auto mt-auto"
|
||||||
|
:count="Math.ceil(filteredVersions.length / 20)"
|
||||||
|
:link-function="(page) => `?page=${currentPage}`"
|
||||||
|
@switch-page="switchPage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="versions.length > 0"
|
v-if="versions.length > 0"
|
||||||
class="flex flex-col gap-4 rounded-2xl bg-bg-raised px-6 pb-8 pt-4 supports-[grid-template-columns:subgrid]:grid supports-[grid-template-columns:subgrid]:grid-cols-[1fr_min-content] sm:px-8 supports-[grid-template-columns:subgrid]:sm:grid-cols-[min-content_auto_auto_auto_min-content] supports-[grid-template-columns:subgrid]:xl:grid-cols-[min-content_auto_auto_auto_auto_auto_min-content]"
|
class="flex flex-col gap-4 rounded-2xl bg-bg-raised px-6 pb-8 pt-4 supports-[grid-template-columns:subgrid]:grid supports-[grid-template-columns:subgrid]:grid-cols-[1fr_min-content] sm:px-8 supports-[grid-template-columns:subgrid]:sm:grid-cols-[min-content_auto_auto_auto_min-content] supports-[grid-template-columns:subgrid]:xl:grid-cols-[min-content_auto_auto_auto_auto_auto_min-content]"
|
||||||
@@ -69,8 +83,12 @@
|
|||||||
<div class="flex flex-col justify-center gap-2 sm:contents">
|
<div class="flex flex-col justify-center gap-2 sm:contents">
|
||||||
<div class="flex flex-row items-center gap-2 sm:contents">
|
<div class="flex flex-row items-center gap-2 sm:contents">
|
||||||
<div class="self-center">
|
<div class="self-center">
|
||||||
<div class="pointer-events-none relative z-[1]">
|
<div class="relative z-[1] cursor-pointer">
|
||||||
<VersionChannelIndicator :channel="version.version_type" />
|
<VersionChannelIndicator
|
||||||
|
v-tooltip="`Toggle filter for ${version.version_type}`"
|
||||||
|
:channel="version.version_type"
|
||||||
|
@click="versionFilters.toggleFilter('channel', version.version_type)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -115,7 +133,13 @@
|
|||||||
class="flex flex-col justify-center gap-1 max-sm:flex-row max-sm:justify-start max-sm:gap-3 xl:contents"
|
class="flex flex-col justify-center gap-1 max-sm:flex-row max-sm:justify-start max-sm:gap-3 xl:contents"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="pointer-events-none z-[1] flex items-center gap-1 text-nowrap font-medium xl:self-center"
|
v-tooltip="
|
||||||
|
formatMessage(commonMessages.dateAtTimeTooltip, {
|
||||||
|
date: new Date(version.date_published),
|
||||||
|
time: new Date(version.date_published),
|
||||||
|
})
|
||||||
|
"
|
||||||
|
class="z-[1] flex cursor-help items-center gap-1 text-nowrap font-medium xl:self-center"
|
||||||
>
|
>
|
||||||
<CalendarIcon class="xl:hidden" />
|
<CalendarIcon class="xl:hidden" />
|
||||||
{{ formatRelativeTime(version.date_published) }}
|
{{ formatRelativeTime(version.date_published) }}
|
||||||
@@ -232,7 +256,10 @@
|
|||||||
</OverflowMenu>
|
</OverflowMenu>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showFiles" class="tag-list pointer-events-none relative z-[1] col-span-full">
|
<div
|
||||||
|
v-if="flags.showVersionFilesInTable"
|
||||||
|
class="tag-list pointer-events-none relative z-[1] col-span-full"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(file, fileIdx) in version.files"
|
v-for="(file, fileIdx) in version.files"
|
||||||
:key="`platform-tag-${fileIdx}`"
|
:key="`platform-tag-${fileIdx}`"
|
||||||
@@ -254,19 +281,6 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div class="normal-page__sidebar">
|
|
||||||
<AdPlaceholder
|
|
||||||
v-if="
|
|
||||||
(!auth.user || !isPermission(auth.user.badges, 1 << 0)) &&
|
|
||||||
tags.approvedStatuses.includes(project.status)
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<VersionFilterControl
|
|
||||||
ref="versionFilters"
|
|
||||||
:versions="props.versions"
|
|
||||||
@switch-page="switchPage"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -296,9 +310,9 @@ import { formatVersionsForDisplay } from "~/helpers/projects.js";
|
|||||||
import VersionFilterControl from "~/components/ui/VersionFilterControl.vue";
|
import VersionFilterControl from "~/components/ui/VersionFilterControl.vue";
|
||||||
import DropArea from "~/components/ui/DropArea.vue";
|
import DropArea from "~/components/ui/DropArea.vue";
|
||||||
import { acceptFileFromProjectType } from "~/helpers/fileUtils.js";
|
import { acceptFileFromProjectType } from "~/helpers/fileUtils.js";
|
||||||
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
|
||||||
|
|
||||||
const formatCompactNumber = useCompactNumber();
|
const formatCompactNumber = useCompactNumber();
|
||||||
|
const { formatMessage } = useVIntl();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
project: {
|
project: {
|
||||||
@@ -321,8 +335,8 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const auth = await useAuth();
|
|
||||||
const tags = useTags();
|
const tags = useTags();
|
||||||
|
const flags = useFeatureFlags();
|
||||||
const formatRelativeTime = useRelativeTime();
|
const formatRelativeTime = useRelativeTime();
|
||||||
|
|
||||||
const emits = defineEmits(["onDownload"]);
|
const emits = defineEmits(["onDownload"]);
|
||||||
@@ -332,8 +346,6 @@ const router = useNativeRouter();
|
|||||||
|
|
||||||
const currentPage = ref(route.query.page ?? 1);
|
const currentPage = ref(route.query.page ?? 1);
|
||||||
|
|
||||||
const showFiles = ref(false);
|
|
||||||
|
|
||||||
function switchPage(page) {
|
function switchPage(page) {
|
||||||
currentPage.value = page;
|
currentPage.value = page;
|
||||||
|
|
||||||
@@ -349,22 +361,30 @@ function getPrimaryFile(version) {
|
|||||||
return version.files.find((x) => x.primary) || version.files[0];
|
return version.files.find((x) => x.primary) || version.files[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectedGameVersions = computed(() => {
|
||||||
|
return getArrayOrString(route.query.g) ?? [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedPlatforms = computed(() => {
|
||||||
|
return getArrayOrString(route.query.l) ?? [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedVersionChannels = computed(() => {
|
||||||
|
return getArrayOrString(route.query.c) ?? [];
|
||||||
|
});
|
||||||
|
|
||||||
const versionFilters = ref(null);
|
const versionFilters = ref(null);
|
||||||
const filteredVersions = computed(() => {
|
const filteredVersions = computed(() => {
|
||||||
const selectedGameVersions = getArrayOrString(route.query.gameVersion) ?? [];
|
|
||||||
const selectedLoaders = getArrayOrString(route.query.platform) ?? [];
|
|
||||||
const selectedVersionTypes = getArrayOrString(route.query.type) ?? [];
|
|
||||||
|
|
||||||
return props.versions.filter(
|
return props.versions.filter(
|
||||||
(projectVersion) =>
|
(projectVersion) =>
|
||||||
(selectedGameVersions.length === 0 ||
|
(selectedGameVersions.value.length === 0 ||
|
||||||
selectedGameVersions.some((gameVersion) =>
|
selectedGameVersions.value.some((gameVersion) =>
|
||||||
projectVersion.game_versions.includes(gameVersion),
|
projectVersion.game_versions.includes(gameVersion),
|
||||||
)) &&
|
)) &&
|
||||||
(selectedLoaders.length === 0 ||
|
(selectedPlatforms.value.length === 0 ||
|
||||||
selectedLoaders.some((loader) => projectVersion.loaders.includes(loader))) &&
|
selectedPlatforms.value.some((loader) => projectVersion.loaders.includes(loader))) &&
|
||||||
(selectedVersionTypes.length === 0 ||
|
(selectedVersionChannels.value.length === 0 ||
|
||||||
selectedVersionTypes.includes(projectVersion.version_type)),
|
selectedVersionChannels.value.includes(projectVersion.version_type)),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -248,7 +248,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<AdPlaceholder v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0)" />
|
<AdPlaceholder
|
||||||
|
v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="normal-page__content">
|
<div class="normal-page__content">
|
||||||
<nav class="navigation-card">
|
<nav class="navigation-card">
|
||||||
@@ -480,6 +482,7 @@ const route = useNativeRoute();
|
|||||||
const auth = await useAuth();
|
const auth = await useAuth();
|
||||||
const cosmetics = useCosmetics();
|
const cosmetics = useCosmetics();
|
||||||
const tags = useTags();
|
const tags = useTags();
|
||||||
|
const flags = useFeatureFlags();
|
||||||
|
|
||||||
const isEditing = ref(false);
|
const isEditing = ref(false);
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AdPlaceholder v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0)" />
|
<AdPlaceholder
|
||||||
|
v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="creator-list universal-card">
|
<div class="creator-list universal-card">
|
||||||
<div class="title-and-link">
|
<div class="title-and-link">
|
||||||
@@ -253,6 +255,7 @@ const user = await useUser();
|
|||||||
const cosmetics = useCosmetics();
|
const cosmetics = useCosmetics();
|
||||||
const route = useNativeRoute();
|
const route = useNativeRoute();
|
||||||
const tags = useTags();
|
const tags = useTags();
|
||||||
|
const flags = useFeatureFlags();
|
||||||
|
|
||||||
let orgId = useRouteId();
|
let orgId = useRouteId();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="new-page sidebar experimental-styles-within"
|
class="new-page sidebar experimental-styles-within"
|
||||||
:class="{ 'alt-layout': cosmetics.searchLayout }"
|
:class="{ 'alt-layout': !cosmetics.rightSearchLayout }"
|
||||||
>
|
>
|
||||||
<Head>
|
<Head>
|
||||||
<Title>Search {{ projectType.display }}s - Modrinth</Title>
|
<Title>Search {{ projectType.display }}s - Modrinth</Title>
|
||||||
@@ -12,7 +12,9 @@
|
|||||||
}"
|
}"
|
||||||
aria-label="Filters"
|
aria-label="Filters"
|
||||||
>
|
>
|
||||||
<AdPlaceholder v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0)" />
|
<AdPlaceholder
|
||||||
|
v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus"
|
||||||
|
/>
|
||||||
<section class="card gap-1" :class="{ 'max-lg:!hidden': !sidebarMenuOpen }">
|
<section class="card gap-1" :class="{ 'max-lg:!hidden': !sidebarMenuOpen }">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="iconified-input w-full">
|
<div class="iconified-input w-full">
|
||||||
@@ -202,6 +204,14 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<pagination
|
||||||
|
v-if="false"
|
||||||
|
:page="currentPage"
|
||||||
|
:count="pageCount"
|
||||||
|
:link-function="(x) => getSearchUrl(x <= 1 ? 0 : (x - 1) * maxResults)"
|
||||||
|
class="mb-3 justify-end"
|
||||||
|
@switch-page="onSearchChangeToTop"
|
||||||
|
/>
|
||||||
<LogoAnimated v-if="searchLoading && !noLoad" />
|
<LogoAnimated v-if="searchLoading && !noLoad" />
|
||||||
<div v-else-if="results && results.hits && results.hits.length === 0" class="no-results">
|
<div v-else-if="results && results.hits && results.hits.length === 0" class="no-results">
|
||||||
<p>No results found for your query!</p>
|
<p>No results found for your query!</p>
|
||||||
@@ -279,6 +289,7 @@ const route = useNativeRoute();
|
|||||||
|
|
||||||
const cosmetics = useCosmetics();
|
const cosmetics = useCosmetics();
|
||||||
const tags = useTags();
|
const tags = useTags();
|
||||||
|
const flags = useFeatureFlags();
|
||||||
const auth = await useAuth();
|
const auth = await useAuth();
|
||||||
|
|
||||||
const query = ref("");
|
const query = ref("");
|
||||||
|
|||||||
@@ -190,15 +190,15 @@
|
|||||||
<div class="adjacent-input small">
|
<div class="adjacent-input small">
|
||||||
<label for="search-layout-toggle">
|
<label for="search-layout-toggle">
|
||||||
<span class="label__title">
|
<span class="label__title">
|
||||||
{{ formatMessage(toggleFeatures.leftAlignedSearchSidebarTitle) }}
|
{{ formatMessage(toggleFeatures.rightAlignedFiltersSidebarTitle) }}
|
||||||
</span>
|
</span>
|
||||||
<span class="label__description">
|
<span class="label__description">
|
||||||
{{ formatMessage(toggleFeatures.leftAlignedSearchSidebarDescription) }}
|
{{ formatMessage(toggleFeatures.rightAlignedFiltersSidebarDescription) }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="search-layout-toggle"
|
id="search-layout-toggle"
|
||||||
v-model="cosmetics.searchLayout"
|
v-model="cosmetics.rightSearchLayout"
|
||||||
class="switch stylized-toggle"
|
class="switch stylized-toggle"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
/>
|
/>
|
||||||
@@ -206,15 +206,15 @@
|
|||||||
<div class="adjacent-input small">
|
<div class="adjacent-input small">
|
||||||
<label for="project-layout-toggle">
|
<label for="project-layout-toggle">
|
||||||
<span class="label__title">
|
<span class="label__title">
|
||||||
{{ formatMessage(toggleFeatures.leftAlignedProjectSidebarTitle) }}
|
{{ formatMessage(toggleFeatures.leftAlignedContentSidebarTitle) }}
|
||||||
</span>
|
</span>
|
||||||
<span class="label__description">
|
<span class="label__description">
|
||||||
{{ formatMessage(toggleFeatures.leftAlignedProjectSidebarDescription) }}
|
{{ formatMessage(toggleFeatures.leftAlignedContentSidebarDescription) }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="project-layout-toggle"
|
id="project-layout-toggle"
|
||||||
v-model="cosmetics.projectLayout"
|
v-model="cosmetics.leftContentLayout"
|
||||||
class="switch stylized-toggle"
|
class="switch stylized-toggle"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
/>
|
/>
|
||||||
@@ -368,21 +368,21 @@ const toggleFeatures = defineMessages({
|
|||||||
defaultMessage:
|
defaultMessage:
|
||||||
'Hides the "Get Modrinth App" buttons from primary navigation. The Modrinth App page can still be found on the landing page or in the footer.',
|
'Hides the "Get Modrinth App" buttons from primary navigation. The Modrinth App page can still be found on the landing page or in the footer.',
|
||||||
},
|
},
|
||||||
leftAlignedSearchSidebarTitle: {
|
rightAlignedFiltersSidebarTitle: {
|
||||||
id: "settings.display.sidebar.Left-aligned-search-sidebar.title",
|
id: "settings.display.sidebar.right-aligned-filters-sidebar.title",
|
||||||
defaultMessage: "Left-aligned search sidebar",
|
defaultMessage: "Right-aligned filters sidebar on search pages",
|
||||||
},
|
},
|
||||||
leftAlignedSearchSidebarDescription: {
|
rightAlignedFiltersSidebarDescription: {
|
||||||
id: "settings.display.sidebar.left-aligned-search-sidebar.description",
|
id: "settings.display.sidebar.right-aligned-filters-sidebar.description",
|
||||||
defaultMessage: "Aligns the search filters sidebar to the left of the search results.",
|
defaultMessage: "Aligns the filters sidebar to the right of the search results.",
|
||||||
},
|
},
|
||||||
leftAlignedProjectSidebarTitle: {
|
leftAlignedContentSidebarTitle: {
|
||||||
id: "settings.display.sidebar.left-aligned-project-sidebar.title",
|
id: "settings.display.sidebar.left-aligned-content-sidebar.title",
|
||||||
defaultMessage: "Left-aligned project sidebar",
|
defaultMessage: "Left-aligned sidebar on content pages",
|
||||||
},
|
},
|
||||||
leftAlignedProjectSidebarDescription: {
|
leftAlignedContentSidebarDescription: {
|
||||||
id: "settings.display.sidebar.left-aligned-project-sidebar.description",
|
id: "settings.display.sidebar.right-aligned-content-sidebar.description",
|
||||||
defaultMessage: "Aligns the project details sidebar to the left of the page's content.",
|
defaultMessage: "Aligns the sidebar to the left of the page's content.",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div v-if="user" class="experimental-styles-within">
|
<div v-if="user" class="experimental-styles-within">
|
||||||
<ModalCreation ref="modal_creation" />
|
<ModalCreation ref="modal_creation" />
|
||||||
<CollectionCreateModal ref="modal_collection_creation" />
|
<CollectionCreateModal ref="modal_collection_creation" />
|
||||||
<div class="new-page sidebar">
|
<div class="new-page sidebar" :class="{ 'alt-layout': cosmetics.leftContentLayout }">
|
||||||
<div class="normal-page__header pt-4">
|
<div class="normal-page__header pt-4">
|
||||||
<div
|
<div
|
||||||
class="mb-4 grid grid-cols-1 gap-x-8 gap-y-6 border-0 border-b border-solid border-button-bg pb-6 lg:grid-cols-[1fr_auto]"
|
class="mb-4 grid grid-cols-1 gap-x-8 gap-y-6 border-0 border-b border-solid border-button-bg pb-6 lg:grid-cols-[1fr_auto]"
|
||||||
@@ -72,11 +72,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="normal-page__content">
|
||||||
<div v-if="navLinks.length > 2" class="mb-4 max-w-full overflow-x-auto">
|
<div v-if="navLinks.length > 2" class="mb-4 max-w-full overflow-x-auto">
|
||||||
<NavTabs :links="navLinks" />
|
<NavTabs :links="navLinks" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="normal-page__content">
|
|
||||||
<div v-if="projects.length > 0">
|
<div v-if="projects.length > 0">
|
||||||
<div
|
<div
|
||||||
v-if="route.params.projectType !== 'collections'"
|
v-if="route.params.projectType !== 'collections'"
|
||||||
@@ -194,7 +194,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="normal-page__sidebar">
|
<div class="normal-page__sidebar">
|
||||||
<AdPlaceholder v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0)" />
|
|
||||||
<div class="card flex-card">
|
<div class="card flex-card">
|
||||||
<h2 class="text-lg text-contrast">{{ formatMessage(messages.profileDetails) }}</h2>
|
<h2 class="text-lg text-contrast">{{ formatMessage(messages.profileDetails) }}</h2>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -258,6 +257,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<AdPlaceholder
|
||||||
|
v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus"
|
||||||
|
/>
|
||||||
<div v-if="organizations.length > 0" class="card flex-card">
|
<div v-if="organizations.length > 0" class="card flex-card">
|
||||||
<h2 class="text-lg text-contrast">{{ formatMessage(messages.profileOrganizations) }}</h2>
|
<h2 class="text-lg text-contrast">{{ formatMessage(messages.profileOrganizations) }}</h2>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
@@ -328,6 +330,7 @@ const route = useNativeRoute();
|
|||||||
const auth = await useAuth();
|
const auth = await useAuth();
|
||||||
const cosmetics = useCosmetics();
|
const cosmetics = useCosmetics();
|
||||||
const tags = useTags();
|
const tags = useTags();
|
||||||
|
const flags = useFeatureFlags();
|
||||||
|
|
||||||
const vintl = useVIntl();
|
const vintl = useVIntl();
|
||||||
const { formatMessage } = vintl;
|
const { formatMessage } = vintl;
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ export type DisplayLocation =
|
|||||||
| "collection";
|
| "collection";
|
||||||
|
|
||||||
export interface Cosmetics {
|
export interface Cosmetics {
|
||||||
searchLayout: boolean;
|
rightSearchLayout: boolean;
|
||||||
projectLayout: boolean;
|
leftContentLayout: boolean;
|
||||||
advancedRendering: boolean;
|
advancedRendering: boolean;
|
||||||
externalLinksNewTab: boolean;
|
externalLinksNewTab: boolean;
|
||||||
notUsingBlockers: boolean;
|
notUsingBlockers: boolean;
|
||||||
@@ -34,8 +34,8 @@ export default defineNuxtPlugin({
|
|||||||
httpOnly: false,
|
httpOnly: false,
|
||||||
path: "/",
|
path: "/",
|
||||||
default: () => ({
|
default: () => ({
|
||||||
searchLayout: false,
|
rightSearchLayout: false,
|
||||||
projectLayout: false,
|
leftContentLayout: false,
|
||||||
advancedRendering: true,
|
advancedRendering: true,
|
||||||
externalLinksNewTab: true,
|
externalLinksNewTab: true,
|
||||||
notUsingBlockers: false,
|
notUsingBlockers: false,
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ module.exports = {
|
|||||||
heading: "var(--color-heading)",
|
heading: "var(--color-heading)",
|
||||||
red: "var(--color-red)",
|
red: "var(--color-red)",
|
||||||
orange: "var(--color-orange)",
|
orange: "var(--color-orange)",
|
||||||
|
green: "var(--color-green)",
|
||||||
|
blue: "var(--color-blue)",
|
||||||
purple: "var(--color-purple)",
|
purple: "var(--color-purple)",
|
||||||
bg: {
|
bg: {
|
||||||
DEFAULT: "var(--color-bg)",
|
DEFAULT: "var(--color-bg)",
|
||||||
|
|||||||
116
packages/ui/src/components/base/ManySelect.vue
Normal file
116
packages/ui/src/components/base/ManySelect.vue
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<template>
|
||||||
|
<ButtonStyled>
|
||||||
|
<PopoutMenu
|
||||||
|
v-if="options.length > 1"
|
||||||
|
v-bind="$attrs"
|
||||||
|
:disabled="disabled"
|
||||||
|
:position="position"
|
||||||
|
:direction="direction"
|
||||||
|
@open="() => {
|
||||||
|
searchQuery = ''
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
<DropdownIcon class="h-5 w-5 text-secondary" />
|
||||||
|
<template #menu>
|
||||||
|
<div class="iconified-input mb-2 w-full" v-if="search">
|
||||||
|
<label for="search-input" hidden>Search...</label>
|
||||||
|
<SearchIcon aria-hidden="true" />
|
||||||
|
<input id="search-input" v-model="searchQuery" placeholder="Search..." type="text" ref="searchInput" @keydown.enter="() => {
|
||||||
|
toggleOption(filteredOptions[0])
|
||||||
|
}" />
|
||||||
|
</div>
|
||||||
|
<ScrollablePanel
|
||||||
|
v-if="search" class="h-[17rem]">
|
||||||
|
<Button
|
||||||
|
v-for="(option, index) in filteredOptions"
|
||||||
|
:key="`option-${index}`"
|
||||||
|
:transparent="!manyValues.includes(option)"
|
||||||
|
:action="() => toggleOption(option)"
|
||||||
|
class="!w-full"
|
||||||
|
:color="manyValues.includes(option) ? 'secondary' : 'default'"
|
||||||
|
>
|
||||||
|
<slot name="option" :option="option">{{ displayName(option) }}</slot>
|
||||||
|
<CheckIcon class="h-5 w-5 text-contrast ml-auto transition-opacity" :class="{ 'opacity-0': !manyValues.includes(option) }" />
|
||||||
|
</Button>
|
||||||
|
</ScrollablePanel>
|
||||||
|
<div
|
||||||
|
v-else class="flex flex-col gap-1">
|
||||||
|
<Button
|
||||||
|
v-for="(option, index) in filteredOptions"
|
||||||
|
:key="`option-${index}`"
|
||||||
|
:transparent="!manyValues.includes(option)"
|
||||||
|
:action="() => toggleOption(option)"
|
||||||
|
class="!w-full"
|
||||||
|
:color="manyValues.includes(option) ? 'secondary' : 'default'"
|
||||||
|
>
|
||||||
|
<slot name="option" :option="option">{{ displayName(option) }}</slot>
|
||||||
|
<CheckIcon class="h-5 w-5 text-contrast ml-auto transition-opacity" :class="{ 'opacity-0': !manyValues.includes(option) }" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<slot name="footer" />
|
||||||
|
</template>
|
||||||
|
</PopoutMenu>
|
||||||
|
</ButtonStyled>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { CheckIcon, DropdownIcon, SearchIcon, XIcon } from '@modrinth/assets'
|
||||||
|
import { ButtonStyled, PopoutMenu, Button } from '../index'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import ScrollablePanel from './ScrollablePanel.vue'
|
||||||
|
|
||||||
|
type Option = string | number | object
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modelValue: Option[],
|
||||||
|
options: Option[]
|
||||||
|
disabled?: boolean
|
||||||
|
position?: string
|
||||||
|
direction?: string,
|
||||||
|
displayName?: (option: Option) => string,
|
||||||
|
search?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
disabled: false,
|
||||||
|
position: 'auto',
|
||||||
|
direction: 'auto',
|
||||||
|
displayName: (option: Option) => option as string,
|
||||||
|
search: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'change']);
|
||||||
|
const selectedValues = ref(props.modelValue || [])
|
||||||
|
const searchInput = ref();
|
||||||
|
|
||||||
|
const searchQuery = ref('')
|
||||||
|
|
||||||
|
const manyValues = computed({
|
||||||
|
get() {
|
||||||
|
return props.modelValue || selectedValues.value
|
||||||
|
},
|
||||||
|
set(newValue) {
|
||||||
|
emit('update:modelValue', newValue)
|
||||||
|
emit('change', newValue)
|
||||||
|
selectedValues.value = newValue
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredOptions = computed(() => {
|
||||||
|
return props.options.filter((x) => !searchQuery.value || props.displayName(x).toLowerCase().includes(searchQuery.value.toLowerCase()))
|
||||||
|
})
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
function toggleOption(id: Option) {
|
||||||
|
if (manyValues.value.includes(id)) {
|
||||||
|
manyValues.value = manyValues.value.filter((x) => x !== id)
|
||||||
|
} else {
|
||||||
|
manyValues.value = [...manyValues.value, id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
ref="dropdownButton"
|
ref="dropdownButton"
|
||||||
:class="{ 'popout-open': dropdownVisible }"
|
:class="{ 'popout-open': dropdownVisible }"
|
||||||
tabindex="-1"
|
:tabindex="tabInto ? -1 : 0"
|
||||||
@click="toggleDropdown"
|
@click="toggleDropdown"
|
||||||
>
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
<div
|
<div
|
||||||
class="popup-menu"
|
class="popup-menu"
|
||||||
:class="`position-${computedPosition}-${computedDirection} ${dropdownVisible ? 'visible' : ''}`"
|
:class="`position-${computedPosition}-${computedDirection} ${dropdownVisible ? 'visible' : ''}`"
|
||||||
|
:inert="!tabInto && !dropdownVisible"
|
||||||
>
|
>
|
||||||
<slot name="menu"> </slot>
|
<slot name="menu"> </slot>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,11 +35,17 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'auto',
|
default: 'auto',
|
||||||
},
|
},
|
||||||
|
tabInto: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
defineOptions({
|
defineOptions({
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['open', 'close'])
|
||||||
|
|
||||||
const dropdownVisible = ref(false)
|
const dropdownVisible = ref(false)
|
||||||
const dropdown = ref(null)
|
const dropdown = ref(null)
|
||||||
const dropdownButton = ref(null)
|
const dropdownButton = ref(null)
|
||||||
@@ -71,8 +78,11 @@ function updateDirection() {
|
|||||||
const toggleDropdown = () => {
|
const toggleDropdown = () => {
|
||||||
if (!props.disabled) {
|
if (!props.disabled) {
|
||||||
dropdownVisible.value = !dropdownVisible.value
|
dropdownVisible.value = !dropdownVisible.value
|
||||||
if (!dropdownVisible.value) {
|
if (dropdownVisible.value) {
|
||||||
|
emit('open')
|
||||||
|
} else {
|
||||||
dropdownButton.value.focus()
|
dropdownButton.value.focus()
|
||||||
|
emit('close')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,10 +90,12 @@ const toggleDropdown = () => {
|
|||||||
const hide = () => {
|
const hide = () => {
|
||||||
dropdownVisible.value = false
|
dropdownVisible.value = false
|
||||||
dropdownButton.value.focus()
|
dropdownButton.value.focus()
|
||||||
|
emit('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
const show = () => {
|
const show = () => {
|
||||||
dropdownVisible.value = true
|
dropdownVisible.value = true
|
||||||
|
emit('open')
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
@@ -99,6 +111,7 @@ const handleClickOutside = (event) => {
|
|||||||
!dropdown.value.contains(event.target)
|
!dropdown.value.contains(event.target)
|
||||||
) {
|
) {
|
||||||
dropdownVisible.value = false
|
dropdownVisible.value = false
|
||||||
|
emit('close')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +119,7 @@ onMounted(() => {
|
|||||||
window.addEventListener('click', handleClickOutside)
|
window.addEventListener('click', handleClickOutside)
|
||||||
window.addEventListener('resize', updateDirection)
|
window.addEventListener('resize', updateDirection)
|
||||||
window.addEventListener('scroll', updateDirection)
|
window.addEventListener('scroll', updateDirection)
|
||||||
|
window.addEventListener('keydown', handleKeyDown)
|
||||||
updateDirection()
|
updateDirection()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -113,7 +127,14 @@ onBeforeUnmount(() => {
|
|||||||
window.removeEventListener('click', handleClickOutside)
|
window.removeEventListener('click', handleClickOutside)
|
||||||
window.removeEventListener('resize', updateDirection)
|
window.removeEventListener('resize', updateDirection)
|
||||||
window.removeEventListener('scroll', updateDirection)
|
window.removeEventListener('scroll', updateDirection)
|
||||||
|
window.removeEventListener('keydown', handleKeyDown)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function handleKeyDown(event) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ function onScroll({ target: { scrollTop, offsetHeight, scrollHeight } }) {
|
|||||||
.scrollable-pane {
|
.scrollable-pane {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.25rem;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export { default as DropArea } from './base/DropArea.vue'
|
|||||||
export { default as DropdownSelect } from './base/DropdownSelect.vue'
|
export { default as DropdownSelect } from './base/DropdownSelect.vue'
|
||||||
export { default as EnvironmentIndicator } from './base/EnvironmentIndicator.vue'
|
export { default as EnvironmentIndicator } from './base/EnvironmentIndicator.vue'
|
||||||
export { default as FileInput } from './base/FileInput.vue'
|
export { default as FileInput } from './base/FileInput.vue'
|
||||||
|
export { default as ManySelect } from './base/ManySelect.vue'
|
||||||
export { default as MarkdownEditor } from './base/MarkdownEditor.vue'
|
export { default as MarkdownEditor } from './base/MarkdownEditor.vue'
|
||||||
export { default as Notifications } from './base/Notifications.vue'
|
export { default as Notifications } from './base/Notifications.vue'
|
||||||
export { default as OverflowMenu } from './base/OverflowMenu.vue'
|
export { default as OverflowMenu } from './base/OverflowMenu.vue'
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ const messages = defineMessages({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
:style="`--_size: ${size};`"
|
|
||||||
:class="`flex ${large ? 'text-lg w-[2.625rem] h-[2.625rem]' : 'text-sm w-9 h-9'} font-bold justify-center items-center rounded-full ${channel === 'release' ? 'bg-bg-green text-brand-green' : channel === 'beta' ? 'bg-bg-orange text-brand-orange' : 'bg-bg-red text-brand-red'}`"
|
:class="`flex ${large ? 'text-lg w-[2.625rem] h-[2.625rem]' : 'text-sm w-9 h-9'} font-bold justify-center items-center rounded-full ${channel === 'release' ? 'bg-bg-green text-brand-green' : channel === 'beta' ? 'bg-bg-orange text-brand-orange' : 'bg-bg-red text-brand-red'}`"
|
||||||
>
|
>
|
||||||
{{ channel ? formatMessage(messages[`${channel}Symbol`]) : '?' }}
|
{{ channel ? formatMessage(messages[`${channel}Symbol`]) : '?' }}
|
||||||
|
|||||||
Reference in New Issue
Block a user