You've already forked AstralRinth
forked from didirus/AstralRinth
Merge commit '74cf3f076eff43755bb4bef62f1c1bb3fc0e6c2a' into feature-clean
This commit is contained in:
@@ -638,6 +638,7 @@
|
||||
shown: !isMember,
|
||||
},
|
||||
{ id: 'copy-id', action: () => copyId() },
|
||||
{ id: 'copy-permalink', action: () => copyPermalink() },
|
||||
]"
|
||||
aria-label="More options"
|
||||
:dropdown-id="`${baseId}-more-options`"
|
||||
@@ -659,6 +660,10 @@
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
Copy ID
|
||||
</template>
|
||||
<template #copy-permalink>
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
Copy permanent link
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
@@ -866,6 +871,7 @@ import {
|
||||
ProjectSidebarDetails,
|
||||
ProjectSidebarLinks,
|
||||
ScrollablePanel,
|
||||
useRelativeTime,
|
||||
} from "@modrinth/ui";
|
||||
import VersionSummary from "@modrinth/ui/src/components/version/VersionSummary.vue";
|
||||
import { formatCategory, isRejected, isStaff, isUnderReview, renderString } from "@modrinth/utils";
|
||||
@@ -888,6 +894,7 @@ import { reportProject } from "~/utils/report-helpers.ts";
|
||||
|
||||
const data = useNuxtApp();
|
||||
const route = useNativeRoute();
|
||||
const config = useRuntimeConfig();
|
||||
|
||||
const auth = await useAuth();
|
||||
const user = await useUser();
|
||||
@@ -1458,6 +1465,10 @@ async function copyId() {
|
||||
await navigator.clipboard.writeText(project.value.id);
|
||||
}
|
||||
|
||||
async function copyPermalink() {
|
||||
await navigator.clipboard.writeText(`${config.public.siteUrl}/project/${project.value.id}`);
|
||||
}
|
||||
|
||||
const collapsedChecklist = ref(false);
|
||||
|
||||
const showModerationChecklist = ref(false);
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
This is a private conversation thread with the Modrinth moderators. They may message you
|
||||
with issues concerning this project. This thread is only checked when you submit your
|
||||
project for review. For additional inquiries, contact
|
||||
<a href="https://support.modrinth.com">Modrinth support</a>.
|
||||
<a href="https://support.modrinth.com">Modrinth Support</a>.
|
||||
</p>
|
||||
<ConversationThread
|
||||
v-if="thread"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<ModalConfirm
|
||||
<ConfirmModal
|
||||
ref="modal_confirm"
|
||||
title="Are you sure you want to delete this project?"
|
||||
description="If you proceed, all versions and any attached data will be removed from our servers. This may break other projects, so be careful."
|
||||
@@ -242,8 +242,8 @@
|
||||
import { formatProjectStatus } from "@modrinth/utils";
|
||||
import { UploadIcon, SaveIcon, TrashIcon, XIcon, IssuesIcon, CheckIcon } from "@modrinth/assets";
|
||||
import { Multiselect } from "vue-multiselect";
|
||||
import { ConfirmModal } from "@modrinth/ui";
|
||||
import Avatar from "~/components/ui/Avatar.vue";
|
||||
import ModalConfirm from "~/components/ui/ModalConfirm.vue";
|
||||
import FileInput from "~/components/ui/FileInput.vue";
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<ModalConfirm
|
||||
<ConfirmModal
|
||||
ref="modal_remove"
|
||||
title="Are you sure you want to remove this project from the organization?"
|
||||
description="If you proceed, this project will no longer be managed by the organization."
|
||||
@@ -530,8 +530,7 @@ import {
|
||||
OrganizationIcon,
|
||||
CrownIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { Avatar, Badge, Card, Checkbox } from "@modrinth/ui";
|
||||
import ModalConfirm from "~/components/ui/ModalConfirm.vue";
|
||||
import { Avatar, Badge, Card, Checkbox, ConfirmModal } from "@modrinth/ui";
|
||||
import { removeSelfFromTeam } from "~/helpers/teams.js";
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@@ -541,7 +541,6 @@
|
||||
:close-on-select="false"
|
||||
:clear-on-select="false"
|
||||
:show-labels="false"
|
||||
:limit="6"
|
||||
:hide-selected="true"
|
||||
placeholder="Choose loaders..."
|
||||
/>
|
||||
@@ -566,7 +565,6 @@
|
||||
:close-on-select="false"
|
||||
:clear-on-select="false"
|
||||
:show-labels="false"
|
||||
:limit="6"
|
||||
:hide-selected="true"
|
||||
:custom-label="(version) => version"
|
||||
placeholder="Choose versions..."
|
||||
@@ -622,7 +620,7 @@
|
||||
<CopyCode :text="version.id" />
|
||||
</div>
|
||||
<div v-if="!isEditing && flags.developerMode">
|
||||
<h4>Modrinth Maven</h4>
|
||||
<h4>Maven coordinates</h4>
|
||||
<div class="maven-section">
|
||||
<CopyCode :text="`maven.modrinth:${project.id}:${version.id}`" />
|
||||
</div>
|
||||
@@ -1555,6 +1553,10 @@ export default defineNuxtComponent({
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
button {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.team-member {
|
||||
|
||||
@@ -169,7 +169,7 @@
|
||||
</template>
|
||||
<template #copy-maven>
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
Copy Modrinth Maven
|
||||
Copy Maven coordinates
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
<div class="mb-4 mt-2 flex w-full items-center gap-1 text-sm text-secondary">
|
||||
{{ formatCategory(subscription.interval) }} ⋅ {{ subscription.status }} ⋅
|
||||
{{ dayjs(subscription.created).format("MMMM D, YYYY [at] h:mma") }} ({{
|
||||
dayjs(subscription.created).fromNow()
|
||||
formatRelativeTime(subscription.created)
|
||||
}})
|
||||
</div>
|
||||
</div>
|
||||
@@ -151,7 +151,7 @@
|
||||
</span>
|
||||
<span class="text-sm text-secondary">
|
||||
{{ dayjs(charge.due).format("MMMM D, YYYY [at] h:mma") }}
|
||||
<span class="text-secondary">({{ dayjs(charge.due).fromNow() }}) </span>
|
||||
<span class="text-secondary">({{ formatRelativeTime(charge.due) }}) </span>
|
||||
</span>
|
||||
<div
|
||||
v-if="flags.developerMode"
|
||||
@@ -196,7 +196,15 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Avatar, ButtonStyled, CopyCode, DropdownSelect, NewModal, Toggle } from "@modrinth/ui";
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
CopyCode,
|
||||
DropdownSelect,
|
||||
NewModal,
|
||||
Toggle,
|
||||
useRelativeTime,
|
||||
} from "@modrinth/ui";
|
||||
import { formatCategory, formatPrice } from "@modrinth/utils";
|
||||
import {
|
||||
CheckIcon,
|
||||
@@ -215,7 +223,9 @@ const flags = useFeatureFlags();
|
||||
const route = useRoute();
|
||||
const data = useNuxtApp();
|
||||
const vintl = useVIntl();
|
||||
|
||||
const { formatMessage } = vintl;
|
||||
const formatRelativeTime = useRelativeTime();
|
||||
|
||||
const messages = defineMessages({
|
||||
userNotFoundError: {
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
<div class="text-sm">
|
||||
<span v-if="notice.announce_at">
|
||||
{{ dayjs(notice.announce_at).format("MMM D, YYYY [at] h:mm A") }} ({{
|
||||
dayjs(notice.announce_at).fromNow()
|
||||
formatRelativeTime(notice.announce_at)
|
||||
}})
|
||||
</span>
|
||||
<template v-else> Never begins </template>
|
||||
@@ -166,7 +166,7 @@
|
||||
v-if="notice.expires"
|
||||
v-tooltip="dayjs(notice.expires).format('MMMM D, YYYY [at] h:mm A')"
|
||||
>
|
||||
{{ dayjs(notice.expires).fromNow() }}
|
||||
{{ formatRelativeTime(notice.expires) }}
|
||||
</span>
|
||||
<template v-else> Never expires </template>
|
||||
</div>
|
||||
@@ -267,6 +267,7 @@ import {
|
||||
NewModal,
|
||||
TeleportDropdownMenu,
|
||||
Toggle,
|
||||
useRelativeTime,
|
||||
} from "@modrinth/ui";
|
||||
import { SettingsIcon, PlusIcon, SaveIcon, TrashIcon, EditIcon, XIcon } from "@modrinth/assets";
|
||||
import dayjs from "dayjs";
|
||||
@@ -278,6 +279,8 @@ import { usePyroFetch } from "~/composables/pyroFetch.ts";
|
||||
import AssignNoticeModal from "~/components/ui/servers/notice/AssignNoticeModal.vue";
|
||||
|
||||
const { formatMessage } = useVIntl();
|
||||
const formatRelativeTime = useRelativeTime();
|
||||
|
||||
const app = useNuxtApp() as unknown as { $notify: any };
|
||||
|
||||
const notices = ref<ServerNoticeType[]>([]);
|
||||
|
||||
@@ -19,7 +19,6 @@ import Checkbox from "~/components/ui/Checkbox.vue";
|
||||
import { homePageProjects } from "~/generated/state.json";
|
||||
|
||||
const os = ref(null);
|
||||
const macValue = ref(null);
|
||||
const downloadWindows = ref(null);
|
||||
const downloadLinux = ref(null);
|
||||
const downloadSection = ref(null);
|
||||
@@ -31,8 +30,7 @@ const linuxLinks = {
|
||||
thirdParty: "https://support.modrinth.com/en/articles/9298760",
|
||||
};
|
||||
const macLinks = {
|
||||
appleSilicon: null,
|
||||
intel: null,
|
||||
universal: null,
|
||||
};
|
||||
|
||||
let downloadLauncher;
|
||||
@@ -53,8 +51,7 @@ const [{ data: launcherUpdates }] = await Promise.all([
|
||||
),
|
||||
]);
|
||||
|
||||
macLinks.appleSilicon = launcherUpdates.value.platforms["darwin-aarch64"].install_urls[0];
|
||||
macLinks.intel = launcherUpdates.value.platforms["darwin-x86_64"].install_urls[0];
|
||||
macLinks.universal = launcherUpdates.value.platforms["darwin-aarch64"].install_urls[0];
|
||||
windowsLink.value = launcherUpdates.value.platforms["windows-x86_64"].install_urls[0];
|
||||
linuxLinks.appImage = launcherUpdates.value.platforms["linux-x86_64"].install_urls[1];
|
||||
linuxLinks.deb = launcherUpdates.value.platforms["linux-x86_64"].install_urls[0];
|
||||
@@ -85,24 +82,6 @@ onMounted(() => {
|
||||
}
|
||||
});
|
||||
|
||||
watch(macValue, () => {
|
||||
if (macValue.value === "Download for Apple Silicon") {
|
||||
const link = document.createElement("a");
|
||||
link.href = macLinks.appleSilicon;
|
||||
link.download = "";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
} else if (macValue.value === "Download for Intel") {
|
||||
const link = document.createElement("a");
|
||||
link.href = macLinks.intel;
|
||||
link.download = "";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
});
|
||||
|
||||
const scrollToSection = () => {
|
||||
nextTick(() => {
|
||||
window.scrollTo({
|
||||
@@ -834,13 +813,9 @@ useSeoMeta({
|
||||
Mac
|
||||
</div>
|
||||
<div class="description apple">
|
||||
<a :href="macLinks.appleSilicon" download="">
|
||||
<a :href="macLinks.universal" download="">
|
||||
<DownloadIcon />
|
||||
<span> Download for Apple Silicon </span>
|
||||
</a>
|
||||
<a :href="macLinks.intel" download="">
|
||||
<DownloadIcon />
|
||||
<span> Download for Intel </span>
|
||||
<span> Download the beta </span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>{{ formatMessage(messages.welcomeLongTitle) }}</h1>
|
||||
<div class="welcome-box has-bot">
|
||||
<img :src="WavingRinthbot" alt="Waving Modrinth Bot" class="welcome-box__waving-bot" />
|
||||
<div class="welcome-box__top-glow" />
|
||||
<div class="welcome-box__body">
|
||||
<h1 class="welcome-box__title">
|
||||
{{ formatMessage(messages.welcomeLongTitle) }}
|
||||
</h1>
|
||||
|
||||
<section class="auth-form">
|
||||
<p>
|
||||
{{ formatMessage(messages.welcomeDescription) }}
|
||||
<p class="welcome-box__subtitle">
|
||||
<IntlFormatted :message-id="messages.welcomeDescription">
|
||||
<template #bold="{ children }">
|
||||
<strong>
|
||||
<component :is="() => normalizeChildren(children)" />
|
||||
</strong>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</p>
|
||||
|
||||
<Checkbox
|
||||
@@ -14,11 +24,12 @@
|
||||
:description="formatMessage(messages.subscribeCheckbox)"
|
||||
/>
|
||||
|
||||
<button class="btn btn-primary continue-btn centered-btn" @click="continueSignUp">
|
||||
{{ formatMessage(commonMessages.continueButton) }} <RightArrowIcon />
|
||||
<button class="btn btn-primary centered-btn" @click="continueSignUp">
|
||||
{{ formatMessage(commonMessages.continueButton) }}
|
||||
<RightArrowIcon />
|
||||
</button>
|
||||
|
||||
<p>
|
||||
<p class="tos-text">
|
||||
<IntlFormatted :message-id="messages.tosLabel">
|
||||
<template #terms-link="{ children }">
|
||||
<NuxtLink to="/legal/terms" class="text-link">
|
||||
@@ -32,12 +43,15 @@
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Checkbox, commonMessages } from "@modrinth/ui";
|
||||
import { RightArrowIcon } from "@modrinth/assets";
|
||||
import { RightArrowIcon, WavingRinthbot } from "@modrinth/assets";
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const { formatMessage } = useVIntl();
|
||||
|
||||
@@ -54,7 +68,7 @@ const messages = defineMessages({
|
||||
welcomeDescription: {
|
||||
id: "auth.welcome.description",
|
||||
defaultMessage:
|
||||
"Thank you for creating an account. You can now follow and create projects, receive updates about your favorite projects, and more!",
|
||||
"You’re now part of the awesome community of creators & explorers already building, downloading, and staying up-to-date with awazing mods.",
|
||||
},
|
||||
welcomeLongTitle: {
|
||||
id: "auth.welcome.long-title",
|
||||
@@ -72,20 +86,18 @@ useHead({
|
||||
|
||||
const subscribe = ref(true);
|
||||
|
||||
async function continueSignUp() {
|
||||
const route = useRoute();
|
||||
|
||||
onMounted(async () => {
|
||||
await useAuth(route.query.authToken);
|
||||
await useUser();
|
||||
});
|
||||
|
||||
async function continueSignUp() {
|
||||
if (subscribe.value) {
|
||||
try {
|
||||
await useBaseFetch("auth/email/subscribe", {
|
||||
method: "POST",
|
||||
});
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (route.query.redirect) {
|
||||
@@ -95,3 +107,84 @@ async function continueSignUp() {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.welcome-box {
|
||||
background-color: var(--color-raised-bg);
|
||||
border-radius: var(--size-rounded-lg);
|
||||
padding: 1.75rem 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
box-shadow: var(--shadow-card);
|
||||
position: relative;
|
||||
|
||||
&.has-bot {
|
||||
margin-block: 120px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
a {
|
||||
color: var(--color-brand);
|
||||
font-weight: var(--weight-bold);
|
||||
&:hover,
|
||||
&:focus {
|
||||
filter: brightness(1.125);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&__waving-bot {
|
||||
--bot-height: 112px;
|
||||
position: absolute;
|
||||
top: calc(-1 * var(--bot-height));
|
||||
right: 5rem;
|
||||
height: var(--bot-height);
|
||||
width: auto;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
--bot-height: 70px;
|
||||
right: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__top-glow {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
opacity: 0.4;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent 2rem,
|
||||
var(--color-green) calc(100% - 13rem),
|
||||
var(--color-green) calc(100% - 5rem),
|
||||
transparent calc(100% - 2rem)
|
||||
);
|
||||
}
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: var(--text-32);
|
||||
font-weight: var(--weight-extrabold);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-size: var(--text-18);
|
||||
}
|
||||
|
||||
.tos-text {
|
||||
font-size: var(--text-14);
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -391,6 +391,7 @@ import {
|
||||
DropdownSelect,
|
||||
FileInput,
|
||||
PopoutMenu,
|
||||
useRelativeTime,
|
||||
} from "@modrinth/ui";
|
||||
|
||||
import { isAdmin } from "@modrinth/utils";
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
import { ChevronRightIcon, HistoryIcon } from "@modrinth/assets";
|
||||
import Avatar from "~/components/ui/Avatar.vue";
|
||||
import NotificationItem from "~/components/ui/NotificationItem.vue";
|
||||
import { fetchExtraNotificationData, groupNotifications } from "~/helpers/notifications.js";
|
||||
import { fetchExtraNotificationData, groupNotifications } from "~/helpers/notifications.ts";
|
||||
|
||||
useHead({
|
||||
title: "Dashboard - Modrinth",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<h2 v-else class="text-2xl">Notifications</h2>
|
||||
</div>
|
||||
<template v-if="!history">
|
||||
<Button v-if="hasRead" @click="updateRoute()">
|
||||
<Button v-if="data.hasRead" @click="updateRoute()">
|
||||
<HistoryIcon />
|
||||
View history
|
||||
</Button>
|
||||
@@ -60,7 +60,7 @@ import {
|
||||
fetchExtraNotificationData,
|
||||
groupNotifications,
|
||||
markAsRead,
|
||||
} from "~/helpers/notifications.js";
|
||||
} from "~/helpers/notifications.ts";
|
||||
import NotificationItem from "~/components/ui/NotificationItem.vue";
|
||||
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
|
||||
import Pagination from "~/components/ui/Pagination.vue";
|
||||
@@ -70,93 +70,69 @@ useHead({
|
||||
});
|
||||
|
||||
const auth = await useAuth();
|
||||
|
||||
const route = useNativeRoute();
|
||||
const router = useNativeRouter();
|
||||
|
||||
const history = computed(() => {
|
||||
return route.name === "dashboard-notifications-history";
|
||||
});
|
||||
|
||||
const history = computed(() => route.name === "dashboard-notifications-history");
|
||||
const selectedType = ref("all");
|
||||
const page = ref(1);
|
||||
|
||||
const perPage = ref(50);
|
||||
|
||||
const { data, pending, error, refresh } = await useAsyncData(
|
||||
async () => {
|
||||
const pageNum = page.value - 1;
|
||||
|
||||
const notifications = await useBaseFetch(`user/${auth.value.user.id}/notifications`);
|
||||
const showRead = history.value;
|
||||
const hasRead = notifications.some((notif) => notif.read);
|
||||
const notifications = await useBaseFetch(`user/${auth.value.user.id}/notifications`);
|
||||
|
||||
const types = [
|
||||
...new Set(
|
||||
notifications
|
||||
.filter((notification) => {
|
||||
return showRead || !notification.read;
|
||||
})
|
||||
.map((notification) => notification.type),
|
||||
),
|
||||
const typesInFeed = [
|
||||
...new Set(notifications.filter((n) => showRead || !n.read).map((n) => n.type)),
|
||||
];
|
||||
|
||||
const filteredNotifications = notifications.filter(
|
||||
(notification) =>
|
||||
(selectedType.value === "all" || notification.type === selectedType.value) &&
|
||||
(showRead || !notification.read),
|
||||
const filtered = notifications.filter(
|
||||
(n) =>
|
||||
(selectedType.value === "all" || n.type === selectedType.value) && (showRead || !n.read),
|
||||
);
|
||||
const pages = Math.ceil(filteredNotifications.length / perPage.value);
|
||||
|
||||
const pages = Math.max(1, Math.ceil(filtered.length / perPage.value));
|
||||
|
||||
return fetchExtraNotificationData(
|
||||
filteredNotifications.slice(pageNum * perPage.value, perPage.value + pageNum * perPage.value),
|
||||
).then((notifications) => {
|
||||
return {
|
||||
notifications,
|
||||
types: types.length > 1 ? ["all", ...types] : types,
|
||||
pages,
|
||||
hasRead,
|
||||
};
|
||||
});
|
||||
filtered.slice(pageNum * perPage.value, pageNum * perPage.value + perPage.value),
|
||||
).then((notifs) => ({
|
||||
notifications: notifs,
|
||||
notifTypes: typesInFeed.length > 1 ? ["all", ...typesInFeed] : typesInFeed,
|
||||
pages,
|
||||
hasRead: notifications.some((n) => n.read),
|
||||
}));
|
||||
},
|
||||
{ watch: [page, history, selectedType] },
|
||||
);
|
||||
|
||||
const notifications = computed(() => {
|
||||
if (data.value === null) {
|
||||
return [];
|
||||
}
|
||||
return groupNotifications(data.value.notifications, history.value);
|
||||
});
|
||||
const notifTypes = computed(() => data.value.types);
|
||||
const pages = computed(() => data.value.pages);
|
||||
const hasRead = computed(() => data.value.hasRead);
|
||||
const notifications = computed(() =>
|
||||
data.value ? groupNotifications(data.value.notifications, history.value) : [],
|
||||
);
|
||||
|
||||
const notifTypes = computed(() => data.value?.notifTypes || []);
|
||||
const pages = computed(() => data.value?.pages ?? 1);
|
||||
|
||||
function updateRoute() {
|
||||
if (history.value) {
|
||||
router.push("/dashboard/notifications");
|
||||
} else {
|
||||
router.push("/dashboard/notifications/history");
|
||||
}
|
||||
router.push(history.value ? "/dashboard/notifications" : "/dashboard/notifications/history");
|
||||
selectedType.value = "all";
|
||||
page.value = 1;
|
||||
}
|
||||
|
||||
async function readAll() {
|
||||
const ids = notifications.value.flatMap((notification) => [
|
||||
notification.id,
|
||||
...(notification.grouped_notifs ? notification.grouped_notifs.map((notif) => notif.id) : []),
|
||||
const ids = notifications.value.flatMap((n) => [
|
||||
n.id,
|
||||
...(n.grouped_notifs ? n.grouped_notifs.map((g) => g.id) : []),
|
||||
]);
|
||||
|
||||
const updateNotifs = await markAsRead(ids);
|
||||
allNotifs.value = updateNotifs(allNotifs.value);
|
||||
await markAsRead(ids);
|
||||
await refresh();
|
||||
}
|
||||
|
||||
function changePage(newPage) {
|
||||
page.value = newPage;
|
||||
if (import.meta.client) {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}
|
||||
if (import.meta.client) window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useRelativeTime } from "@modrinth/ui";
|
||||
|
||||
const vintl = useVIntl();
|
||||
const { formatMessage } = vintl;
|
||||
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
<CalendarIcon aria-hidden="true" />
|
||||
<span>
|
||||
Received
|
||||
{{ fromNow(notification.date_modified) }}
|
||||
{{ formatRelativeTime(notification.date_modified) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -527,7 +527,7 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import { Multiselect } from "vue-multiselect";
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import { ButtonStyled, useRelativeTime } from "@modrinth/ui";
|
||||
import {
|
||||
CompassIcon,
|
||||
LogInIcon,
|
||||
@@ -544,6 +544,8 @@ import ProjectCard from "~/components/ui/ProjectCard.vue";
|
||||
|
||||
import { homePageProjects, homePageSearch, homePageNotifs } from "~/generated/state.json";
|
||||
|
||||
const formatRelativeTime = useRelativeTime();
|
||||
|
||||
const searchQuery = ref("leave");
|
||||
const sortType = ref("relevance");
|
||||
|
||||
|
||||
@@ -52,10 +52,7 @@
|
||||
>
|
||||
<div class="project-title">
|
||||
<div class="mobile-row">
|
||||
<nuxt-link
|
||||
:to="`/${project.inferred_project_type}/${project.slug}`"
|
||||
class="iconified-stacked-link"
|
||||
>
|
||||
<nuxt-link :to="`/project/${project.id}`" class="iconified-stacked-link">
|
||||
<Avatar :src="project.icon_url" size="xs" no-shadow raised />
|
||||
<span class="stacked">
|
||||
<span class="title">{{ project.name }}</span>
|
||||
@@ -67,7 +64,7 @@
|
||||
by
|
||||
<nuxt-link
|
||||
v-if="project.owner"
|
||||
:to="`/user/${project.owner.user.username}`"
|
||||
:to="`/user/${project.owner.user.id}`"
|
||||
class="iconified-link"
|
||||
>
|
||||
<Avatar :src="project.owner.user.avatar_url" circle size="xxs" raised />
|
||||
@@ -75,7 +72,7 @@
|
||||
</nuxt-link>
|
||||
<nuxt-link
|
||||
v-else-if="project.org"
|
||||
:to="`/organization/${project.org.slug}`"
|
||||
:to="`/organization/${project.org.id}`"
|
||||
class="iconified-link"
|
||||
>
|
||||
<Avatar :src="project.org.icon_url" circle size="xxs" raised />
|
||||
@@ -88,10 +85,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<nuxt-link
|
||||
:to="`/${project.inferred_project_type}/${project.slug}`"
|
||||
class="iconified-button raised-button"
|
||||
>
|
||||
<nuxt-link :to="`/project/${project.id}`" class="iconified-button raised-button">
|
||||
<EyeIcon />
|
||||
View project
|
||||
</nuxt-link>
|
||||
@@ -100,7 +94,7 @@
|
||||
<IssuesIcon v-if="project.age_warning" />
|
||||
Submitted
|
||||
<span v-tooltip="$dayjs(project.queued).format('MMMM D, YYYY [at] h:mm A')">{{
|
||||
fromNow(project.queued)
|
||||
formatRelativeTime(project.queued)
|
||||
}}</span>
|
||||
</span>
|
||||
<span v-else class="submitter-info"><UnknownIcon /> Unknown queue date</span>
|
||||
@@ -109,7 +103,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Chips } from "@modrinth/ui";
|
||||
import { Chips, useRelativeTime } from "@modrinth/ui";
|
||||
import {
|
||||
UnknownIcon,
|
||||
EyeIcon,
|
||||
@@ -134,6 +128,8 @@ const now = app.$dayjs();
|
||||
const TIME_24H = 86400000;
|
||||
const TIME_48H = TIME_24H * 2;
|
||||
|
||||
const formatRelativeTime = useRelativeTime();
|
||||
|
||||
const { data: projects } = await useAsyncData("moderation/projects?count=1000", () =>
|
||||
useBaseFetch("moderation/projects?count=1000", { internal: true }),
|
||||
);
|
||||
|
||||
@@ -39,13 +39,8 @@ if (!changelogEntry.value) {
|
||||
>
|
||||
<ChevronLeftIcon /> View full changelog
|
||||
</nuxt-link>
|
||||
<Timeline fade-out-end :fade-out-start="!isFirst" :class="{ '-mt-8': !isFirst }">
|
||||
<ChangelogEntry
|
||||
:entry="changelogEntry"
|
||||
:first="isFirst"
|
||||
show-type
|
||||
:class="{ 'mt-8': !isFirst }"
|
||||
/>
|
||||
<Timeline fade-out-end :fade-out-start="!isFirst">
|
||||
<ChangelogEntry :entry="changelogEntry" :first="isFirst" show-type />
|
||||
</Timeline>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -123,6 +123,7 @@
|
||||
},
|
||||
{ divider: true, shown: auth.user && currentMember },
|
||||
{ id: 'copy-id', action: () => copyId() },
|
||||
{ id: 'copy-permalink', action: () => copyPermalink() },
|
||||
]"
|
||||
aria-label="More options"
|
||||
>
|
||||
@@ -135,6 +136,10 @@
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.copyIdButton) }}
|
||||
</template>
|
||||
<template #copy-permalink>
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.copyPermalinkButton) }}
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
@@ -287,6 +292,7 @@ const cosmetics = useCosmetics();
|
||||
const route = useNativeRoute();
|
||||
const tags = useTags();
|
||||
const flags = useFeatureFlags();
|
||||
const config = useRuntimeConfig();
|
||||
|
||||
let orgId = useRouteId();
|
||||
|
||||
@@ -502,6 +508,12 @@ const navLinks = computed(() => [
|
||||
async function copyId() {
|
||||
await navigator.clipboard.writeText(organization.value.id);
|
||||
}
|
||||
|
||||
async function copyPermalink() {
|
||||
await navigator.clipboard.writeText(
|
||||
`${config.public.siteUrl}/organization/${organization.value.id}`,
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="experimental-styles-within flex flex-col gap-2">
|
||||
<RadialHeader class="top-box mb-2 text-center" color="orange">
|
||||
<RadialHeader class="top-box mb-2 flex flex-col items-center justify-center" color="orange">
|
||||
<ScaleIcon class="h-12 w-12 text-brand-orange" />
|
||||
<h1 class="m-3 gap-2 text-3xl font-extrabold">
|
||||
{{
|
||||
|
||||
@@ -303,7 +303,7 @@
|
||||
</svg>
|
||||
<h2 class="m-0 text-lg font-bold">Backups included</h2>
|
||||
<h3 class="m-0 text-base font-normal text-secondary">
|
||||
Every server comes with 15 backups stored securely off-site with Backblaze.
|
||||
Every server comes with 15 backups stored securely off-site.
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
@@ -399,7 +399,7 @@
|
||||
</summary>
|
||||
<p class="m-0 ml-6 leading-[160%]">
|
||||
Modrinth Servers are powered by AMD Ryzen 7900 and 7950X3D equivalent CPUs at 5+
|
||||
GHz, paired with with DDR5 memory.
|
||||
GHz, paired with DDR5 memory.
|
||||
</p>
|
||||
</details>
|
||||
<details pyro-hash="cpu-burst" class="group" :open="$route.hash === '#cpu-burst'">
|
||||
@@ -480,6 +480,18 @@
|
||||
plan for the content you're running on the server.
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<details pyro-hash="players" class="group" :open="$route.hash === '#prices'">
|
||||
<summary class="flex cursor-pointer items-center py-3 font-medium text-contrast">
|
||||
<span class="mr-2 transition-transform duration-200 group-open:rotate-90">
|
||||
<RightArrowIcon />
|
||||
</span>
|
||||
What currency are the prices in?
|
||||
</summary>
|
||||
<p class="m-0 ml-6 leading-[160%]">
|
||||
All prices are listed in United States Dollars (USD).
|
||||
</p>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,26 +33,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="serverData?.status === 'suspended' && serverData.suspension_reason === 'support'"
|
||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="grid place-content-center rounded-full bg-bg-blue p-4">
|
||||
<TransferIcon class="size-12 text-blue" />
|
||||
</div>
|
||||
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">We're working on your server</h1>
|
||||
</div>
|
||||
<p class="text-lg text-secondary">
|
||||
You recently contacted Modrinth Support, and we're actively working on your server. It
|
||||
will be back online shortly.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="serverData?.status === 'suspended' && serverData.suspension_reason !== 'upgrading'"
|
||||
v-else-if="serverData?.status === 'suspended'"
|
||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
@@ -72,7 +53,7 @@
|
||||
: "Your server has been suspended."
|
||||
}}
|
||||
<br />
|
||||
Contact Modrinth support if you believe this is an error.
|
||||
Contact Modrinth Support if you believe this is an error.
|
||||
</p>
|
||||
</div>
|
||||
<ButtonStyled size="large" color="brand" @click="() => router.push('/settings/billing')">
|
||||
@@ -97,7 +78,7 @@
|
||||
</div>
|
||||
<p class="text-lg text-secondary">
|
||||
You don't have permission to view this server or it no longer exists. If you believe this
|
||||
is an error, please contact Modrinth support.
|
||||
is an error, please contact Modrinth Support.
|
||||
</p>
|
||||
</div>
|
||||
<UiCopyCode :text="JSON.stringify(server.general?.error)" />
|
||||
@@ -289,7 +270,7 @@
|
||||
can change the loader by clicking the "Change Loader" button.
|
||||
</li>
|
||||
<li>
|
||||
If you're stuck, please contact Modrinth support with the information below:
|
||||
If you're stuck, please contact Modrinth Support with the information below:
|
||||
</li>
|
||||
</ul>
|
||||
<ButtonStyled>
|
||||
@@ -309,7 +290,7 @@
|
||||
An error occurred while installing your server because Modrinth Servers does not
|
||||
support the version of Minecraft or the loader you specified. Try reinstalling your
|
||||
server with a different version or loader, and if the problem persists, please
|
||||
contact Modrinth support with your server's debug information.
|
||||
contact Modrinth Support with your server's debug information.
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -787,6 +768,40 @@ const handleWebSocketMessage = (data: WSEvent) => {
|
||||
|
||||
break;
|
||||
}
|
||||
case "filesystem-ops": {
|
||||
if (!server.fs) {
|
||||
console.error("FilesystemOps received, but server.fs is not available", data.all);
|
||||
break;
|
||||
}
|
||||
if (JSON.stringify(server.fs.ops) !== JSON.stringify(data.all)) {
|
||||
server.fs.ops = data.all;
|
||||
}
|
||||
|
||||
server.fs.queuedOps = server.fs.queuedOps.filter(
|
||||
(queuedOp) => !data.all.some((x) => x.src === queuedOp.src),
|
||||
);
|
||||
|
||||
const cancelled = data.all.filter((x) => x.state === "cancelled");
|
||||
Promise.all(cancelled.map((x) => server.fs?.modifyOp(x.id, "dismiss")));
|
||||
|
||||
const completed = data.all.filter((x) => x.state === "done");
|
||||
if (completed.length > 0) {
|
||||
setTimeout(
|
||||
async () =>
|
||||
await Promise.all(
|
||||
completed.map((x) => {
|
||||
if (!server.fs?.opsQueuedForModification.includes(x.id)) {
|
||||
server.fs?.opsQueuedForModification.push(x.id);
|
||||
return server.fs?.modifyOp(x.id, "dismiss");
|
||||
}
|
||||
return Promise.resolve();
|
||||
}),
|
||||
),
|
||||
3000,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.warn("Unhandled WebSocket event:", data);
|
||||
}
|
||||
|
||||
@@ -56,8 +56,7 @@
|
||||
</TagItem>
|
||||
</div>
|
||||
<p class="m-0">
|
||||
You can have up to {{ data.backup_quota }} backups at once, securely off-site with
|
||||
Backblaze.
|
||||
You can have up to {{ data.backup_quota }} backups at once, stored securely off-site.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
:type="newItemType"
|
||||
@create="handleCreateNewItem"
|
||||
/>
|
||||
<FilesUploadZipUrlModal ref="uploadZipModal" :server="server" />
|
||||
<FilesUploadConflictModal ref="uploadConflictModal" @proceed="extractItem" />
|
||||
|
||||
<LazyUiServersFilesRenameItemModal
|
||||
ref="renameItemModal"
|
||||
@@ -35,9 +37,12 @@
|
||||
:breadcrumb-segments="breadcrumbSegments"
|
||||
:search-query="searchQuery"
|
||||
:current-filter="viewFilter"
|
||||
:base-id="`browse-navbar-${baseId}`"
|
||||
@navigate="navigateToSegment"
|
||||
@create="showCreateModal"
|
||||
@upload="initiateFileUpload"
|
||||
@upload-zip="() => {}"
|
||||
@unzip-from-url="showUnzipFromUrlModal"
|
||||
@filter="handleFilter"
|
||||
@update:search-query="searchQuery = $event"
|
||||
/>
|
||||
@@ -46,6 +51,110 @@
|
||||
:sort-desc="sortDesc"
|
||||
@sort="handleSort"
|
||||
/>
|
||||
<div
|
||||
v-for="op in ops"
|
||||
:key="`fs-op-${op.op}-${op.src}`"
|
||||
class="sticky top-20 z-20 grid grid-cols-[auto_1fr_auto] items-center gap-2 border-0 border-b-[1px] border-solid border-button-bg bg-table-alternateRow px-4 py-2 md:grid-cols-[auto_1fr_1fr_2fr_auto]"
|
||||
>
|
||||
<div>
|
||||
<PackageOpenIcon class="h-5 w-5 text-secondary" />
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-x-4 gap-y-1 md:contents">
|
||||
<div class="flex items-center text-wrap break-all text-sm font-bold text-contrast">
|
||||
Extracting {{ op.src.includes("https://") ? "modpack from URL" : op.src }}
|
||||
</div>
|
||||
<span
|
||||
class="flex items-center gap-2 text-sm font-semibold"
|
||||
:class="{
|
||||
'text-green': op.state === 'done',
|
||||
'text-red': op.state?.startsWith('fail'),
|
||||
'text-orange': !op.state?.startsWith('fail') && op.state !== 'done',
|
||||
}"
|
||||
>
|
||||
<template v-if="op.state === 'done'">
|
||||
Done
|
||||
<CheckIcon style="stroke-width: 3px" />
|
||||
</template>
|
||||
<template v-else-if="op.state?.startsWith('fail')">
|
||||
Failed
|
||||
<XIcon style="stroke-width: 3px" />
|
||||
</template>
|
||||
<template v-else-if="op.state === 'cancelled'">
|
||||
<SpinnerIcon class="animate-spin" />
|
||||
Cancelling
|
||||
</template>
|
||||
<template v-else-if="op.state === 'queued'">
|
||||
<SpinnerIcon class="animate-spin" />
|
||||
Queued...
|
||||
</template>
|
||||
<template v-else-if="op.state === 'ongoing'">
|
||||
<SpinnerIcon class="animate-spin" />
|
||||
Extracting...
|
||||
</template>
|
||||
<template v-else>
|
||||
<UnknownIcon />
|
||||
Unknown state: {{ op.state }}
|
||||
</template>
|
||||
</span>
|
||||
<div class="col-span-2 flex grow flex-col gap-1 md:col-span-1 md:items-end">
|
||||
<div class="text-xs font-semibold text-contrast opacity-80">
|
||||
<span :class="{ invisible: 'current_file' in op && !op.current_file }">
|
||||
{{
|
||||
"current_file" in op
|
||||
? op.current_file?.split("/")?.pop() ?? "unknown"
|
||||
: "unknown"
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<ProgressBar
|
||||
:progress="'progress' in op ? op.progress : 0"
|
||||
:max="1"
|
||||
:color="
|
||||
op.state === 'done'
|
||||
? 'green'
|
||||
: op.state?.startsWith('fail')
|
||||
? 'red'
|
||||
: op.state === 'cancelled'
|
||||
? 'gray'
|
||||
: 'orange'
|
||||
"
|
||||
:waiting="op.state === 'queued' || !op.progress || op.progress === 0"
|
||||
/>
|
||||
<div
|
||||
class="text-xs text-secondary opacity-80"
|
||||
:class="{ invisible: 'bytes_processed' in op && !op.bytes_processed }"
|
||||
>
|
||||
{{ "bytes_processed" in op ? formatBytes(op.bytes_processed) : "0 B" }} extracted
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ButtonStyled circular>
|
||||
<button
|
||||
:disabled="!('id' in op) || !op.id"
|
||||
class="radial-progress-animation-overlay"
|
||||
:class="{ active: op.state === 'done' }"
|
||||
@click="
|
||||
() => {
|
||||
op.state === 'done'
|
||||
? server.fs?.modifyOp(op.id, 'dismiss')
|
||||
: 'id' in op
|
||||
? server.fs?.modifyOp(op.id, 'cancel')
|
||||
: () => {};
|
||||
}
|
||||
"
|
||||
>
|
||||
<XIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<pre
|
||||
v-if="flags.advancedDebugInfo"
|
||||
class="markdown-body col-span-full m-0 rounded-xl bg-button-bg text-xs"
|
||||
>{{ op }}</pre
|
||||
>
|
||||
</div>
|
||||
<FilesUploadDropdown
|
||||
v-if="props.server.fs"
|
||||
ref="uploadDropdownRef"
|
||||
@@ -55,7 +164,6 @@
|
||||
@upload-complete="refreshList()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UiServersFilesEditingNavbar
|
||||
v-else
|
||||
:file-name="editingFile?.name"
|
||||
@@ -97,10 +205,10 @@
|
||||
/>
|
||||
<UiServersFilesImageViewer v-else :image-blob="imagePreview" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="items.length > 0" class="h-full w-full overflow-hidden rounded-b-2xl">
|
||||
<UiServersFileVirtualList
|
||||
:items="filteredItems"
|
||||
@extract="handleExtractItem"
|
||||
@delete="showDeleteModal"
|
||||
@rename="showRenameModal"
|
||||
@download="downloadFile"
|
||||
@@ -159,10 +267,32 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useInfiniteScroll } from "@vueuse/core";
|
||||
import { UploadIcon, FolderOpenIcon } from "@modrinth/assets";
|
||||
import type { DirectoryResponse, DirectoryItem, Server } from "~/composables/pyroServers";
|
||||
import {
|
||||
UnknownIcon,
|
||||
XIcon,
|
||||
SpinnerIcon,
|
||||
PackageOpenIcon,
|
||||
CheckIcon,
|
||||
UploadIcon,
|
||||
FolderOpenIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { computed } from "vue";
|
||||
import { ButtonStyled, ProgressBar } from "@modrinth/ui";
|
||||
import { formatBytes } from "@modrinth/utils";
|
||||
import {
|
||||
type DirectoryResponse,
|
||||
type DirectoryItem,
|
||||
type Server,
|
||||
handleError,
|
||||
} from "~/composables/pyroServers.ts";
|
||||
import FilesUploadDragAndDrop from "~/components/ui/servers/FilesUploadDragAndDrop.vue";
|
||||
import FilesUploadDropdown from "~/components/ui/servers/FilesUploadDropdown.vue";
|
||||
import type { FilesystemOp, FSQueuedOp } from "~/types/servers.ts";
|
||||
import FilesUploadZipUrlModal from "~/components/ui/servers/FilesUploadZipUrlModal.vue";
|
||||
import FilesUploadConflictModal from "~/components/ui/servers/FilesUploadConflictModal.vue";
|
||||
|
||||
const flags = useFeatureFlags();
|
||||
const baseId = useId();
|
||||
|
||||
interface BaseOperation {
|
||||
type: "move" | "rename";
|
||||
@@ -217,6 +347,8 @@ const createItemModal = ref();
|
||||
const renameItemModal = ref();
|
||||
const moveItemModal = ref();
|
||||
const deleteItemModal = ref();
|
||||
const uploadZipModal = ref();
|
||||
const uploadConflictModal = ref();
|
||||
|
||||
const newItemType = ref<"file" | "directory">("file");
|
||||
const selectedItem = ref<any>(null);
|
||||
@@ -449,6 +581,33 @@ const handleRenameItem = async (newName: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const extractItem = async (path: string) => {
|
||||
try {
|
||||
await props.server.fs?.extractFile(path, true, false);
|
||||
} catch (error) {
|
||||
console.error("Error extracting item:", error);
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExtractItem = async (item: { name: string; type: string; path: string }) => {
|
||||
try {
|
||||
const dry = await props.server.fs?.extractFile(item.path, true, true, true);
|
||||
if (dry) {
|
||||
if (dry.conflicting_files.length === 0) {
|
||||
await extractItem(item.path);
|
||||
} else {
|
||||
uploadConflictModal.value.show(item.path, dry.conflicting_files);
|
||||
}
|
||||
} else {
|
||||
handleError(new Error("Error running dry run"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error extracting item:", error);
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMoveItem = async (destination: string) => {
|
||||
try {
|
||||
const itemType = selectedItem.value.type;
|
||||
@@ -536,6 +695,10 @@ const showCreateModal = (type: "file" | "directory") => {
|
||||
createItemModal.value?.show();
|
||||
};
|
||||
|
||||
const showUnzipFromUrlModal = (cf: boolean) => {
|
||||
uploadZipModal.value?.show(cf);
|
||||
};
|
||||
|
||||
const showRenameModal = (item: any) => {
|
||||
selectedItem.value = item;
|
||||
renameItemModal.value?.show(item);
|
||||
@@ -760,6 +923,8 @@ onMounted(async () => {
|
||||
redoLastOperation();
|
||||
}
|
||||
});
|
||||
|
||||
props.server.fs?.clearQueuedOps();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -768,6 +933,22 @@ onUnmounted(() => {
|
||||
document.removeEventListener("keydown", () => {});
|
||||
});
|
||||
|
||||
const clientSideQueued = computed<FSQueuedOp[]>(() => props.server.fs?.queuedOps ?? []);
|
||||
|
||||
type QueuedOpWithState = FSQueuedOp & { state: "queued" };
|
||||
|
||||
const ops = computed<(QueuedOpWithState | FilesystemOp)[]>(() => [
|
||||
...clientSideQueued.value.map((x) => ({ ...x, state: "queued" }) satisfies QueuedOpWithState),
|
||||
...(props.server.fs?.ops ?? []),
|
||||
]);
|
||||
|
||||
watch(
|
||||
() => props.server.fs?.ops,
|
||||
() => {
|
||||
refreshList();
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => route.query,
|
||||
async (newQuery) => {
|
||||
@@ -984,4 +1165,43 @@ const onScroll = () => {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.radial-progress-animation-overlay {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@property --_radial-percentage {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 0%;
|
||||
}
|
||||
|
||||
.radial-progress-animation-overlay.active::before {
|
||||
animation: radial-progress 3s linear forwards;
|
||||
}
|
||||
|
||||
.radial-progress-animation-overlay::before {
|
||||
content: "";
|
||||
inset: -2px;
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
box-sizing: content-box;
|
||||
border: 2px solid var(--color-button-bg);
|
||||
filter: brightness(var(--hover-brightness));
|
||||
mask-image: conic-gradient(
|
||||
black 0%,
|
||||
black var(--_radial-percentage),
|
||||
transparent var(--_radial-percentage),
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
@keyframes radial-progress {
|
||||
from {
|
||||
--_radial-percentage: 0%;
|
||||
}
|
||||
to {
|
||||
--_radial-percentage: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<ModalConfirm
|
||||
<ConfirmModal
|
||||
ref="modal_confirm"
|
||||
title="Are you sure you want to delete your account?"
|
||||
description="This will **immediately delete all of your user data and follows**. This will not delete your projects. Deleting your account cannot be reversed.<br><br>If you need help with your account, get support on the [Modrinth Discord](https://discord.modrinth.com)."
|
||||
@@ -421,6 +421,7 @@ import {
|
||||
DownloadIcon,
|
||||
} from "@modrinth/assets";
|
||||
import QrcodeVue from "qrcode.vue";
|
||||
import { ConfirmModal } from "@modrinth/ui";
|
||||
import GitHubIcon from "assets/icons/auth/sso-github.svg";
|
||||
import MicrosoftIcon from "assets/icons/auth/sso-microsoft.svg";
|
||||
import GoogleIcon from "assets/icons/auth/sso-google.svg";
|
||||
@@ -428,7 +429,6 @@ import SteamIcon from "assets/icons/auth/sso-steam.svg";
|
||||
import DiscordIcon from "assets/icons/auth/sso-discord.svg";
|
||||
import KeyIcon from "assets/icons/auth/key.svg";
|
||||
import GitLabIcon from "assets/icons/auth/sso-gitlab.svg";
|
||||
import ModalConfirm from "~/components/ui/ModalConfirm.vue";
|
||||
import Modal from "~/components/ui/Modal.vue";
|
||||
|
||||
useHead({
|
||||
|
||||
@@ -64,6 +64,21 @@
|
||||
</template>
|
||||
</span>
|
||||
<template v-if="midasCharge">
|
||||
<span
|
||||
v-if="
|
||||
midasCharge.status === 'open' && midasCharge.subscription_interval === 'monthly'
|
||||
"
|
||||
class="text-sm text-purple"
|
||||
>
|
||||
Save
|
||||
{{
|
||||
formatPrice(
|
||||
vintl.locale,
|
||||
midasCharge.amount * 12 - oppositePrice,
|
||||
midasCharge.currency_code,
|
||||
)
|
||||
}}/year by switching to yearly billing!
|
||||
</span>
|
||||
<span class="text-sm text-secondary">
|
||||
Since {{ $dayjs(midasSubscription.created).format("MMMM D, YYYY") }}
|
||||
</span>
|
||||
@@ -118,19 +133,46 @@
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<ButtonStyled v-else-if="midasCharge && midasCharge.status !== 'cancelled'">
|
||||
<button
|
||||
class="ml-auto"
|
||||
@click="
|
||||
() => {
|
||||
cancelSubscriptionId = midasSubscription.id;
|
||||
$refs.modalCancel.show();
|
||||
}
|
||||
"
|
||||
<div
|
||||
v-else-if="midasCharge && midasCharge.status !== 'cancelled'"
|
||||
class="ml-auto flex gap-2"
|
||||
>
|
||||
<ButtonStyled>
|
||||
<button
|
||||
:disabled="changingInterval"
|
||||
@click="
|
||||
() => {
|
||||
cancelSubscriptionId = midasSubscription.id;
|
||||
$refs.modalCancel.show();
|
||||
}
|
||||
"
|
||||
>
|
||||
<XIcon /> Cancel
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
:color="midasCharge.subscription_interval === 'yearly' ? 'standard' : 'purple'"
|
||||
color-fill="text"
|
||||
>
|
||||
<XIcon /> Cancel
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<button
|
||||
v-tooltip="
|
||||
midasCharge.subscription_interval === 'yearly'
|
||||
? `Monthly billing will cost you an additional ${formatPrice(
|
||||
vintl.locale,
|
||||
oppositePrice * 12 - midasCharge.amount,
|
||||
midasCharge.currency_code,
|
||||
)} per year`
|
||||
: undefined
|
||||
"
|
||||
:disabled="changingInterval"
|
||||
@click="switchMidasInterval(oppositeInterval)"
|
||||
>
|
||||
<SpinnerIcon v-if="changingInterval" class="animate-spin" />
|
||||
<TransferIcon v-else /> {{ changingInterval ? "Switching" : "Switch" }} to
|
||||
{{ oppositeInterval }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<ButtonStyled
|
||||
v-else-if="midasCharge && midasCharge.status === 'cancelled'"
|
||||
color="purple"
|
||||
@@ -178,8 +220,12 @@
|
||||
/>
|
||||
<div v-else class="w-fit">
|
||||
<p>
|
||||
A linked server couldn't be found with this subscription. It may have been deleted
|
||||
or suspended. Please contact Modrinth support with the following information:
|
||||
A linked server couldn't be found for this subscription. There are a few possible
|
||||
explanations for this. If you just purchased your server, this is normal. It could
|
||||
take up to an hour for your server to be provisioned. Otherwise, if you purchased
|
||||
this server a while ago, it has likely since been suspended. If this is not what
|
||||
you were expecting, please contact Modrinth Support with the following
|
||||
information:
|
||||
</p>
|
||||
<div class="flex w-full flex-col gap-2">
|
||||
<CopyCode
|
||||
@@ -288,7 +334,7 @@
|
||||
getPyroCharge(subscription).status !== 'failed'
|
||||
"
|
||||
>
|
||||
<button @click="showPyroCancelModal(subscription.id)">
|
||||
<button @click="showCancellationSurvey(subscription)">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
@@ -315,7 +361,14 @@
|
||||
"
|
||||
color="green"
|
||||
>
|
||||
<button @click="resubscribePyro(subscription.id)">
|
||||
<button
|
||||
@click="
|
||||
resubscribePyro(
|
||||
subscription.id,
|
||||
$dayjs(getPyroCharge(subscription).due).isBefore($dayjs()),
|
||||
)
|
||||
"
|
||||
>
|
||||
Resubscribe <RightArrowIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
@@ -547,6 +600,8 @@ import {
|
||||
} from "@modrinth/ui";
|
||||
import {
|
||||
PlusIcon,
|
||||
TransferIcon,
|
||||
SpinnerIcon,
|
||||
ArrowBigUpDashIcon,
|
||||
XIcon,
|
||||
CardIcon,
|
||||
@@ -750,6 +805,13 @@ const midasCharge = computed(() =>
|
||||
: null,
|
||||
);
|
||||
|
||||
const oppositePrice = computed(() =>
|
||||
midasSubscription.value
|
||||
? midasProduct.value?.prices?.find((price) => price.id === midasSubscription.value.price_id)
|
||||
?.prices?.intervals?.[oppositeInterval.value]
|
||||
: undefined,
|
||||
);
|
||||
|
||||
const pyroSubscriptions = computed(() => {
|
||||
const pyroSubs = subscriptions.value?.filter((s) => s?.metadata?.type === "pyro") || [];
|
||||
const servers = serversData.value?.servers || [];
|
||||
@@ -847,6 +909,31 @@ async function submit() {
|
||||
|
||||
const removePaymentMethodIndex = ref();
|
||||
|
||||
const changingInterval = ref(false);
|
||||
|
||||
const oppositeInterval = computed(() =>
|
||||
midasCharge.value?.subscription_interval === "yearly" ? "monthly" : "yearly",
|
||||
);
|
||||
|
||||
async function switchMidasInterval(interval) {
|
||||
changingInterval.value = true;
|
||||
startLoading();
|
||||
try {
|
||||
await useBaseFetch(`billing/subscription/${midasSubscription.value.id}`, {
|
||||
internal: true,
|
||||
method: "PATCH",
|
||||
body: {
|
||||
interval,
|
||||
},
|
||||
});
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
console.error("Error switching Modrinth+ payment interval:", error);
|
||||
}
|
||||
stopLoading();
|
||||
changingInterval.value = false;
|
||||
}
|
||||
|
||||
async function editPaymentMethod(index, primary) {
|
||||
startLoading();
|
||||
try {
|
||||
@@ -946,15 +1033,6 @@ const getProductPrice = (product, interval) => {
|
||||
|
||||
const modalCancel = ref(null);
|
||||
|
||||
const showPyroCancelModal = (subscriptionId) => {
|
||||
cancelSubscriptionId.value = subscriptionId;
|
||||
if (modalCancel.value) {
|
||||
modalCancel.value.show();
|
||||
} else {
|
||||
console.error("modalCancel ref is undefined");
|
||||
}
|
||||
};
|
||||
|
||||
const pyroPurchaseModal = ref();
|
||||
const currentSubscription = ref(null);
|
||||
const currentProduct = ref(null);
|
||||
@@ -1027,7 +1105,7 @@ async function fetchCapacityStatuses(serverId, product) {
|
||||
}
|
||||
}
|
||||
|
||||
const resubscribePyro = async (subscriptionId) => {
|
||||
const resubscribePyro = async (subscriptionId, wasSuspended) => {
|
||||
try {
|
||||
await useBaseFetch(`billing/subscription/${subscriptionId}`, {
|
||||
internal: true,
|
||||
@@ -1037,6 +1115,21 @@ const resubscribePyro = async (subscriptionId) => {
|
||||
},
|
||||
});
|
||||
await refresh();
|
||||
if (wasSuspended) {
|
||||
data.$notify({
|
||||
group: "main",
|
||||
title: "Resubscription request submitted",
|
||||
text: "If the server is currently suspended, it may take up to 10 minutes for another charge attempt to be made.",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
data.$notify({
|
||||
group: "main",
|
||||
title: "Success",
|
||||
text: "Server subscription resubscribed successfully",
|
||||
type: "success",
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
data.$notify({
|
||||
group: "main",
|
||||
@@ -1057,4 +1150,66 @@ const refresh = async () => {
|
||||
refreshServers(),
|
||||
]);
|
||||
};
|
||||
|
||||
function showCancellationSurvey(subscription) {
|
||||
if (!subscription) {
|
||||
console.warn("No survey notice to open");
|
||||
return;
|
||||
}
|
||||
|
||||
const product = getPyroProduct(subscription);
|
||||
const priceObj = product?.prices?.find((x) => x.id === subscription.price_id);
|
||||
const price = priceObj?.prices?.intervals?.[subscription.interval];
|
||||
const currency = priceObj?.currency_code;
|
||||
|
||||
const popupOptions = {
|
||||
layout: "modal",
|
||||
width: 700,
|
||||
autoClose: 2000,
|
||||
hideTitle: true,
|
||||
hiddenFields: {
|
||||
username: auth.value?.user?.username,
|
||||
user_id: auth.value?.user?.id,
|
||||
user_email: auth.value?.user?.email,
|
||||
subscription_id: subscription.id,
|
||||
price_id: subscription.price_id,
|
||||
interval: subscription.interval,
|
||||
started: subscription.created,
|
||||
plan_ram: product?.metadata.ram / 1024,
|
||||
plan_cpu: product?.metadata.cpu,
|
||||
price: price ? `${price / 100}` : "unknown",
|
||||
currency: currency ?? "unknown",
|
||||
},
|
||||
onOpen: () => console.log(`Opened cancellation survey for: ${subscription.id}`),
|
||||
onClose: () => console.log(`Closed cancellation survey for: ${subscription.id}`),
|
||||
onSubmit: (payload) => {
|
||||
console.log("Form submitted, cancelling server.", payload);
|
||||
cancelSubscription(subscription.id, true);
|
||||
},
|
||||
};
|
||||
|
||||
const formId = "mOr7lM";
|
||||
|
||||
try {
|
||||
if (window.Tally?.openPopup) {
|
||||
console.log(
|
||||
`Opening Tally popup for servers subscription ${subscription.id} (form ID: ${formId})`,
|
||||
);
|
||||
window.Tally.openPopup(formId, popupOptions);
|
||||
} else {
|
||||
console.warn("Tally script not yet loaded");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error opening Tally popup:", e);
|
||||
}
|
||||
}
|
||||
|
||||
useHead({
|
||||
script: [
|
||||
{
|
||||
src: "https://tally.so/widgets/embed.js",
|
||||
defer: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -203,7 +203,13 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import { PlusIcon, XIcon, TrashIcon, EditIcon, SaveIcon } from "@modrinth/assets";
|
||||
import { Checkbox, ConfirmModal, commonSettingsMessages, commonMessages } from "@modrinth/ui";
|
||||
import {
|
||||
Checkbox,
|
||||
ConfirmModal,
|
||||
commonSettingsMessages,
|
||||
commonMessages,
|
||||
useRelativeTime,
|
||||
} from "@modrinth/ui";
|
||||
|
||||
import {
|
||||
hasScope,
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import { XIcon } from "@modrinth/assets";
|
||||
import { commonMessages, commonSettingsMessages } from "@modrinth/ui";
|
||||
import { commonMessages, commonSettingsMessages, useRelativeTime } from "@modrinth/ui";
|
||||
|
||||
definePageMeta({
|
||||
middleware: "auth",
|
||||
|
||||
@@ -125,6 +125,7 @@
|
||||
shown: auth.user?.id !== user.id,
|
||||
},
|
||||
{ id: 'copy-id', action: () => copyId() },
|
||||
{ id: 'copy-permalink', action: () => copyPermalink() },
|
||||
{
|
||||
id: 'open-billing',
|
||||
action: () => navigateTo(`/admin/billing/${user.id}`),
|
||||
@@ -151,6 +152,10 @@
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.copyIdButton) }}
|
||||
</template>
|
||||
<template #copy-permalink>
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.copyPermalinkButton) }}
|
||||
</template>
|
||||
<template #open-billing>
|
||||
<CurrencyIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.billingButton) }}
|
||||
@@ -355,6 +360,7 @@ import {
|
||||
ContentPageHeader,
|
||||
commonMessages,
|
||||
NewModal,
|
||||
useRelativeTime,
|
||||
} from "@modrinth/ui";
|
||||
import { isStaff } from "~/helpers/users.js";
|
||||
import NavTabs from "~/components/ui/NavTabs.vue";
|
||||
@@ -381,6 +387,7 @@ const auth = await useAuth();
|
||||
const cosmetics = useCosmetics();
|
||||
const tags = useTags();
|
||||
const flags = useFeatureFlags();
|
||||
const config = useRuntimeConfig();
|
||||
|
||||
const vintl = useVIntl();
|
||||
const { formatMessage } = vintl;
|
||||
@@ -616,6 +623,10 @@ async function copyId() {
|
||||
await navigator.clipboard.writeText(user.value.id);
|
||||
}
|
||||
|
||||
async function copyPermalink() {
|
||||
await navigator.clipboard.writeText(`${config.public.siteUrl}/user/${user.value.id}`);
|
||||
}
|
||||
|
||||
const navLinks = computed(() => [
|
||||
{
|
||||
label: formatMessage(commonMessages.allProjectType),
|
||||
|
||||
Reference in New Issue
Block a user