chore: fix frontend lint issues
This commit is contained in:
parent
f7fc208b15
commit
34e65ace1e
@ -1,6 +1,6 @@
|
||||
project_id: 518556
|
||||
preserve_hierarchy: true
|
||||
commit_message: "[ci skip]"
|
||||
commit_message: '[ci skip]'
|
||||
|
||||
files:
|
||||
- source: /locales/en-US/*
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import config from '@modrinth/tooling-config/eslint/nuxt.mjs'
|
||||
export default config.append([{
|
||||
export default config.append([
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
"import/no-unresolved": "off",
|
||||
'no-undef': 'off'
|
||||
}
|
||||
}])
|
||||
'import/no-unresolved': 'off',
|
||||
'no-undef': 'off',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
@ -6,6 +6,6 @@
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import ModrinthLoadingIndicator from "~/components/ui/modrinth-loading-indicator.ts";
|
||||
import Notifications from "~/components/ui/Notifications.vue";
|
||||
import ModrinthLoadingIndicator from '~/components/ui/modrinth-loading-indicator.ts'
|
||||
import Notifications from '~/components/ui/Notifications.vue'
|
||||
</script>
|
||||
|
||||
@ -415,7 +415,7 @@
|
||||
}
|
||||
|
||||
&:disabled,
|
||||
&[disabled="true"] {
|
||||
&[disabled='true'] {
|
||||
cursor: not-allowed;
|
||||
filter: grayscale(50%);
|
||||
opacity: 0.5;
|
||||
@ -461,7 +461,7 @@ tr.button-transparent {
|
||||
}
|
||||
|
||||
&:disabled > *,
|
||||
&[disabled="true"] > * {
|
||||
&[disabled='true'] > * {
|
||||
cursor: not-allowed;
|
||||
filter: grayscale(50%);
|
||||
opacity: 0.5;
|
||||
@ -492,7 +492,7 @@ tr.button-transparent {
|
||||
box-shadow: none;
|
||||
|
||||
&disabled,
|
||||
&[disabled="true"] {
|
||||
&[disabled='true'] {
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
@ -678,7 +678,7 @@ tr.button-transparent {
|
||||
background: var(--color-button-bg);
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 7px;
|
||||
|
||||
@ -125,8 +125,8 @@ html {
|
||||
|
||||
--shadow-card: rgba(50, 50, 100, 0.1) 0px 2px 4px 0px;
|
||||
|
||||
--landing-maze-bg: url("https://cdn.modrinth.com/landing-new/landing-light.webp");
|
||||
--landing-maze-gradient-bg: url("https://cdn.modrinth.com/landing-new/landing-lower-light.webp");
|
||||
--landing-maze-bg: url('https://cdn.modrinth.com/landing-new/landing-light.webp');
|
||||
--landing-maze-gradient-bg: url('https://cdn.modrinth.com/landing-new/landing-lower-light.webp');
|
||||
--landing-maze-outer-bg: linear-gradient(180deg, #f0f0f0 0%, #ffffff 100%);
|
||||
|
||||
--landing-color-heading: #000;
|
||||
@ -259,10 +259,10 @@ html {
|
||||
|
||||
--shadow-card: rgba(0, 0, 0, 0.25) 0px 2px 4px 0px;
|
||||
|
||||
--landing-maze-bg: url("https://cdn.modrinth.com/landing-new/landing.webp");
|
||||
--landing-maze-bg: url('https://cdn.modrinth.com/landing-new/landing.webp');
|
||||
--landing-maze-gradient-bg:
|
||||
linear-gradient(0deg, #31375f 0%, rgba(8, 14, 55, 0) 100%),
|
||||
url("https://cdn.modrinth.com/landing-new/landing-lower.webp");
|
||||
url('https://cdn.modrinth.com/landing-new/landing-lower.webp');
|
||||
--landing-maze-outer-bg: linear-gradient(180deg, #06060d 0%, #000000 100%);
|
||||
|
||||
--landing-color-heading: #fff;
|
||||
@ -492,7 +492,7 @@ textarea {
|
||||
}
|
||||
|
||||
&:disabled,
|
||||
&[disabled="true"] {
|
||||
&[disabled='true'] {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
@ -509,7 +509,7 @@ textarea {
|
||||
}
|
||||
|
||||
button,
|
||||
input[type="button"] {
|
||||
input[type='button'] {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
outline: 2px solid transparent;
|
||||
@ -525,13 +525,13 @@ kbd {
|
||||
font-size: 0.85em !important;
|
||||
}
|
||||
|
||||
@import "~/assets/styles/layout.scss";
|
||||
@import "~/assets/styles/utils.scss";
|
||||
@import "~/assets/styles/components.scss";
|
||||
@import '~/assets/styles/layout.scss';
|
||||
@import '~/assets/styles/utils.scss';
|
||||
@import '~/assets/styles/components.scss';
|
||||
|
||||
button:focus-visible,
|
||||
a:focus-visible,
|
||||
[tabindex="0"]:focus-visible {
|
||||
[tabindex='0']:focus-visible {
|
||||
outline: 0.25rem solid #ea80ff;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
@ -42,9 +42,9 @@
|
||||
padding: 0 1.5rem;
|
||||
|
||||
grid-template:
|
||||
"sidebar"
|
||||
"content"
|
||||
"info"
|
||||
'sidebar'
|
||||
'content'
|
||||
'info'
|
||||
/ 100%;
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
@ -81,25 +81,25 @@
|
||||
column-gap: 0.75rem;
|
||||
|
||||
grid-template:
|
||||
"sidebar content" auto
|
||||
"info content" auto
|
||||
"dummy content" 1fr
|
||||
'sidebar content' auto
|
||||
'info content' auto
|
||||
'dummy content' 1fr
|
||||
/ 18.75rem 1fr;
|
||||
|
||||
&.alt-layout {
|
||||
grid-template:
|
||||
"content sidebar" auto
|
||||
"content info" auto
|
||||
"content dummy" 1fr
|
||||
'content sidebar' auto
|
||||
'content info' auto
|
||||
'content dummy' 1fr
|
||||
/ 1fr 18.75rem;
|
||||
}
|
||||
|
||||
&.no-sidebar {
|
||||
grid-template:
|
||||
"header header" auto
|
||||
"content content" auto
|
||||
"info info" auto
|
||||
"dummy dummy" 1fr
|
||||
'header header' auto
|
||||
'content content' auto
|
||||
'info info' auto
|
||||
'dummy dummy' 1fr
|
||||
/ 1fr 1fr;
|
||||
|
||||
.normal-page__content {
|
||||
@ -129,9 +129,9 @@
|
||||
padding-bottom: 1.5rem;
|
||||
|
||||
grid-template:
|
||||
"header"
|
||||
"content"
|
||||
"sidebar"
|
||||
'header'
|
||||
'content'
|
||||
'sidebar'
|
||||
/ 100%;
|
||||
|
||||
.normal-page__ultimate-sidebar {
|
||||
@ -152,16 +152,16 @@
|
||||
@media screen and (min-width: 1024px) {
|
||||
&.sidebar {
|
||||
grid-template:
|
||||
"header header" auto
|
||||
"content sidebar" auto
|
||||
"content dummy" 1fr
|
||||
'header header' auto
|
||||
'content sidebar' auto
|
||||
'content dummy' 1fr
|
||||
/ 1fr 18.75rem;
|
||||
|
||||
&.alt-layout {
|
||||
grid-template:
|
||||
"header header" auto
|
||||
"sidebar content" auto
|
||||
"dummy content" 1fr
|
||||
'header header' auto
|
||||
'sidebar content' auto
|
||||
'dummy content' 1fr
|
||||
/ 18.75rem 1fr;
|
||||
}
|
||||
}
|
||||
@ -177,9 +177,9 @@
|
||||
max-width: calc(80rem + 0.75rem + 600px);
|
||||
|
||||
grid-template:
|
||||
"header header ultimate-sidebar" auto
|
||||
"content sidebar ultimate-sidebar" auto
|
||||
"content dummy ultimate-sidebar" 1fr
|
||||
'header header ultimate-sidebar' auto
|
||||
'content sidebar ultimate-sidebar' auto
|
||||
'content dummy ultimate-sidebar' 1fr
|
||||
/ 1fr 18.75rem auto;
|
||||
|
||||
.normal-page__header {
|
||||
@ -203,9 +203,9 @@
|
||||
|
||||
&.alt-layout {
|
||||
grid-template:
|
||||
"ultimate-sidebar header header" auto
|
||||
"ultimate-sidebar sidebar content" auto
|
||||
"ultimate-sidebar dummy content" 1fr
|
||||
'ultimate-sidebar header header' auto
|
||||
'ultimate-sidebar sidebar content' auto
|
||||
'ultimate-sidebar dummy content' 1fr
|
||||
/ auto 18.75rem 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,21 +54,21 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const loading = useLoading();
|
||||
const loading = useLoading()
|
||||
|
||||
const config = useRuntimeConfig();
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const api = computed(() => {
|
||||
const apiUrl = config.public.apiBaseUrl;
|
||||
if (apiUrl.startsWith("https://api.modrinth.com")) {
|
||||
return "prod";
|
||||
} else if (apiUrl.startsWith("https://staging-api.modrinth.com")) {
|
||||
return "staging";
|
||||
} else if (apiUrl.startsWith("localhost") || apiUrl.startsWith("127.0.0.1")) {
|
||||
return "localhost";
|
||||
const apiUrl = config.public.apiBaseUrl
|
||||
if (apiUrl.startsWith('https://api.modrinth.com')) {
|
||||
return 'prod'
|
||||
} else if (apiUrl.startsWith('https://staging-api.modrinth.com')) {
|
||||
return 'staging'
|
||||
} else if (apiUrl.startsWith('localhost') || apiUrl.startsWith('127.0.0.1')) {
|
||||
return 'localhost'
|
||||
}
|
||||
return "foreign";
|
||||
});
|
||||
return 'foreign'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -19,43 +19,43 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DropdownIcon } from "@modrinth/assets";
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import { DropdownIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled } from '@modrinth/ui'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
openByDefault?: boolean;
|
||||
type?: "standard" | "outlined" | "transparent";
|
||||
openByDefault?: boolean
|
||||
type?: 'standard' | 'outlined' | 'transparent'
|
||||
}>(),
|
||||
{
|
||||
type: "standard",
|
||||
type: 'standard',
|
||||
openByDefault: false,
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
const isOpen = ref(props.openByDefault);
|
||||
const emit = defineEmits(["onOpen", "onClose"]);
|
||||
const isOpen = ref(props.openByDefault)
|
||||
const emit = defineEmits(['onOpen', 'onClose'])
|
||||
|
||||
const slots = useSlots();
|
||||
const slots = useSlots()
|
||||
|
||||
function open() {
|
||||
isOpen.value = true;
|
||||
emit("onOpen");
|
||||
isOpen.value = true
|
||||
emit('onOpen')
|
||||
}
|
||||
function close() {
|
||||
isOpen.value = false;
|
||||
emit("onClose");
|
||||
isOpen.value = false
|
||||
emit('onClose')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open,
|
||||
close,
|
||||
isOpen,
|
||||
});
|
||||
})
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.accordion-content {
|
||||
|
||||
@ -31,53 +31,53 @@ useHead({
|
||||
// },
|
||||
{
|
||||
// Aditude
|
||||
src: "https://dn0qt3r0xannq.cloudfront.net/modrinth-7JfmkEIXEp/modrinth-longform/prebid-load.js",
|
||||
src: 'https://dn0qt3r0xannq.cloudfront.net/modrinth-7JfmkEIXEp/modrinth-longform/prebid-load.js',
|
||||
async: true,
|
||||
},
|
||||
{
|
||||
// Optima
|
||||
src: "https://bservr.com/o.js?uid=8118d1fdb2e0d6f32180bd27",
|
||||
src: 'https://bservr.com/o.js?uid=8118d1fdb2e0d6f32180bd27',
|
||||
async: true,
|
||||
},
|
||||
{
|
||||
src: "/inmobi.js",
|
||||
src: '/inmobi.js',
|
||||
async: true,
|
||||
},
|
||||
],
|
||||
link: [
|
||||
{
|
||||
rel: "preload",
|
||||
as: "script",
|
||||
href: "https://www.googletagservices.com/tag/js/gpt.js",
|
||||
rel: 'preload',
|
||||
as: 'script',
|
||||
href: 'https://www.googletagservices.com/tag/js/gpt.js',
|
||||
},
|
||||
],
|
||||
});
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
window.tude = window.tude || { cmd: [] };
|
||||
window.Raven = window.Raven || { cmd: [] };
|
||||
window.tude = window.tude || { cmd: [] }
|
||||
window.Raven = window.Raven || { cmd: [] }
|
||||
|
||||
window.Raven.cmd.push(({ config }) => {
|
||||
config.setCustom({
|
||||
param1: "web",
|
||||
});
|
||||
});
|
||||
param1: 'web',
|
||||
})
|
||||
})
|
||||
|
||||
tude.cmd.push(function () {
|
||||
tude.refreshAdsViaDivMappings([
|
||||
{
|
||||
divId: "modrinth-rail-1",
|
||||
baseDivId: "pb-slot-square-2",
|
||||
divId: 'modrinth-rail-1',
|
||||
baseDivId: 'pb-slot-square-2',
|
||||
targeting: {
|
||||
location: "web",
|
||||
location: 'web',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
])
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
iframe[id^="google_ads_iframe"] {
|
||||
iframe[id^='google_ads_iframe'] {
|
||||
color-scheme: normal;
|
||||
background: transparent;
|
||||
}
|
||||
@ -96,21 +96,21 @@ iframe[id^="google_ads_iframe"] {
|
||||
background: var(--color-raised-bg);
|
||||
}
|
||||
|
||||
#qc-cmp2-ui button[mode="primary"] {
|
||||
#qc-cmp2-ui button[mode='primary'] {
|
||||
background: var(--color-brand);
|
||||
color: var(--color-accent-contrast);
|
||||
border-radius: var(--radius-lg);
|
||||
border: none;
|
||||
}
|
||||
|
||||
#qc-cmp2-ui button[mode="secondary"] {
|
||||
#qc-cmp2-ui button[mode='secondary'] {
|
||||
background: var(--color-button-bg);
|
||||
color: var(--color-base);
|
||||
border-radius: var(--radius-lg);
|
||||
border: none;
|
||||
}
|
||||
|
||||
#qc-cmp2-ui button[mode="link"] {
|
||||
#qc-cmp2-ui button[mode='link'] {
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
@ -129,7 +129,7 @@ iframe[id^="google_ads_iframe"] {
|
||||
font-family: var(--font-standard);
|
||||
}
|
||||
|
||||
#qc-cmp2-ui .qc-cmp2-toggle[aria-checked="true"] {
|
||||
#qc-cmp2-ui .qc-cmp2-toggle[aria-checked='true'] {
|
||||
background-color: var(--color-brand);
|
||||
border: 1px solid var(--color-brand);
|
||||
}
|
||||
|
||||
@ -13,35 +13,35 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
})
|
||||
|
||||
const slotContainer = ref();
|
||||
const slotContainer = ref()
|
||||
|
||||
const hasContent = ref(false);
|
||||
const hasContent = ref(false)
|
||||
|
||||
const mutationObserver = ref<MutationObserver | null>(null);
|
||||
const mutationObserver = ref<MutationObserver | null>(null)
|
||||
|
||||
function updateContent() {
|
||||
if (!slotContainer.value) return false;
|
||||
if (!slotContainer.value) return false
|
||||
|
||||
hasContent.value = slotContainer.value ? slotContainer.value.children.length > 0 : false;
|
||||
hasContent.value = slotContainer.value ? slotContainer.value.children.length > 0 : false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
mutationObserver.value = new MutationObserver(updateContent);
|
||||
mutationObserver.value = new MutationObserver(updateContent)
|
||||
|
||||
mutationObserver.value.observe(slotContainer.value, {
|
||||
childList: true,
|
||||
});
|
||||
})
|
||||
|
||||
updateContent();
|
||||
});
|
||||
updateContent()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (mutationObserver.value) {
|
||||
mutationObserver.value.disconnect();
|
||||
mutationObserver.value.disconnect()
|
||||
}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.accordion-content {
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ChevronRightIcon } from "@modrinth/assets";
|
||||
import { ChevronRightIcon } from '@modrinth/assets'
|
||||
|
||||
defineProps({
|
||||
linkStack: {
|
||||
@ -26,7 +26,7 @@ defineProps({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { CheckIcon, DropdownIcon } from "@modrinth/assets";
|
||||
import { CheckIcon, DropdownIcon } from '@modrinth/assets'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -36,7 +36,7 @@ export default {
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
@ -60,15 +60,15 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
emits: ['update:modelValue'],
|
||||
methods: {
|
||||
toggle() {
|
||||
if (!this.disabled) {
|
||||
this.$emit("update:modelValue", !this.modelValue);
|
||||
this.$emit('update:modelValue', !this.modelValue)
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { CheckIcon } from "@modrinth/assets";
|
||||
import { CheckIcon } from '@modrinth/assets'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -42,32 +42,32 @@ export default {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
emits: ['update:modelValue'],
|
||||
computed: {
|
||||
selected: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
return this.modelValue
|
||||
},
|
||||
set(value) {
|
||||
this.$emit("update:modelValue", value);
|
||||
this.$emit('update:modelValue', value)
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.items.length > 0 && this.neverEmpty) {
|
||||
this.selected = this.items[0];
|
||||
this.selected = this.items[0]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleItem(item) {
|
||||
if (this.selected === item && !this.neverEmpty) {
|
||||
this.selected = null;
|
||||
this.selected = null
|
||||
} else {
|
||||
this.selected = item;
|
||||
this.selected = item
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -28,8 +28,8 @@
|
||||
</div>
|
||||
<p class="m-0 max-w-[30rem]">
|
||||
Your new collection will be created as a public collection with
|
||||
{{ projectIds.length > 0 ? projectIds.length : "no" }}
|
||||
{{ projectIds.length !== 1 ? "projects" : "project" }}.
|
||||
{{ projectIds.length > 0 ? projectIds.length : 'no' }}
|
||||
{{ projectIds.length !== 1 ? 'projects' : 'project' }}.
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled color="brand">
|
||||
@ -49,61 +49,61 @@
|
||||
</NewModal>
|
||||
</template>
|
||||
<script setup>
|
||||
import { PlusIcon, XIcon } from "@modrinth/assets";
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { PlusIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, NewModal } from '@modrinth/ui'
|
||||
|
||||
const router = useNativeRouter();
|
||||
const router = useNativeRouter()
|
||||
|
||||
const name = ref("");
|
||||
const description = ref("");
|
||||
const name = ref('')
|
||||
const description = ref('')
|
||||
|
||||
const modal = ref();
|
||||
const modal = ref()
|
||||
|
||||
const props = defineProps({
|
||||
projectIds: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
return []
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
async function create() {
|
||||
startLoading();
|
||||
startLoading()
|
||||
try {
|
||||
const result = await useBaseFetch("collection", {
|
||||
method: "POST",
|
||||
const result = await useBaseFetch('collection', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
name: name.value.trim(),
|
||||
description: description.value.trim() || undefined,
|
||||
projects: props.projectIds,
|
||||
},
|
||||
apiVersion: 3,
|
||||
});
|
||||
})
|
||||
|
||||
await initUserCollections();
|
||||
await initUserCollections()
|
||||
|
||||
modal.value.hide();
|
||||
await router.push(`/collection/${result.id}`);
|
||||
modal.value.hide()
|
||||
await router.push(`/collection/${result.id}`)
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
group: "main",
|
||||
title: "An error occurred",
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err?.data?.description || err?.message || err,
|
||||
type: "error",
|
||||
});
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading();
|
||||
stopLoading()
|
||||
}
|
||||
function show(event) {
|
||||
name.value = "";
|
||||
description.value = "";
|
||||
modal.value.show(event);
|
||||
name.value = ''
|
||||
description.value = ''
|
||||
modal.value.show(event)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@ -17,5 +17,5 @@ defineProps({
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -4,10 +4,10 @@
|
||||
class="drop-area"
|
||||
@drop.stop.prevent="
|
||||
(event) => {
|
||||
$refs.drop_area.style.visibility = 'hidden';
|
||||
$refs.drop_area.style.visibility = 'hidden'
|
||||
|
||||
if (event.dataTransfer && event.dataTransfer.files && fileAllowed) {
|
||||
$emit('change', event.dataTransfer.files);
|
||||
$emit('change', event.dataTransfer.files)
|
||||
}
|
||||
}
|
||||
"
|
||||
@ -22,45 +22,45 @@ export default {
|
||||
props: {
|
||||
accept: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ["change"],
|
||||
emits: ['change'],
|
||||
data() {
|
||||
return {
|
||||
fileAllowed: false,
|
||||
};
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener("dragenter", this.allowDrag);
|
||||
document.addEventListener('dragenter', this.allowDrag)
|
||||
},
|
||||
methods: {
|
||||
allowDrag(event) {
|
||||
const file = event.dataTransfer?.items[0];
|
||||
const file = event.dataTransfer?.items[0]
|
||||
|
||||
if (
|
||||
file &&
|
||||
this.accept
|
||||
.split(",")
|
||||
.reduce((acc, t) => acc || file.type.startsWith(t) || file.type === t || t === "*", false)
|
||||
.split(',')
|
||||
.reduce((acc, t) => acc || file.type.startsWith(t) || file.type === t || t === '*', false)
|
||||
) {
|
||||
this.fileAllowed = true;
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
event.preventDefault();
|
||||
this.fileAllowed = true
|
||||
event.dataTransfer.dropEffect = 'copy'
|
||||
event.preventDefault()
|
||||
|
||||
if (this.$refs.drop_area) {
|
||||
this.$refs.drop_area.style.visibility = "visible";
|
||||
this.$refs.drop_area.style.visibility = 'visible'
|
||||
}
|
||||
} else {
|
||||
this.fileAllowed = false;
|
||||
this.fileAllowed = false
|
||||
|
||||
if (this.$refs.drop_area) {
|
||||
this.$refs.drop_area.style.visibility = "hidden";
|
||||
this.$refs.drop_area.style.visibility = 'hidden'
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@ -81,7 +81,7 @@ export default {
|
||||
&::before {
|
||||
--indent: 4rem;
|
||||
|
||||
content: " ";
|
||||
content: ' ';
|
||||
position: relative;
|
||||
top: var(--indent);
|
||||
left: var(--indent);
|
||||
|
||||
@ -49,22 +49,22 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ClientIcon, GlobeIcon, InfoIcon, ServerIcon } from "@modrinth/assets";
|
||||
import { ClientIcon, GlobeIcon, InfoIcon, ServerIcon } from '@modrinth/assets'
|
||||
|
||||
defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: "mod",
|
||||
default: 'mod',
|
||||
},
|
||||
serverSide: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
clientSide: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
typeOnly: {
|
||||
type: Boolean,
|
||||
@ -85,12 +85,12 @@ defineProps({
|
||||
type: Array,
|
||||
required: false,
|
||||
default() {
|
||||
return [];
|
||||
return []
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const tags = useTags();
|
||||
const tags = useTags()
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.environment {
|
||||
|
||||
@ -18,14 +18,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { fileIsValid } from "~/helpers/fileUtils.js";
|
||||
import { fileIsValid } from '~/helpers/fileUtils.js'
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
props: {
|
||||
prompt: {
|
||||
type: String,
|
||||
default: "Select file",
|
||||
default: 'Select file',
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
@ -59,33 +59,33 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["change"],
|
||||
emits: ['change'],
|
||||
data() {
|
||||
return {
|
||||
files: [],
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addFiles(files, shouldNotReset) {
|
||||
if (!shouldNotReset || this.shouldAlwaysReset) {
|
||||
this.files = files;
|
||||
this.files = files
|
||||
}
|
||||
|
||||
const validationOptions = { maxSize: this.maxSize, alertOnInvalid: true };
|
||||
this.files = [...this.files].filter((file) => fileIsValid(file, validationOptions));
|
||||
const validationOptions = { maxSize: this.maxSize, alertOnInvalid: true }
|
||||
this.files = [...this.files].filter((file) => fileIsValid(file, validationOptions))
|
||||
|
||||
if (this.files.length > 0) {
|
||||
this.$emit("change", this.files);
|
||||
this.$emit('change', this.files)
|
||||
}
|
||||
},
|
||||
handleDrop(e) {
|
||||
this.addFiles(e.dataTransfer.files);
|
||||
this.addFiles(e.dataTransfer.files)
|
||||
},
|
||||
handleChange(e) {
|
||||
this.addFiles(e.target.files);
|
||||
this.addFiles(e.target.files)
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -1,40 +1,40 @@
|
||||
<script setup>
|
||||
const token = defineModel();
|
||||
const id = ref(null);
|
||||
const token = defineModel()
|
||||
const id = ref(null)
|
||||
|
||||
function hCaptchaUpdateToken(newToken) {
|
||||
token.value = newToken;
|
||||
token.value = newToken
|
||||
}
|
||||
|
||||
function hCaptchaReady() {
|
||||
window.hCaptchaUpdateToken = hCaptchaUpdateToken;
|
||||
id.value = window.hcaptcha.render("h-captcha");
|
||||
window.hCaptchaUpdateToken = hCaptchaUpdateToken
|
||||
id.value = window.hcaptcha.render('h-captcha')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (window.hcaptcha) {
|
||||
hCaptchaReady();
|
||||
hCaptchaReady()
|
||||
} else {
|
||||
window.hCaptchaReady = hCaptchaReady;
|
||||
window.hCaptchaReady = hCaptchaReady
|
||||
|
||||
useHead({
|
||||
script: [
|
||||
{
|
||||
src: "https://js.hcaptcha.com/1/api.js?render=explicit&onload=hCaptchaReady",
|
||||
src: 'https://js.hcaptcha.com/1/api.js?render=explicit&onload=hCaptchaReady',
|
||||
async: true,
|
||||
defer: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
})
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
reset: () => {
|
||||
token.value = null;
|
||||
window.hcaptcha.reset(id.value);
|
||||
token.value = null
|
||||
window.hcaptcha.reset(id.value)
|
||||
},
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@ -7,12 +7,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
type MessageType = "information" | "warning";
|
||||
type MessageType = 'information' | 'warning'
|
||||
const props = withDefaults(defineProps<{ messageType?: MessageType }>(), {
|
||||
messageType: "information",
|
||||
});
|
||||
const cardClassByType = computed(() => `message-banner__content_${props.messageType}`);
|
||||
const ariaLabelByType = computed(() => `Banner with ${props.messageType} message`);
|
||||
messageType: 'information',
|
||||
})
|
||||
const cardClassByType = computed(() => `message-banner__content_${props.messageType}`)
|
||||
const ariaLabelByType = computed(() => `Banner with ${props.messageType} message`)
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { XIcon } from "@modrinth/assets";
|
||||
import { XIcon } from '@modrinth/assets'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -39,31 +39,31 @@ export default {
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const cosmetics = useCosmetics();
|
||||
const cosmetics = useCosmetics()
|
||||
|
||||
return { cosmetics };
|
||||
return { cosmetics }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
shown: false,
|
||||
actuallyShown: false,
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.shown = true;
|
||||
this.shown = true
|
||||
setTimeout(() => {
|
||||
this.actuallyShown = true;
|
||||
}, 50);
|
||||
this.actuallyShown = true
|
||||
}, 50)
|
||||
},
|
||||
hide() {
|
||||
this.actuallyShown = false;
|
||||
this.actuallyShown = false
|
||||
setTimeout(() => {
|
||||
this.shown = false;
|
||||
}, 300);
|
||||
this.shown = false
|
||||
}, 300)
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -84,11 +84,11 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { PlusIcon,XIcon } from "@modrinth/assets";
|
||||
import { ButtonStyled, DropdownSelect,NewModal } from "@modrinth/ui";
|
||||
import { PlusIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, DropdownSelect, NewModal } from '@modrinth/ui'
|
||||
|
||||
const router = useRouter();
|
||||
const app = useNuxtApp();
|
||||
const router = useRouter()
|
||||
const app = useNuxtApp()
|
||||
|
||||
const props = defineProps({
|
||||
organizationId: {
|
||||
@ -96,120 +96,120 @@ const props = defineProps({
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const modal = ref();
|
||||
const modal = ref()
|
||||
|
||||
const name = ref("");
|
||||
const slug = ref("");
|
||||
const description = ref("");
|
||||
const manualSlug = ref(false);
|
||||
const name = ref('')
|
||||
const slug = ref('')
|
||||
const description = ref('')
|
||||
const manualSlug = ref(false)
|
||||
const visibilities = ref([
|
||||
{
|
||||
actual: "approved",
|
||||
display: "Public",
|
||||
actual: 'approved',
|
||||
display: 'Public',
|
||||
},
|
||||
{
|
||||
actual: "unlisted",
|
||||
display: "Unlisted",
|
||||
actual: 'unlisted',
|
||||
display: 'Unlisted',
|
||||
},
|
||||
{
|
||||
actual: "private",
|
||||
display: "Private",
|
||||
actual: 'private',
|
||||
display: 'Private',
|
||||
},
|
||||
]);
|
||||
])
|
||||
const visibility = ref({
|
||||
actual: "approved",
|
||||
display: "Public",
|
||||
});
|
||||
actual: 'approved',
|
||||
display: 'Public',
|
||||
})
|
||||
|
||||
const cancel = () => {
|
||||
modal.value.hide();
|
||||
};
|
||||
modal.value.hide()
|
||||
}
|
||||
|
||||
async function createProject() {
|
||||
startLoading();
|
||||
startLoading()
|
||||
|
||||
const formData = new FormData();
|
||||
const formData = new FormData()
|
||||
|
||||
const auth = await useAuth();
|
||||
const auth = await useAuth()
|
||||
|
||||
const projectData = {
|
||||
title: name.value.trim(),
|
||||
project_type: "mod",
|
||||
project_type: 'mod',
|
||||
slug: slug.value,
|
||||
description: description.value.trim(),
|
||||
body: "",
|
||||
body: '',
|
||||
requested_status: visibility.value.actual,
|
||||
initial_versions: [],
|
||||
team_members: [
|
||||
{
|
||||
user_id: auth.value.user.id,
|
||||
name: auth.value.user.username,
|
||||
role: "Owner",
|
||||
role: 'Owner',
|
||||
},
|
||||
],
|
||||
categories: [],
|
||||
client_side: "required",
|
||||
server_side: "required",
|
||||
license_id: "LicenseRef-Unknown",
|
||||
client_side: 'required',
|
||||
server_side: 'required',
|
||||
license_id: 'LicenseRef-Unknown',
|
||||
is_draft: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (props.organizationId) {
|
||||
projectData.organization_id = props.organizationId;
|
||||
projectData.organization_id = props.organizationId
|
||||
}
|
||||
|
||||
formData.append("data", JSON.stringify(projectData));
|
||||
formData.append('data', JSON.stringify(projectData))
|
||||
|
||||
try {
|
||||
await useBaseFetch("project", {
|
||||
method: "POST",
|
||||
await useBaseFetch('project', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
"Content-Disposition": formData,
|
||||
'Content-Disposition': formData,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
modal.value.hide();
|
||||
modal.value.hide()
|
||||
await router.push({
|
||||
name: "type-id",
|
||||
name: 'type-id',
|
||||
params: {
|
||||
type: "project",
|
||||
type: 'project',
|
||||
id: slug.value,
|
||||
},
|
||||
});
|
||||
})
|
||||
} catch (err) {
|
||||
app.$notify({
|
||||
group: "main",
|
||||
title: "An error occurred",
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data ? err.data.description : err,
|
||||
type: "error",
|
||||
});
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading();
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
function show(event) {
|
||||
name.value = "";
|
||||
slug.value = "";
|
||||
description.value = "";
|
||||
manualSlug.value = false;
|
||||
modal.value.show(event);
|
||||
name.value = ''
|
||||
slug.value = ''
|
||||
description.value = ''
|
||||
manualSlug.value = false
|
||||
modal.value.show(event)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
});
|
||||
})
|
||||
|
||||
function updatedName() {
|
||||
if (!manualSlug.value) {
|
||||
slug.value = name.value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replaceAll(" ", "-")
|
||||
.replaceAll(/[^a-zA-Z0-9!@$()`.+,_"-]/g, "")
|
||||
.replaceAll(/--+/gm, "-");
|
||||
.replaceAll(' ', '-')
|
||||
.replaceAll(/[^a-zA-Z0-9!@$()`.+,_"-]/g, '')
|
||||
.replaceAll(/--+/gm, '-')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const route = useNativeRoute();
|
||||
const route = useNativeRoute()
|
||||
|
||||
const props = defineProps({
|
||||
links: {
|
||||
@ -35,59 +35,59 @@ const props = defineProps({
|
||||
default: null,
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const sliderPositionX = ref(0);
|
||||
const sliderPositionY = ref(18);
|
||||
const selectedElementWidth = ref(0);
|
||||
const activeIndex = ref(-1);
|
||||
const oldIndex = ref(-1);
|
||||
const sliderPositionX = ref(0)
|
||||
const sliderPositionY = ref(18)
|
||||
const selectedElementWidth = ref(0)
|
||||
const activeIndex = ref(-1)
|
||||
const oldIndex = ref(-1)
|
||||
|
||||
const filteredLinks = computed(() =>
|
||||
props.links.filter((x) => (x.shown === undefined ? true : x.shown)),
|
||||
);
|
||||
const positionToMoveX = computed(() => `${sliderPositionX.value}px`);
|
||||
const positionToMoveY = computed(() => `${sliderPositionY.value}px`);
|
||||
const sliderWidth = computed(() => `${selectedElementWidth.value}px`);
|
||||
)
|
||||
const positionToMoveX = computed(() => `${sliderPositionX.value}px`)
|
||||
const positionToMoveY = computed(() => `${sliderPositionY.value}px`)
|
||||
const sliderWidth = computed(() => `${selectedElementWidth.value}px`)
|
||||
|
||||
function pickLink() {
|
||||
activeIndex.value = props.query
|
||||
? filteredLinks.value.findIndex(
|
||||
(x) => (x.href === "" ? undefined : x.href) === route.path[props.query],
|
||||
(x) => (x.href === '' ? undefined : x.href) === route.path[props.query],
|
||||
)
|
||||
: filteredLinks.value.findIndex((x) => x.href === decodeURIComponent(route.path));
|
||||
: filteredLinks.value.findIndex((x) => x.href === decodeURIComponent(route.path))
|
||||
|
||||
if (activeIndex.value !== -1) {
|
||||
startAnimation();
|
||||
startAnimation()
|
||||
} else {
|
||||
oldIndex.value = -1;
|
||||
sliderPositionX.value = 0;
|
||||
selectedElementWidth.value = 0;
|
||||
oldIndex.value = -1
|
||||
sliderPositionX.value = 0
|
||||
selectedElementWidth.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
const rowLinkElements = ref();
|
||||
const rowLinkElements = ref()
|
||||
|
||||
function startAnimation() {
|
||||
const el = rowLinkElements.value[activeIndex.value].$el;
|
||||
const el = rowLinkElements.value[activeIndex.value].$el
|
||||
|
||||
if (!el || !el.offsetParent) return;
|
||||
if (!el || !el.offsetParent) return
|
||||
|
||||
sliderPositionX.value = el.offsetLeft;
|
||||
sliderPositionY.value = el.offsetTop + el.offsetHeight;
|
||||
selectedElementWidth.value = el.offsetWidth;
|
||||
sliderPositionX.value = el.offsetLeft
|
||||
sliderPositionY.value = el.offsetTop + el.offsetHeight
|
||||
selectedElementWidth.value = el.offsetWidth
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("resize", pickLink);
|
||||
pickLink();
|
||||
});
|
||||
window.addEventListener('resize', pickLink)
|
||||
pickLink()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("resize", pickLink);
|
||||
});
|
||||
window.removeEventListener('resize', pickLink)
|
||||
})
|
||||
|
||||
watch(route, () => pickLink());
|
||||
watch(route, () => pickLink())
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {};
|
||||
export default {}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ChevronRightIcon } from "@modrinth/assets";
|
||||
import { ChevronRightIcon } from '@modrinth/assets'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -55,7 +55,7 @@ export default {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -35,129 +35,129 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted,ref, watch } from "vue";
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const route = useNativeRoute();
|
||||
const route = useNativeRoute()
|
||||
|
||||
interface Tab {
|
||||
label: string;
|
||||
href: string;
|
||||
shown?: boolean;
|
||||
icon?: string;
|
||||
subpages?: string[];
|
||||
label: string
|
||||
href: string
|
||||
shown?: boolean
|
||||
icon?: string
|
||||
subpages?: string[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
links: Tab[];
|
||||
query?: string;
|
||||
}>();
|
||||
links: Tab[]
|
||||
query?: string
|
||||
}>()
|
||||
|
||||
const scrollContainer = ref<HTMLElement | null>(null);
|
||||
const scrollContainer = ref<HTMLElement | null>(null)
|
||||
|
||||
const sliderLeft = ref(4);
|
||||
const sliderTop = ref(4);
|
||||
const sliderRight = ref(4);
|
||||
const sliderBottom = ref(4);
|
||||
const activeIndex = ref(-1);
|
||||
const subpageSelected = ref(false);
|
||||
const sliderLeft = ref(4)
|
||||
const sliderTop = ref(4)
|
||||
const sliderRight = ref(4)
|
||||
const sliderBottom = ref(4)
|
||||
const activeIndex = ref(-1)
|
||||
const subpageSelected = ref(false)
|
||||
|
||||
const filteredLinks = computed(() =>
|
||||
props.links.filter((x) => (x.shown === undefined ? true : x.shown)),
|
||||
);
|
||||
const sliderLeftPx = computed(() => `${sliderLeft.value}px`);
|
||||
const sliderTopPx = computed(() => `${sliderTop.value}px`);
|
||||
const sliderRightPx = computed(() => `${sliderRight.value}px`);
|
||||
const sliderBottomPx = computed(() => `${sliderBottom.value}px`);
|
||||
)
|
||||
const sliderLeftPx = computed(() => `${sliderLeft.value}px`)
|
||||
const sliderTopPx = computed(() => `${sliderTop.value}px`)
|
||||
const sliderRightPx = computed(() => `${sliderRight.value}px`)
|
||||
const sliderBottomPx = computed(() => `${sliderBottom.value}px`)
|
||||
|
||||
const tabLinkElements = ref();
|
||||
const tabLinkElements = ref()
|
||||
|
||||
function pickLink() {
|
||||
let index = -1;
|
||||
subpageSelected.value = false;
|
||||
let index = -1
|
||||
subpageSelected.value = false
|
||||
for (let i = filteredLinks.value.length - 1; i >= 0; i--) {
|
||||
const link = filteredLinks.value[i];
|
||||
const link = filteredLinks.value[i]
|
||||
if (props.query) {
|
||||
if (route.query[props.query] === link.href || (!route.query[props.query] && !link.href)) {
|
||||
index = i;
|
||||
break;
|
||||
index = i
|
||||
break
|
||||
}
|
||||
} else if (decodeURIComponent(route.path) === link.href) {
|
||||
index = i;
|
||||
break;
|
||||
index = i
|
||||
break
|
||||
} else if (
|
||||
decodeURIComponent(route.path).includes(link.href) ||
|
||||
(link.subpages &&
|
||||
link.subpages.some((subpage) => decodeURIComponent(route.path).includes(subpage)))
|
||||
) {
|
||||
index = i;
|
||||
subpageSelected.value = true;
|
||||
break;
|
||||
index = i
|
||||
subpageSelected.value = true
|
||||
break
|
||||
}
|
||||
}
|
||||
activeIndex.value = index;
|
||||
activeIndex.value = index
|
||||
|
||||
if (activeIndex.value !== -1) {
|
||||
startAnimation();
|
||||
startAnimation()
|
||||
} else {
|
||||
sliderLeft.value = 0;
|
||||
sliderRight.value = 0;
|
||||
sliderLeft.value = 0
|
||||
sliderRight.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
function startAnimation() {
|
||||
const el = tabLinkElements.value[activeIndex.value]?.$el;
|
||||
const el = tabLinkElements.value[activeIndex.value]?.$el
|
||||
|
||||
if (!el || !el.offsetParent) return;
|
||||
if (!el || !el.offsetParent) return
|
||||
|
||||
const newValues = {
|
||||
left: el.offsetLeft,
|
||||
top: el.offsetTop,
|
||||
right: el.offsetParent.offsetWidth - el.offsetLeft - el.offsetWidth,
|
||||
bottom: el.offsetParent.offsetHeight - el.offsetTop - el.offsetHeight,
|
||||
};
|
||||
}
|
||||
|
||||
if (sliderLeft.value === 4 && sliderRight.value === 4) {
|
||||
sliderLeft.value = newValues.left;
|
||||
sliderRight.value = newValues.right;
|
||||
sliderTop.value = newValues.top;
|
||||
sliderBottom.value = newValues.bottom;
|
||||
sliderLeft.value = newValues.left
|
||||
sliderRight.value = newValues.right
|
||||
sliderTop.value = newValues.top
|
||||
sliderBottom.value = newValues.bottom
|
||||
} else {
|
||||
const delay = 200;
|
||||
const delay = 200
|
||||
|
||||
if (newValues.left < sliderLeft.value) {
|
||||
sliderLeft.value = newValues.left;
|
||||
sliderLeft.value = newValues.left
|
||||
setTimeout(() => {
|
||||
sliderRight.value = newValues.right;
|
||||
}, delay);
|
||||
sliderRight.value = newValues.right
|
||||
}, delay)
|
||||
} else {
|
||||
sliderRight.value = newValues.right;
|
||||
sliderRight.value = newValues.right
|
||||
setTimeout(() => {
|
||||
sliderLeft.value = newValues.left;
|
||||
}, delay);
|
||||
sliderLeft.value = newValues.left
|
||||
}, delay)
|
||||
}
|
||||
|
||||
if (newValues.top < sliderTop.value) {
|
||||
sliderTop.value = newValues.top;
|
||||
sliderTop.value = newValues.top
|
||||
setTimeout(() => {
|
||||
sliderBottom.value = newValues.bottom;
|
||||
}, delay);
|
||||
sliderBottom.value = newValues.bottom
|
||||
}, delay)
|
||||
} else {
|
||||
sliderBottom.value = newValues.bottom;
|
||||
sliderBottom.value = newValues.bottom
|
||||
setTimeout(() => {
|
||||
sliderTop.value = newValues.top;
|
||||
}, delay);
|
||||
sliderTop.value = newValues.top
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
pickLink();
|
||||
});
|
||||
pickLink()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => [route.path, route.query],
|
||||
() => pickLink(),
|
||||
);
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -211,8 +211,8 @@
|
||||
class="iconified-button square-button brand-button button-transparent"
|
||||
@click="
|
||||
() => {
|
||||
acceptTeamInvite(notification.body.team_id);
|
||||
read();
|
||||
acceptTeamInvite(notification.body.team_id)
|
||||
read()
|
||||
}
|
||||
"
|
||||
>
|
||||
@ -223,8 +223,8 @@
|
||||
class="iconified-button square-button danger-button button-transparent"
|
||||
@click="
|
||||
() => {
|
||||
removeSelfFromTeam(notification.body.team_id);
|
||||
read();
|
||||
removeSelfFromTeam(notification.body.team_id)
|
||||
read()
|
||||
}
|
||||
"
|
||||
>
|
||||
@ -249,8 +249,8 @@
|
||||
class="iconified-button brand-button"
|
||||
@click="
|
||||
() => {
|
||||
acceptTeamInvite(notification.body.team_id);
|
||||
read();
|
||||
acceptTeamInvite(notification.body.team_id)
|
||||
read()
|
||||
}
|
||||
"
|
||||
>
|
||||
@ -261,8 +261,8 @@
|
||||
class="iconified-button danger-button"
|
||||
@click="
|
||||
() => {
|
||||
removeSelfFromTeam(notification.body.team_id);
|
||||
read();
|
||||
removeSelfFromTeam(notification.body.team_id)
|
||||
read()
|
||||
}
|
||||
"
|
||||
>
|
||||
@ -329,22 +329,22 @@ import {
|
||||
UserPlusIcon,
|
||||
VersionIcon,
|
||||
XIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { Avatar, CopyCode, ProjectStatusBadge, useRelativeTime } from "@modrinth/ui";
|
||||
import { renderString } from "@modrinth/utils";
|
||||
} from '@modrinth/assets'
|
||||
import { Avatar, CopyCode, ProjectStatusBadge, useRelativeTime } from '@modrinth/ui'
|
||||
import { renderString } from '@modrinth/utils'
|
||||
|
||||
import DoubleIcon from "~/components/ui/DoubleIcon.vue";
|
||||
import Categories from "~/components/ui/search/Categories.vue";
|
||||
import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
|
||||
import { markAsRead } from "~/helpers/notifications.ts";
|
||||
import { getProjectLink, getVersionLink } from "~/helpers/projects.js";
|
||||
import { acceptTeamInvite, removeSelfFromTeam } from "~/helpers/teams.js";
|
||||
import { getUserLink } from "~/helpers/users.js";
|
||||
import DoubleIcon from '~/components/ui/DoubleIcon.vue'
|
||||
import Categories from '~/components/ui/search/Categories.vue'
|
||||
import ThreadSummary from '~/components/ui/thread/ThreadSummary.vue'
|
||||
import { markAsRead } from '~/helpers/notifications.ts'
|
||||
import { getProjectLink, getVersionLink } from '~/helpers/projects.js'
|
||||
import { acceptTeamInvite, removeSelfFromTeam } from '~/helpers/teams.js'
|
||||
import { getUserLink } from '~/helpers/users.js'
|
||||
|
||||
const app = useNuxtApp();
|
||||
const emit = defineEmits(["update:notifications"]);
|
||||
const app = useNuxtApp()
|
||||
const emit = defineEmits(['update:notifications'])
|
||||
|
||||
const formatRelativeTime = useRelativeTime();
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
|
||||
const props = defineProps({
|
||||
notification: {
|
||||
@ -367,34 +367,34 @@ const props = defineProps({
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const flags = useFeatureFlags();
|
||||
const tags = useTags();
|
||||
const flags = useFeatureFlags()
|
||||
const tags = useTags()
|
||||
|
||||
const type = computed(() =>
|
||||
!props.notification.body || props.notification.body.type === "legacy_markdown"
|
||||
!props.notification.body || props.notification.body.type === 'legacy_markdown'
|
||||
? null
|
||||
: props.notification.body.type,
|
||||
);
|
||||
const thread = computed(() => props.notification.extra_data.thread);
|
||||
const report = computed(() => props.notification.extra_data.report);
|
||||
const project = computed(() => props.notification.extra_data.project);
|
||||
const version = computed(() => props.notification.extra_data.version);
|
||||
const user = computed(() => props.notification.extra_data.user);
|
||||
const organization = computed(() => props.notification.extra_data.organization);
|
||||
const invitedBy = computed(() => props.notification.extra_data.invited_by);
|
||||
)
|
||||
const thread = computed(() => props.notification.extra_data.thread)
|
||||
const report = computed(() => props.notification.extra_data.report)
|
||||
const project = computed(() => props.notification.extra_data.project)
|
||||
const version = computed(() => props.notification.extra_data.version)
|
||||
const user = computed(() => props.notification.extra_data.user)
|
||||
const organization = computed(() => props.notification.extra_data.organization)
|
||||
const invitedBy = computed(() => props.notification.extra_data.invited_by)
|
||||
|
||||
const threadLink = computed(() => {
|
||||
if (report.value) {
|
||||
return `/dashboard/report/${report.value.id}`;
|
||||
return `/dashboard/report/${report.value.id}`
|
||||
} else if (project.value) {
|
||||
return `${getProjectLink(project.value)}/moderation#messages`;
|
||||
return `${getProjectLink(project.value)}/moderation#messages`
|
||||
}
|
||||
return "#";
|
||||
});
|
||||
return '#'
|
||||
})
|
||||
|
||||
const hasBody = computed(() => !type.value || thread.value || type.value === "project_update");
|
||||
const hasBody = computed(() => !type.value || thread.value || type.value === 'project_update')
|
||||
|
||||
async function read() {
|
||||
try {
|
||||
@ -403,54 +403,54 @@ async function read() {
|
||||
...(props.notification.grouped_notifs
|
||||
? props.notification.grouped_notifs.map((notif) => notif.id)
|
||||
: []),
|
||||
];
|
||||
const updateNotifs = await markAsRead(ids);
|
||||
const newNotifs = updateNotifs(props.notifications);
|
||||
emit("update:notifications", newNotifs);
|
||||
]
|
||||
const updateNotifs = await markAsRead(ids)
|
||||
const newNotifs = updateNotifs(props.notifications)
|
||||
emit('update:notifications', newNotifs)
|
||||
} catch (err) {
|
||||
app.$notify({
|
||||
group: "main",
|
||||
title: "Error marking notification as read",
|
||||
group: 'main',
|
||||
title: 'Error marking notification as read',
|
||||
text: err.data ? err.data.description : err,
|
||||
type: "error",
|
||||
});
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function performAction(notification, actionIndex) {
|
||||
startLoading();
|
||||
startLoading()
|
||||
try {
|
||||
await read();
|
||||
await read()
|
||||
|
||||
if (actionIndex !== null) {
|
||||
await useBaseFetch(`${notification.actions[actionIndex].action_route[1]}`, {
|
||||
method: notification.actions[actionIndex].action_route[0].toUpperCase(),
|
||||
});
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
app.$notify({
|
||||
group: "main",
|
||||
title: "An error occurred",
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data ? err.data.description : err,
|
||||
type: "error",
|
||||
});
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading();
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
function getMessages() {
|
||||
const messages = [];
|
||||
const messages = []
|
||||
if (props.notification.body.message_id) {
|
||||
messages.push(props.notification.body.message_id);
|
||||
messages.push(props.notification.body.message_id)
|
||||
}
|
||||
if (props.notification.grouped_notifs) {
|
||||
for (const notif of props.notification.grouped_notifs) {
|
||||
if (notif.body.message_id) {
|
||||
messages.push(notif.body.message_id);
|
||||
messages.push(notif.body.message_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
return messages;
|
||||
return messages
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -458,35 +458,35 @@ function getMessages() {
|
||||
.notification {
|
||||
display: grid;
|
||||
grid-template:
|
||||
"icon title"
|
||||
"actions actions"
|
||||
"date date";
|
||||
'icon title'
|
||||
'actions actions'
|
||||
'date date';
|
||||
grid-template-columns: min-content 1fr;
|
||||
grid-template-rows: min-content min-content min-content;
|
||||
gap: var(--spacing-card-sm);
|
||||
|
||||
&.compact {
|
||||
grid-template:
|
||||
"icon title actions"
|
||||
"date date date";
|
||||
'icon title actions'
|
||||
'date date date';
|
||||
grid-template-columns: min-content 1fr auto;
|
||||
grid-template-rows: auto min-content;
|
||||
}
|
||||
|
||||
&.has-body {
|
||||
grid-template:
|
||||
"icon title"
|
||||
"body body"
|
||||
"actions actions"
|
||||
"date date";
|
||||
'icon title'
|
||||
'body body'
|
||||
'actions actions'
|
||||
'date date';
|
||||
grid-template-columns: min-content 1fr;
|
||||
grid-template-rows: min-content auto auto min-content;
|
||||
|
||||
&.compact {
|
||||
grid-template:
|
||||
"icon title actions"
|
||||
"body body body"
|
||||
"date date date";
|
||||
'icon title actions'
|
||||
'body body body'
|
||||
'date date date';
|
||||
grid-template-columns: min-content 1fr auto;
|
||||
grid-template-rows: min-content auto min-content;
|
||||
}
|
||||
|
||||
@ -31,88 +31,88 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" generic="T">
|
||||
import { computed, onMounted,ref } from "vue";
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
const modelValue = defineModel<T>({ required: true });
|
||||
const modelValue = defineModel<T>({ required: true })
|
||||
|
||||
const props = defineProps<{
|
||||
options: T[];
|
||||
}>();
|
||||
options: T[]
|
||||
}>()
|
||||
|
||||
const scrollContainer = ref<HTMLElement | null>(null);
|
||||
const scrollContainer = ref<HTMLElement | null>(null)
|
||||
|
||||
const sliderLeft = ref(4);
|
||||
const sliderTop = ref(4);
|
||||
const sliderRight = ref(4);
|
||||
const sliderBottom = ref(4);
|
||||
const sliderLeft = ref(4)
|
||||
const sliderTop = ref(4)
|
||||
const sliderRight = ref(4)
|
||||
const sliderBottom = ref(4)
|
||||
|
||||
const sliderLeftPx = computed(() => `${sliderLeft.value}px`);
|
||||
const sliderTopPx = computed(() => `${sliderTop.value}px`);
|
||||
const sliderRightPx = computed(() => `${sliderRight.value}px`);
|
||||
const sliderBottomPx = computed(() => `${sliderBottom.value}px`);
|
||||
const sliderLeftPx = computed(() => `${sliderLeft.value}px`)
|
||||
const sliderTopPx = computed(() => `${sliderTop.value}px`)
|
||||
const sliderRightPx = computed(() => `${sliderRight.value}px`)
|
||||
const sliderBottomPx = computed(() => `${sliderBottom.value}px`)
|
||||
|
||||
const optionButtons = ref();
|
||||
const optionButtons = ref()
|
||||
|
||||
const initialized = ref(false);
|
||||
const initialized = ref(false)
|
||||
|
||||
function setOption(option: T) {
|
||||
modelValue.value = option;
|
||||
modelValue.value = option
|
||||
}
|
||||
|
||||
watch(modelValue, () => {
|
||||
startAnimation(props.options.indexOf(modelValue.value));
|
||||
});
|
||||
startAnimation(props.options.indexOf(modelValue.value))
|
||||
})
|
||||
|
||||
function startAnimation(index: number) {
|
||||
const el = optionButtons.value[index];
|
||||
const el = optionButtons.value[index]
|
||||
|
||||
if (!el || !el.offsetParent) return;
|
||||
if (!el || !el.offsetParent) return
|
||||
|
||||
const newValues = {
|
||||
left: el.offsetLeft,
|
||||
top: el.offsetTop,
|
||||
right: el.offsetParent.offsetWidth - el.offsetLeft - el.offsetWidth,
|
||||
bottom: el.offsetParent.offsetHeight - el.offsetTop - el.offsetHeight,
|
||||
};
|
||||
}
|
||||
|
||||
if (sliderLeft.value === 4 && sliderRight.value === 4) {
|
||||
sliderLeft.value = newValues.left;
|
||||
sliderRight.value = newValues.right;
|
||||
sliderTop.value = newValues.top;
|
||||
sliderBottom.value = newValues.bottom;
|
||||
sliderLeft.value = newValues.left
|
||||
sliderRight.value = newValues.right
|
||||
sliderTop.value = newValues.top
|
||||
sliderBottom.value = newValues.bottom
|
||||
} else {
|
||||
const delay = 200;
|
||||
const delay = 200
|
||||
|
||||
if (newValues.left < sliderLeft.value) {
|
||||
sliderLeft.value = newValues.left;
|
||||
sliderLeft.value = newValues.left
|
||||
setTimeout(() => {
|
||||
sliderRight.value = newValues.right;
|
||||
}, delay);
|
||||
sliderRight.value = newValues.right
|
||||
}, delay)
|
||||
} else {
|
||||
sliderRight.value = newValues.right;
|
||||
sliderRight.value = newValues.right
|
||||
setTimeout(() => {
|
||||
sliderLeft.value = newValues.left;
|
||||
}, delay);
|
||||
sliderLeft.value = newValues.left
|
||||
}, delay)
|
||||
}
|
||||
|
||||
if (newValues.top < sliderTop.value) {
|
||||
sliderTop.value = newValues.top;
|
||||
sliderTop.value = newValues.top
|
||||
setTimeout(() => {
|
||||
sliderBottom.value = newValues.bottom;
|
||||
}, delay);
|
||||
sliderBottom.value = newValues.bottom
|
||||
}, delay)
|
||||
} else {
|
||||
sliderBottom.value = newValues.bottom;
|
||||
sliderBottom.value = newValues.bottom
|
||||
setTimeout(() => {
|
||||
sliderTop.value = newValues.top;
|
||||
}, delay);
|
||||
sliderTop.value = newValues.top
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
initialized.value = true;
|
||||
initialized.value = true
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
startAnimation(props.options.indexOf(modelValue.value));
|
||||
});
|
||||
startAnimation(props.options.indexOf(modelValue.value))
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -71,51 +71,51 @@
|
||||
</NewModal>
|
||||
</template>
|
||||
<script setup>
|
||||
import { PlusIcon,XIcon } from "@modrinth/assets";
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { PlusIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, NewModal } from '@modrinth/ui'
|
||||
|
||||
const router = useNativeRouter();
|
||||
const router = useNativeRouter()
|
||||
|
||||
const name = ref("");
|
||||
const slug = ref("");
|
||||
const description = ref("");
|
||||
const manualSlug = ref(false);
|
||||
const name = ref('')
|
||||
const slug = ref('')
|
||||
const description = ref('')
|
||||
const manualSlug = ref(false)
|
||||
|
||||
const modal = ref();
|
||||
const modal = ref()
|
||||
|
||||
async function createOrganization() {
|
||||
startLoading();
|
||||
startLoading()
|
||||
try {
|
||||
const value = {
|
||||
name: name.value.trim(),
|
||||
description: description.value.trim(),
|
||||
slug: slug.value.trim().replace(/ +/g, ""),
|
||||
};
|
||||
slug: slug.value.trim().replace(/ +/g, ''),
|
||||
}
|
||||
|
||||
const result = await useBaseFetch("organization", {
|
||||
method: "POST",
|
||||
const result = await useBaseFetch('organization', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(value),
|
||||
apiVersion: 3,
|
||||
});
|
||||
})
|
||||
|
||||
modal.value.hide();
|
||||
modal.value.hide()
|
||||
|
||||
await router.push(`/organization/${result.slug}`);
|
||||
await router.push(`/organization/${result.slug}`)
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
console.error(err)
|
||||
addNotification({
|
||||
group: "main",
|
||||
title: "An error occurred",
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data ? err.data.description : err,
|
||||
type: "error",
|
||||
});
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading();
|
||||
stopLoading()
|
||||
}
|
||||
function show(event) {
|
||||
name.value = "";
|
||||
description.value = "";
|
||||
modal.value.show(event);
|
||||
name.value = ''
|
||||
description.value = ''
|
||||
modal.value.show(event)
|
||||
}
|
||||
|
||||
function updateSlug() {
|
||||
@ -123,15 +123,15 @@ function updateSlug() {
|
||||
slug.value = name.value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replaceAll(" ", "-")
|
||||
.replaceAll(/[^a-zA-Z0-9!@$()`.+,_"-]/g, "")
|
||||
.replaceAll(/--+/gm, "-");
|
||||
.replaceAll(' ', '-')
|
||||
.replaceAll(/[^a-zA-Z0-9!@$()`.+,_"-]/g, '')
|
||||
.replaceAll(/--+/gm, '-')
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@ -59,7 +59,7 @@
|
||||
<span>{{
|
||||
formatProjectType(
|
||||
$getProjectTypeForDisplay(
|
||||
project.project_types?.[0] ?? "project",
|
||||
project.project_types?.[0] ?? 'project',
|
||||
project.loaders,
|
||||
),
|
||||
)
|
||||
@ -88,13 +88,13 @@
|
||||
<span>
|
||||
{{
|
||||
selectedProjects.length === props.projects.length
|
||||
? "All"
|
||||
? 'All'
|
||||
: selectedProjects.length
|
||||
}}
|
||||
</span>
|
||||
<span>
|
||||
{{ " " }}
|
||||
{{ selectedProjects.length === 1 ? "project" : "projects" }}
|
||||
{{ ' ' }}
|
||||
{{ selectedProjects.length === 1 ? 'project' : 'projects' }}
|
||||
</span>
|
||||
</span>
|
||||
</Button>
|
||||
@ -109,40 +109,40 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { BoxIcon, SettingsIcon, TransferIcon, XIcon } from "@modrinth/assets";
|
||||
import { Avatar,Button, Checkbox, CopyCode, Modal } from "@modrinth/ui";
|
||||
import { formatProjectType } from "@modrinth/utils";
|
||||
import { BoxIcon, SettingsIcon, TransferIcon, XIcon } from '@modrinth/assets'
|
||||
import { Avatar, Button, Checkbox, CopyCode, Modal } from '@modrinth/ui'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
|
||||
const modalOpen = ref(null);
|
||||
const modalOpen = ref(null)
|
||||
|
||||
const props = defineProps({
|
||||
projects: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
// define emit for submission
|
||||
const emit = defineEmits(["submit"]);
|
||||
const emit = defineEmits(['submit'])
|
||||
|
||||
const selectedProjects = ref([]);
|
||||
const selectedProjects = ref([])
|
||||
|
||||
const toggleSelectedProjects = () => {
|
||||
if (selectedProjects.value.length === props.projects.length) {
|
||||
selectedProjects.value = [];
|
||||
selectedProjects.value = []
|
||||
} else {
|
||||
selectedProjects.value = props.projects;
|
||||
selectedProjects.value = props.projects
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const onSubmitHandler = () => {
|
||||
if (selectedProjects.value.length === 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
emit("submit", selectedProjects.value);
|
||||
selectedProjects.value = [];
|
||||
modalOpen.value?.hide();
|
||||
};
|
||||
emit('submit', selectedProjects.value)
|
||||
selectedProjects.value = []
|
||||
modalOpen.value?.hide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@ -176,7 +176,7 @@ const onSubmitHandler = () => {
|
||||
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template: "checkbox icon name type settings" "checkbox icon id type settings";
|
||||
grid-template: 'checkbox icon name type settings' 'checkbox icon id type settings';
|
||||
grid-template-columns:
|
||||
min-content min-content minmax(min-content, 2fr)
|
||||
minmax(min-content, 1fr) min-content;
|
||||
@ -208,7 +208,7 @@ const onSubmitHandler = () => {
|
||||
}
|
||||
|
||||
.table-head {
|
||||
grid-template: "checkbox settings";
|
||||
grid-template: 'checkbox settings';
|
||||
grid-template-columns: min-content minmax(min-content, 1fr);
|
||||
|
||||
:nth-child(2),
|
||||
@ -223,7 +223,7 @@ const onSubmitHandler = () => {
|
||||
@media screen and (max-width: 560px) {
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template: "checkbox icon name settings" "checkbox icon id settings" "checkbox icon type settings" "checkbox icon status settings";
|
||||
grid-template: 'checkbox icon name settings' 'checkbox icon id settings' 'checkbox icon type settings' 'checkbox icon status settings';
|
||||
grid-template-columns: min-content min-content minmax(min-content, 1fr) min-content;
|
||||
|
||||
:nth-child(5) {
|
||||
@ -232,7 +232,7 @@ const onSubmitHandler = () => {
|
||||
}
|
||||
|
||||
.table-head {
|
||||
grid-template: "checkbox settings";
|
||||
grid-template: 'checkbox settings';
|
||||
grid-template-columns: min-content minmax(min-content, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,11 +90,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { CalendarIcon, DownloadIcon, HeartIcon,UpdatedIcon } from "@modrinth/assets";
|
||||
import { Avatar, ProjectStatusBadge, useRelativeTime } from "@modrinth/ui";
|
||||
import { CalendarIcon, DownloadIcon, HeartIcon, UpdatedIcon } from '@modrinth/assets'
|
||||
import { Avatar, ProjectStatusBadge, useRelativeTime } from '@modrinth/ui'
|
||||
|
||||
import EnvironmentIndicator from "~/components/ui/EnvironmentIndicator.vue";
|
||||
import Categories from "~/components/ui/search/Categories.vue";
|
||||
import EnvironmentIndicator from '~/components/ui/EnvironmentIndicator.vue'
|
||||
import Categories from '~/components/ui/search/Categories.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -110,15 +110,15 @@ export default {
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: "modrinth-0",
|
||||
default: 'modrinth-0',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "mod",
|
||||
default: 'mod',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: "Project Name",
|
||||
default: 'Project Name',
|
||||
},
|
||||
author: {
|
||||
type: String,
|
||||
@ -126,11 +126,11 @@ export default {
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: "A _type description",
|
||||
default: 'A _type description',
|
||||
},
|
||||
iconUrl: {
|
||||
type: String,
|
||||
default: "#",
|
||||
default: '#',
|
||||
required: false,
|
||||
},
|
||||
downloads: {
|
||||
@ -145,7 +145,7 @@ export default {
|
||||
},
|
||||
createdAt: {
|
||||
type: String,
|
||||
default: "0000-00-00",
|
||||
default: '0000-00-00',
|
||||
},
|
||||
updatedAt: {
|
||||
type: String,
|
||||
@ -154,7 +154,7 @@ export default {
|
||||
categories: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
return []
|
||||
},
|
||||
},
|
||||
status: {
|
||||
@ -168,12 +168,12 @@ export default {
|
||||
serverSide: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
clientSide: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
moderation: {
|
||||
type: Boolean,
|
||||
@ -212,26 +212,26 @@ export default {
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const tags = useTags();
|
||||
const formatRelativeTime = useRelativeTime();
|
||||
const tags = useTags()
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
|
||||
return { tags, formatRelativeTime };
|
||||
return { tags, formatRelativeTime }
|
||||
},
|
||||
computed: {
|
||||
projectTypeDisplay() {
|
||||
return this.$getProjectTypeForDisplay(this.type, this.categories);
|
||||
return this.$getProjectTypeForDisplay(this.type, this.categories)
|
||||
},
|
||||
toColor() {
|
||||
let color = this.color;
|
||||
let color = this.color
|
||||
|
||||
color >>>= 0;
|
||||
const b = color & 0xff;
|
||||
const g = (color & 0xff00) >>> 8;
|
||||
const r = (color & 0xff0000) >>> 16;
|
||||
return "rgba(" + [r, g, b, 1].join(",") + ")";
|
||||
color >>>= 0
|
||||
const b = color & 0xff
|
||||
const g = (color & 0xff00) >>> 8
|
||||
const r = (color & 0xff0000) >>> 16
|
||||
return 'rgba(' + [r, g, b, 1].join(',') + ')'
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@ -244,9 +244,9 @@ export default {
|
||||
|
||||
.display-mode--list .project-card {
|
||||
grid-template:
|
||||
"icon title stats"
|
||||
"icon description stats"
|
||||
"icon tags stats";
|
||||
'icon title stats'
|
||||
'icon description stats'
|
||||
'icon tags stats';
|
||||
grid-template-columns: min-content 1fr auto;
|
||||
grid-template-rows: min-content 1fr min-content;
|
||||
column-gap: var(--spacing-card-md);
|
||||
@ -255,20 +255,20 @@ export default {
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
grid-template:
|
||||
"icon title"
|
||||
"icon description"
|
||||
"icon tags"
|
||||
"stats stats";
|
||||
'icon title'
|
||||
'icon description'
|
||||
'icon tags'
|
||||
'stats stats';
|
||||
grid-template-columns: min-content auto;
|
||||
grid-template-rows: min-content 1fr min-content min-content;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
grid-template:
|
||||
"icon title"
|
||||
"icon description"
|
||||
"tags tags"
|
||||
"stats stats";
|
||||
'icon title'
|
||||
'icon description'
|
||||
'tags tags'
|
||||
'stats stats';
|
||||
grid-template-columns: min-content auto;
|
||||
grid-template-rows: min-content 1fr min-content min-content;
|
||||
}
|
||||
@ -277,7 +277,7 @@ export default {
|
||||
.display-mode--gallery .project-card,
|
||||
.display-mode--grid .project-card {
|
||||
padding: 0 0 var(--spacing-card-bg) 0;
|
||||
grid-template: "gallery gallery" "icon title" "description description" "tags tags" "stats stats";
|
||||
grid-template: 'gallery gallery' 'icon title' 'description description' 'tags tags' 'stats stats';
|
||||
grid-template-columns: min-content 1fr;
|
||||
grid-template-rows: min-content min-content 1fr min-content min-content;
|
||||
row-gap: var(--spacing-card-sm);
|
||||
@ -501,10 +501,10 @@ export default {
|
||||
.small-mode {
|
||||
@media screen and (min-width: 750px) {
|
||||
grid-template:
|
||||
"icon title"
|
||||
"icon description"
|
||||
"icon tags"
|
||||
"stats stats" !important;
|
||||
'icon title'
|
||||
'icon description'
|
||||
'icon tags'
|
||||
'stats stats' !important;
|
||||
grid-template-columns: min-content auto !important;
|
||||
grid-template-rows: min-content 1fr min-content min-content !important;
|
||||
|
||||
|
||||
@ -117,10 +117,10 @@ import {
|
||||
ScaleIcon,
|
||||
SendIcon,
|
||||
XIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { formatProjectType } from "@modrinth/utils";
|
||||
} from '@modrinth/assets'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
|
||||
import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js";
|
||||
import { acceptTeamInvite, removeTeamMember } from '~/helpers/teams.js'
|
||||
|
||||
const props = defineProps({
|
||||
project: {
|
||||
@ -130,7 +130,7 @@ const props = defineProps({
|
||||
versions: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
return []
|
||||
},
|
||||
},
|
||||
currentMember: {
|
||||
@ -151,7 +151,7 @@ const props = defineProps({
|
||||
},
|
||||
routeName: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
auth: {
|
||||
type: Object,
|
||||
@ -166,12 +166,12 @@ const props = defineProps({
|
||||
default() {
|
||||
return () => {
|
||||
addNotification({
|
||||
group: "main",
|
||||
title: "An error occurred",
|
||||
text: "setProcessing function not found",
|
||||
type: "error",
|
||||
});
|
||||
};
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: 'setProcessing function not found',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
toggleCollapsed: {
|
||||
@ -179,12 +179,12 @@ const props = defineProps({
|
||||
default() {
|
||||
return () => {
|
||||
addNotification({
|
||||
group: "main",
|
||||
title: "An error occurred",
|
||||
text: "toggleCollapsed function not found",
|
||||
type: "error",
|
||||
});
|
||||
};
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: 'toggleCollapsed function not found',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
updateMembers: {
|
||||
@ -192,81 +192,81 @@ const props = defineProps({
|
||||
default() {
|
||||
return () => {
|
||||
addNotification({
|
||||
group: "main",
|
||||
title: "An error occurred",
|
||||
text: "updateMembers function not found",
|
||||
type: "error",
|
||||
});
|
||||
};
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: 'updateMembers function not found',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const featuredGalleryImage = computed(() => props.project.gallery.find((img) => img.featured));
|
||||
const featuredGalleryImage = computed(() => props.project.gallery.find((img) => img.featured))
|
||||
|
||||
const nags = computed(() => [
|
||||
{
|
||||
condition: props.versions.length < 1,
|
||||
title: "Upload a version",
|
||||
id: "upload-version",
|
||||
description: "At least one version is required for a project to be submitted for review.",
|
||||
status: "required",
|
||||
title: 'Upload a version',
|
||||
id: 'upload-version',
|
||||
description: 'At least one version is required for a project to be submitted for review.',
|
||||
status: 'required',
|
||||
link: {
|
||||
path: "versions",
|
||||
title: "Visit versions page",
|
||||
hide: props.routeName === "type-id-versions",
|
||||
path: 'versions',
|
||||
title: 'Visit versions page',
|
||||
hide: props.routeName === 'type-id-versions',
|
||||
},
|
||||
},
|
||||
{
|
||||
condition:
|
||||
props.project.body === "" || props.project.body.startsWith("# Placeholder description"),
|
||||
title: "Add a description",
|
||||
id: "add-description",
|
||||
props.project.body === '' || props.project.body.startsWith('# Placeholder description'),
|
||||
title: 'Add a description',
|
||||
id: 'add-description',
|
||||
description:
|
||||
"A description that clearly describes the project's purpose and function is required.",
|
||||
status: "required",
|
||||
status: 'required',
|
||||
link: {
|
||||
path: "settings/description",
|
||||
title: "Visit description settings",
|
||||
hide: props.routeName === "type-id-settings-description",
|
||||
path: 'settings/description',
|
||||
title: 'Visit description settings',
|
||||
hide: props.routeName === 'type-id-settings-description',
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: !props.project.icon_url,
|
||||
title: "Add an icon",
|
||||
id: "add-icon",
|
||||
title: 'Add an icon',
|
||||
id: 'add-icon',
|
||||
description:
|
||||
"Your project should have a nice-looking icon to uniquely identify your project at a glance.",
|
||||
status: "suggestion",
|
||||
'Your project should have a nice-looking icon to uniquely identify your project at a glance.',
|
||||
status: 'suggestion',
|
||||
link: {
|
||||
path: "settings",
|
||||
title: "Visit general settings",
|
||||
hide: props.routeName === "type-id-settings",
|
||||
path: 'settings',
|
||||
title: 'Visit general settings',
|
||||
hide: props.routeName === 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: props.project.gallery.length === 0 || !featuredGalleryImage.value,
|
||||
title: "Feature a gallery image",
|
||||
id: "feature-gallery-image",
|
||||
description: "Featured gallery images may be the first impression of many users.",
|
||||
status: "suggestion",
|
||||
title: 'Feature a gallery image',
|
||||
id: 'feature-gallery-image',
|
||||
description: 'Featured gallery images may be the first impression of many users.',
|
||||
status: 'suggestion',
|
||||
link: {
|
||||
path: "gallery",
|
||||
title: "Visit gallery page",
|
||||
hide: props.routeName === "type-id-gallery",
|
||||
path: 'gallery',
|
||||
title: 'Visit gallery page',
|
||||
hide: props.routeName === 'type-id-gallery',
|
||||
},
|
||||
},
|
||||
{
|
||||
hide: props.project.versions.length === 0,
|
||||
condition: props.project.categories.length < 1,
|
||||
title: "Select tags",
|
||||
id: "select-tags",
|
||||
description: "Select all tags that apply to your project.",
|
||||
status: "suggestion",
|
||||
title: 'Select tags',
|
||||
id: 'select-tags',
|
||||
description: 'Select all tags that apply to your project.',
|
||||
status: 'suggestion',
|
||||
link: {
|
||||
path: "settings/tags",
|
||||
title: "Visit tag settings",
|
||||
hide: props.routeName === "type-id-settings-tags",
|
||||
path: 'settings/tags',
|
||||
title: 'Visit tag settings',
|
||||
hide: props.routeName === 'type-id-settings-tags',
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -277,111 +277,111 @@ const nags = computed(() => [
|
||||
props.project.discord_url ||
|
||||
props.project.donation_urls.length > 0
|
||||
),
|
||||
title: "Add external links",
|
||||
id: "add-links",
|
||||
title: 'Add external links',
|
||||
id: 'add-links',
|
||||
description:
|
||||
"Add any relevant links targeted outside of Modrinth, such as sources, issues, or a Discord invite.",
|
||||
status: "suggestion",
|
||||
'Add any relevant links targeted outside of Modrinth, such as sources, issues, or a Discord invite.',
|
||||
status: 'suggestion',
|
||||
link: {
|
||||
path: "settings/links",
|
||||
title: "Visit links settings",
|
||||
hide: props.routeName === "type-id-settings-links",
|
||||
path: 'settings/links',
|
||||
title: 'Visit links settings',
|
||||
hide: props.routeName === 'type-id-settings-links',
|
||||
},
|
||||
},
|
||||
{
|
||||
hide:
|
||||
props.project.versions.length === 0 ||
|
||||
props.project.project_type === "resourcepack" ||
|
||||
props.project.project_type === "plugin" ||
|
||||
props.project.project_type === "shader" ||
|
||||
props.project.project_type === "datapack",
|
||||
props.project.project_type === 'resourcepack' ||
|
||||
props.project.project_type === 'plugin' ||
|
||||
props.project.project_type === 'shader' ||
|
||||
props.project.project_type === 'datapack',
|
||||
condition:
|
||||
props.project.client_side === "unknown" ||
|
||||
props.project.server_side === "unknown" ||
|
||||
(props.project.client_side === "unsupported" && props.project.server_side === "unsupported"),
|
||||
title: "Select supported environments",
|
||||
id: "select-environments",
|
||||
props.project.client_side === 'unknown' ||
|
||||
props.project.server_side === 'unknown' ||
|
||||
(props.project.client_side === 'unsupported' && props.project.server_side === 'unsupported'),
|
||||
title: 'Select supported environments',
|
||||
id: 'select-environments',
|
||||
description: `Select if the ${formatProjectType(
|
||||
props.project.project_type,
|
||||
).toLowerCase()} functions on the client-side and/or server-side.`,
|
||||
status: "required",
|
||||
status: 'required',
|
||||
link: {
|
||||
path: "settings",
|
||||
title: "Visit general settings",
|
||||
hide: props.routeName === "type-id-settings",
|
||||
path: 'settings',
|
||||
title: 'Visit general settings',
|
||||
hide: props.routeName === 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: props.project.license.id === "LicenseRef-Unknown",
|
||||
title: "Select license",
|
||||
id: "select-license",
|
||||
condition: props.project.license.id === 'LicenseRef-Unknown',
|
||||
title: 'Select license',
|
||||
id: 'select-license',
|
||||
description: `Select the license your ${formatProjectType(
|
||||
props.project.project_type,
|
||||
).toLowerCase()} is distributed under.`,
|
||||
status: "required",
|
||||
status: 'required',
|
||||
link: {
|
||||
path: "settings/license",
|
||||
title: "Visit license settings",
|
||||
hide: props.routeName === "type-id-settings-license",
|
||||
path: 'settings/license',
|
||||
title: 'Visit license settings',
|
||||
hide: props.routeName === 'type-id-settings-license',
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: props.project.status === "draft",
|
||||
title: "Submit for review",
|
||||
id: "submit-for-review",
|
||||
condition: props.project.status === 'draft',
|
||||
title: 'Submit for review',
|
||||
id: 'submit-for-review',
|
||||
description:
|
||||
"Your project is only viewable by members of the project. It must be reviewed by moderators in order to be published.",
|
||||
status: "review",
|
||||
'Your project is only viewable by members of the project. It must be reviewed by moderators in order to be published.',
|
||||
status: 'review',
|
||||
link: null,
|
||||
action: {
|
||||
onClick: submitForReview,
|
||||
title: "Submit for review",
|
||||
disabled: () => nags.value.filter((x) => x.condition && x.status === "required").length > 0,
|
||||
title: 'Submit for review',
|
||||
disabled: () => nags.value.filter((x) => x.condition && x.status === 'required').length > 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
hide: props.project.stats === "draft",
|
||||
hide: props.project.stats === 'draft',
|
||||
condition: props.tags.rejectedStatuses.includes(props.project.status),
|
||||
title: "Resubmit for review",
|
||||
id: "resubmit-for-review",
|
||||
title: 'Resubmit for review',
|
||||
id: 'resubmit-for-review',
|
||||
description: `Your project has been ${props.project.status} by
|
||||
Modrinth's staff. In most cases, you can resubmit for review after
|
||||
addressing the staff's message.`,
|
||||
status: "review",
|
||||
status: 'review',
|
||||
link: {
|
||||
path: "moderation",
|
||||
title: "Visit moderation page",
|
||||
hide: props.routeName === "type-id-moderation",
|
||||
path: 'moderation',
|
||||
title: 'Visit moderation page',
|
||||
hide: props.routeName === 'type-id-moderation',
|
||||
},
|
||||
},
|
||||
]);
|
||||
])
|
||||
|
||||
const showInvitation = computed(() => {
|
||||
if (props.allMembers && props.auth) {
|
||||
const member = props.allMembers.find((x) => x.user.id === props.auth.user.id);
|
||||
return member && !member.accepted;
|
||||
const member = props.allMembers.find((x) => x.user.id === props.auth.user.id)
|
||||
return member && !member.accepted
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return false
|
||||
})
|
||||
|
||||
const acceptInvite = () => {
|
||||
acceptTeamInvite(props.project.team);
|
||||
props.updateMembers();
|
||||
};
|
||||
acceptTeamInvite(props.project.team)
|
||||
props.updateMembers()
|
||||
}
|
||||
|
||||
const declineInvite = () => {
|
||||
removeTeamMember(props.project.team, props.auth.user.id);
|
||||
props.updateMembers();
|
||||
};
|
||||
removeTeamMember(props.project.team, props.auth.user.id)
|
||||
props.updateMembers()
|
||||
}
|
||||
|
||||
const submitForReview = async () => {
|
||||
if (
|
||||
!props.acknowledgedMessage ||
|
||||
nags.value.filter((x) => x.condition && x.status === "required").length === 0
|
||||
nags.value.filter((x) => x.condition && x.status === 'required').length === 0
|
||||
) {
|
||||
await props.setProcessing();
|
||||
await props.setProcessing()
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -64,23 +64,23 @@ import {
|
||||
MailIcon,
|
||||
MastodonIcon,
|
||||
TwitterIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
} from '@modrinth/assets'
|
||||
import { ButtonStyled } from '@modrinth/ui'
|
||||
|
||||
const props = defineProps<{
|
||||
title?: string;
|
||||
url: string;
|
||||
}>();
|
||||
title?: string
|
||||
url: string
|
||||
}>()
|
||||
|
||||
const copied = ref(false);
|
||||
const encodedUrl = computed(() => encodeURIComponent(props.url));
|
||||
const encodedTitle = computed(() => (props.title ? encodeURIComponent(props.title) : undefined));
|
||||
const copied = ref(false)
|
||||
const encodedUrl = computed(() => encodeURIComponent(props.url))
|
||||
const encodedTitle = computed(() => (props.title ? encodeURIComponent(props.title) : undefined))
|
||||
|
||||
async function copyToClipboard(text: string) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
copied.value = true;
|
||||
await navigator.clipboard.writeText(text)
|
||||
copied.value = true
|
||||
setTimeout(() => {
|
||||
copied.value = false;
|
||||
}, 3000);
|
||||
copied.value = false
|
||||
}, 3000)
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { formatMoney,formatNumber } from "@modrinth/utils";
|
||||
import dayjs from "dayjs";
|
||||
import VueApexCharts from "vue3-apexcharts";
|
||||
import { formatMoney, formatNumber } from '@modrinth/utils'
|
||||
import dayjs from 'dayjs'
|
||||
import VueApexCharts from 'vue3-apexcharts'
|
||||
|
||||
const props = defineProps({
|
||||
name: {
|
||||
@ -18,7 +18,7 @@ const props = defineProps({
|
||||
},
|
||||
formatLabels: {
|
||||
type: Function,
|
||||
default: (label) => dayjs(label).format("MMM D"),
|
||||
default: (label) => dayjs(label).format('MMM D'),
|
||||
},
|
||||
colors: {
|
||||
type: Array,
|
||||
@ -26,11 +26,11 @@ const props = defineProps({
|
||||
},
|
||||
prefix: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
suffix: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
hideToolbar: {
|
||||
type: Boolean,
|
||||
@ -46,7 +46,7 @@ const props = defineProps({
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "bar",
|
||||
default: 'bar',
|
||||
},
|
||||
hideTotal: {
|
||||
type: Boolean,
|
||||
@ -58,11 +58,11 @@ const props = defineProps({
|
||||
},
|
||||
legendPosition: {
|
||||
type: String,
|
||||
default: "right",
|
||||
default: 'right',
|
||||
},
|
||||
xAxisType: {
|
||||
type: String,
|
||||
default: "datetime",
|
||||
default: 'datetime',
|
||||
},
|
||||
percentStacked: {
|
||||
type: Boolean,
|
||||
@ -76,14 +76,14 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
function formatTooltipValue(value, props) {
|
||||
return props.isMoney ? formatMoney(value, false) : formatNumber(value, false);
|
||||
return props.isMoney ? formatMoney(value, false) : formatNumber(value, false)
|
||||
}
|
||||
|
||||
function generateListEntry(value, index, _, w, props) {
|
||||
const color = w.globals.colors?.[index];
|
||||
const color = w.globals.colors?.[index]
|
||||
|
||||
return `<div class="list-entry">
|
||||
<span class="circle" style="background-color: ${color}"></span>
|
||||
@ -93,35 +93,35 @@ function generateListEntry(value, index, _, w, props) {
|
||||
<div class="value">
|
||||
${props.prefix}${formatTooltipValue(value, props)}${props.suffix}
|
||||
</div>
|
||||
</div>`;
|
||||
</div>`
|
||||
}
|
||||
|
||||
function generateTooltip({ series, seriesIndex, dataPointIndex, w }, props) {
|
||||
const label = w.globals.lastXAxis.categories?.[dataPointIndex];
|
||||
const label = w.globals.lastXAxis.categories?.[dataPointIndex]
|
||||
|
||||
const formattedLabel = props.formatLabels(label);
|
||||
const formattedLabel = props.formatLabels(label)
|
||||
|
||||
let tooltip = `<div class="bar-tooltip">
|
||||
<div class="seperated-entry title">
|
||||
<div class="label">${formattedLabel}</div>`;
|
||||
<div class="label">${formattedLabel}</div>`
|
||||
|
||||
// Logic for total and percent stacked
|
||||
if (!props.hideTotal) {
|
||||
if (props.percentStacked) {
|
||||
const total = series.reduce((a, b) => a + (b?.[dataPointIndex] || 0), 0);
|
||||
const percentValue = (100 * series[seriesIndex][dataPointIndex]) / total;
|
||||
const total = series.reduce((a, b) => a + (b?.[dataPointIndex] || 0), 0)
|
||||
const percentValue = (100 * series[seriesIndex][dataPointIndex]) / total
|
||||
tooltip += `<div class="value">${props.prefix}${formatNumber(percentValue)}%${
|
||||
props.suffix
|
||||
}</div>`;
|
||||
}</div>`
|
||||
} else {
|
||||
const totalValue = series.reduce((a, b) => a + (b?.[dataPointIndex] || 0), 0);
|
||||
const totalValue = series.reduce((a, b) => a + (b?.[dataPointIndex] || 0), 0)
|
||||
tooltip += `<div class="value">${props.prefix}${formatTooltipValue(totalValue, props)}${
|
||||
props.suffix
|
||||
}</div>`;
|
||||
}</div>`
|
||||
}
|
||||
}
|
||||
|
||||
tooltip += '</div><hr class="card-divider" />';
|
||||
tooltip += '</div><hr class="card-divider" />'
|
||||
|
||||
// Logic for generating list entries
|
||||
if (props.percentStacked) {
|
||||
@ -131,9 +131,9 @@ function generateTooltip({ series, seriesIndex, dataPointIndex, w }, props) {
|
||||
seriesIndex,
|
||||
w,
|
||||
props,
|
||||
);
|
||||
)
|
||||
} else {
|
||||
const returnTopN = 15;
|
||||
const returnTopN = 15
|
||||
|
||||
const listEntries = series
|
||||
.map((value, index) => [
|
||||
@ -144,13 +144,13 @@ function generateTooltip({ series, seriesIndex, dataPointIndex, w }, props) {
|
||||
.sort((a, b) => b[0] - a[0])
|
||||
.slice(0, returnTopN) // Return only the top X entries
|
||||
.map((value) => value[1])
|
||||
.join("");
|
||||
.join('')
|
||||
|
||||
tooltip += listEntries;
|
||||
tooltip += listEntries
|
||||
}
|
||||
|
||||
tooltip += "</div>";
|
||||
return tooltip;
|
||||
tooltip += '</div>'
|
||||
return tooltip
|
||||
}
|
||||
|
||||
const chartOptions = computed(() => {
|
||||
@ -158,19 +158,19 @@ const chartOptions = computed(() => {
|
||||
chart: {
|
||||
id: props.name,
|
||||
fontFamily:
|
||||
"Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif",
|
||||
foreColor: "var(--color-base)",
|
||||
'Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif',
|
||||
foreColor: 'var(--color-base)',
|
||||
selection: {
|
||||
enabled: true,
|
||||
fill: {
|
||||
color: "var(--color-brand)",
|
||||
color: 'var(--color-brand)',
|
||||
},
|
||||
},
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
stacked: props.stacked,
|
||||
stackType: props.percentStacked ? "100%" : "normal",
|
||||
stackType: props.percentStacked ? '100%' : 'normal',
|
||||
zoom: {
|
||||
autoScaleYaxis: true,
|
||||
},
|
||||
@ -183,7 +183,7 @@ const chartOptions = computed(() => {
|
||||
categories: props.labels,
|
||||
labels: {
|
||||
style: {
|
||||
borderRadius: "var(--radius-sm)",
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
},
|
||||
},
|
||||
axisTicks: {
|
||||
@ -207,8 +207,8 @@ const chartOptions = computed(() => {
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
borderColor: "var(--color-button-bg)",
|
||||
tickColor: "var(--color-button-bg)",
|
||||
borderColor: 'var(--color-button-bg)',
|
||||
tickColor: 'var(--color-button-bg)',
|
||||
},
|
||||
legend: {
|
||||
show: !props.hideLegend,
|
||||
@ -216,16 +216,16 @@ const chartOptions = computed(() => {
|
||||
showForZeroSeries: false,
|
||||
showForSingleSeries: false,
|
||||
showForNullSeries: false,
|
||||
fontSize: "var(--font-size-nm)",
|
||||
fontSize: 'var(--font-size-nm)',
|
||||
fontFamily:
|
||||
"Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif",
|
||||
'Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif',
|
||||
onItemClick: {
|
||||
toggleDataSeries: true,
|
||||
},
|
||||
},
|
||||
markers: {
|
||||
size: 0,
|
||||
strokeColor: "var(--color-contrast)",
|
||||
strokeColor: 'var(--color-contrast)',
|
||||
strokeWidth: 3,
|
||||
strokeOpacity: 1,
|
||||
fillOpacity: 1,
|
||||
@ -236,29 +236,29 @@ const chartOptions = computed(() => {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: props.horizontalBar,
|
||||
columnWidth: "80%",
|
||||
endingShape: "rounded",
|
||||
columnWidth: '80%',
|
||||
endingShape: 'rounded',
|
||||
borderRadius: 5,
|
||||
borderRadiusApplication: "end",
|
||||
borderRadiusWhenStacked: "last",
|
||||
borderRadiusApplication: 'end',
|
||||
borderRadiusWhenStacked: 'last',
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
curve: "smooth",
|
||||
curve: 'smooth',
|
||||
width: 2,
|
||||
},
|
||||
tooltip: {
|
||||
custom: (d) => generateTooltip(d, props),
|
||||
},
|
||||
fill:
|
||||
props.type === "area"
|
||||
props.type === 'area'
|
||||
? {
|
||||
colors: props.colors,
|
||||
type: "gradient",
|
||||
type: 'gradient',
|
||||
opacity: 1,
|
||||
gradient: {
|
||||
shade: "light",
|
||||
type: "vertical",
|
||||
shade: 'light',
|
||||
type: 'vertical',
|
||||
shadeIntensity: 0,
|
||||
gradientToColors: props.colors,
|
||||
inverseColors: true,
|
||||
@ -269,40 +269,40 @@ const chartOptions = computed(() => {
|
||||
},
|
||||
}
|
||||
: {},
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
const chart = ref(null);
|
||||
const chart = ref(null)
|
||||
|
||||
const legendValues = ref(
|
||||
[...props.data].map((project, index) => {
|
||||
return { name: project.name, visible: true, color: props.colors[index] };
|
||||
return { name: project.name, visible: true, color: props.colors[index] }
|
||||
}),
|
||||
);
|
||||
)
|
||||
|
||||
const flipLegend = (legend, newVal) => {
|
||||
legend.visible = newVal;
|
||||
chart.value.toggleSeries(legend.name);
|
||||
};
|
||||
legend.visible = newVal
|
||||
chart.value.toggleSeries(legend.name)
|
||||
}
|
||||
|
||||
const resetChart = () => {
|
||||
if (!chart.value) return;
|
||||
chart.value.updateSeries([...props.data]);
|
||||
if (!chart.value) return
|
||||
chart.value.updateSeries([...props.data])
|
||||
chart.value.updateOptions({
|
||||
xaxis: {
|
||||
categories: props.labels,
|
||||
},
|
||||
});
|
||||
chart.value.resetSeries();
|
||||
})
|
||||
chart.value.resetSeries()
|
||||
legendValues.value.forEach((legend) => {
|
||||
legend.visible = true;
|
||||
});
|
||||
};
|
||||
legend.visible = true
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
resetChart,
|
||||
flipLegend,
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@ -304,28 +304,28 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DownloadIcon,UpdatedIcon } from "@modrinth/assets";
|
||||
import { Button, Card, DropdownSelect } from "@modrinth/ui";
|
||||
import { formatCategoryHeader,formatMoney, formatNumber } from "@modrinth/utils";
|
||||
import dayjs from "dayjs";
|
||||
import { computed } from "vue";
|
||||
import { DownloadIcon, UpdatedIcon } from '@modrinth/assets'
|
||||
import { Button, Card, DropdownSelect } from '@modrinth/ui'
|
||||
import { formatCategoryHeader, formatMoney, formatNumber } from '@modrinth/utils'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { UiChartsChart as Chart,UiChartsCompactChart as CompactChart } from "#components";
|
||||
import PaletteIcon from "~/assets/icons/palette.svg?component";
|
||||
import { analyticsSetToCSVString, intToRgba } from "~/utils/analytics.js";
|
||||
import { UiChartsChart as Chart, UiChartsCompactChart as CompactChart } from '#components'
|
||||
import PaletteIcon from '~/assets/icons/palette.svg?component'
|
||||
import { analyticsSetToCSVString, intToRgba } from '~/utils/analytics.js'
|
||||
|
||||
const router = useNativeRouter();
|
||||
const theme = useTheme();
|
||||
const router = useNativeRouter()
|
||||
const theme = useTheme()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
projects?: any[];
|
||||
projects?: any[]
|
||||
/**
|
||||
* @deprecated Use `ranges` instead
|
||||
*/
|
||||
resoloutions?: Record<string, number>;
|
||||
ranges?: RangeObject[];
|
||||
personal?: boolean;
|
||||
resoloutions?: Record<string, number>
|
||||
ranges?: RangeObject[]
|
||||
personal?: boolean
|
||||
}>(),
|
||||
{
|
||||
projects: undefined,
|
||||
@ -333,19 +333,19 @@ const props = withDefaults(
|
||||
ranges: () => defaultRanges,
|
||||
personal: false,
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
const projects = ref(props.projects || []);
|
||||
const projects = ref(props.projects || [])
|
||||
|
||||
// const selectedChart = ref('downloads')
|
||||
const selectedChart = computed({
|
||||
get: () => {
|
||||
const id = (router.currentRoute.value.query?.chart as string | undefined) || "downloads";
|
||||
const id = (router.currentRoute.value.query?.chart as string | undefined) || 'downloads'
|
||||
// if the id is anything but the 3 charts we have or undefined, throw an error
|
||||
if (!["downloads", "views", "revenue"].includes(id)) {
|
||||
throw new Error(`Unknown chart ${id}`);
|
||||
if (!['downloads', 'views', 'revenue'].includes(id)) {
|
||||
throw new Error(`Unknown chart ${id}`)
|
||||
}
|
||||
return id;
|
||||
return id
|
||||
},
|
||||
set: (chart) => {
|
||||
router.push({
|
||||
@ -353,119 +353,119 @@ const selectedChart = computed({
|
||||
...router.currentRoute.value.query,
|
||||
chart,
|
||||
},
|
||||
});
|
||||
})
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
// Chart refs
|
||||
const downloadsChart = ref();
|
||||
const viewsChart = ref();
|
||||
const revenueChart = ref();
|
||||
const tinyDownloadChart = ref();
|
||||
const tinyViewChart = ref();
|
||||
const tinyRevenueChart = ref();
|
||||
const downloadsChart = ref()
|
||||
const viewsChart = ref()
|
||||
const revenueChart = ref()
|
||||
const tinyDownloadChart = ref()
|
||||
const tinyViewChart = ref()
|
||||
const tinyRevenueChart = ref()
|
||||
|
||||
const selectedDisplayProjects = ref(props.projects || []);
|
||||
const selectedDisplayProjects = ref(props.projects || [])
|
||||
|
||||
const removeProjectFromDisplay = (id: string) => {
|
||||
selectedDisplayProjects.value = selectedDisplayProjects.value.filter((p) => p.id !== id);
|
||||
};
|
||||
selectedDisplayProjects.value = selectedDisplayProjects.value.filter((p) => p.id !== id)
|
||||
}
|
||||
|
||||
const addProjectToDisplay = (id: string) => {
|
||||
selectedDisplayProjects.value = [
|
||||
...selectedDisplayProjects.value,
|
||||
props.projects?.find((p) => p.id === id),
|
||||
].filter(Boolean);
|
||||
};
|
||||
].filter(Boolean)
|
||||
}
|
||||
|
||||
const projectIsOnDisplay = (id: string) => {
|
||||
return selectedDisplayProjects.value?.some((p) => p.id === id) ?? false;
|
||||
};
|
||||
return selectedDisplayProjects.value?.some((p) => p.id === id) ?? false
|
||||
}
|
||||
|
||||
const resetCharts = () => {
|
||||
downloadsChart.value?.resetChart();
|
||||
viewsChart.value?.resetChart();
|
||||
revenueChart.value?.resetChart();
|
||||
downloadsChart.value?.resetChart()
|
||||
viewsChart.value?.resetChart()
|
||||
revenueChart.value?.resetChart()
|
||||
|
||||
tinyDownloadChart.value?.resetChart();
|
||||
tinyViewChart.value?.resetChart();
|
||||
tinyRevenueChart.value?.resetChart();
|
||||
};
|
||||
tinyDownloadChart.value?.resetChart()
|
||||
tinyViewChart.value?.resetChart()
|
||||
tinyRevenueChart.value?.resetChart()
|
||||
}
|
||||
|
||||
const isUsingProjectColors = computed({
|
||||
get: () => {
|
||||
return (
|
||||
router.currentRoute.value.query?.colors === "true" ||
|
||||
router.currentRoute.value.query?.colors === 'true' ||
|
||||
router.currentRoute.value.query?.colors === undefined
|
||||
);
|
||||
)
|
||||
},
|
||||
set: (newValue) => {
|
||||
router.push({
|
||||
query: {
|
||||
...router.currentRoute.value.query,
|
||||
colors: newValue ? "true" : "false",
|
||||
colors: newValue ? 'true' : 'false',
|
||||
},
|
||||
});
|
||||
})
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const startDate = ref(dayjs().startOf("day"));
|
||||
const endDate = ref(dayjs().endOf("day"));
|
||||
const timeResolution = ref(30);
|
||||
const startDate = ref(dayjs().startOf('day'))
|
||||
const endDate = ref(dayjs().endOf('day'))
|
||||
const timeResolution = ref(30)
|
||||
|
||||
onBeforeMount(() => {
|
||||
// Load cached data and range from localStorage - cache.
|
||||
if (import.meta.client) {
|
||||
const rangeLabel = localStorage.getItem("analyticsSelectedRange");
|
||||
const rangeLabel = localStorage.getItem('analyticsSelectedRange')
|
||||
if (rangeLabel) {
|
||||
const range = props.ranges.find((r) => r.getLabel([dayjs(), dayjs()]) === rangeLabel)!;
|
||||
const range = props.ranges.find((r) => r.getLabel([dayjs(), dayjs()]) === rangeLabel)!
|
||||
|
||||
if (range !== undefined) {
|
||||
internalRange.value = range;
|
||||
const ranges = range.getDates(dayjs());
|
||||
timeResolution.value = range.timeResolution;
|
||||
startDate.value = ranges.startDate;
|
||||
endDate.value = ranges.endDate;
|
||||
internalRange.value = range
|
||||
const ranges = range.getDates(dayjs())
|
||||
timeResolution.value = range.timeResolution
|
||||
startDate.value = ranges.startDate
|
||||
endDate.value = ranges.endDate
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (internalRange.value === null) {
|
||||
internalRange.value = props.ranges.find(
|
||||
(r) => r.getLabel([dayjs(), dayjs()]) === "Previous 30 days",
|
||||
)!;
|
||||
(r) => r.getLabel([dayjs(), dayjs()]) === 'Previous 30 days',
|
||||
)!
|
||||
}
|
||||
|
||||
const ranges = selectedRange.value.getDates(dayjs());
|
||||
startDate.value = ranges.startDate;
|
||||
endDate.value = ranges.endDate;
|
||||
timeResolution.value = selectedRange.value.timeResolution;
|
||||
});
|
||||
const ranges = selectedRange.value.getDates(dayjs())
|
||||
startDate.value = ranges.startDate
|
||||
endDate.value = ranges.endDate
|
||||
timeResolution.value = selectedRange.value.timeResolution
|
||||
})
|
||||
|
||||
const internalRange: Ref<RangeObject> = ref(null as unknown as RangeObject);
|
||||
const internalRange: Ref<RangeObject> = ref(null as unknown as RangeObject)
|
||||
|
||||
const selectedRange = computed({
|
||||
get: () => {
|
||||
return internalRange.value;
|
||||
return internalRange.value
|
||||
},
|
||||
set: (newRange) => {
|
||||
const ranges = newRange.getDates(dayjs());
|
||||
startDate.value = ranges.startDate;
|
||||
endDate.value = ranges.endDate;
|
||||
timeResolution.value = newRange.timeResolution;
|
||||
const ranges = newRange.getDates(dayjs())
|
||||
startDate.value = ranges.startDate
|
||||
endDate.value = ranges.endDate
|
||||
timeResolution.value = newRange.timeResolution
|
||||
|
||||
internalRange.value = newRange;
|
||||
internalRange.value = newRange
|
||||
|
||||
if (import.meta.client) {
|
||||
localStorage.setItem(
|
||||
"analyticsSelectedRange",
|
||||
internalRange.value?.getLabel([dayjs(), dayjs()]) ?? "Previous 30 days",
|
||||
);
|
||||
'analyticsSelectedRange',
|
||||
internalRange.value?.getLabel([dayjs(), dayjs()]) ?? 'Previous 30 days',
|
||||
)
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const analytics = useFetchAllAnalytics(
|
||||
resetCharts,
|
||||
@ -475,53 +475,53 @@ const analytics = useFetchAllAnalytics(
|
||||
startDate,
|
||||
endDate,
|
||||
timeResolution,
|
||||
);
|
||||
)
|
||||
|
||||
const formattedCategorySubtitle = computed(() => {
|
||||
return (
|
||||
selectedRange.value?.getLabel([dayjs(startDate.value), dayjs(endDate.value)]) ?? "Loading..."
|
||||
);
|
||||
});
|
||||
selectedRange.value?.getLabel([dayjs(startDate.value), dayjs(endDate.value)]) ?? 'Loading...'
|
||||
)
|
||||
})
|
||||
|
||||
const selectedDataSet = computed(() => {
|
||||
switch (selectedChart.value) {
|
||||
case "downloads":
|
||||
return analytics.totalData.value.downloads;
|
||||
case "views":
|
||||
return analytics.totalData.value.views;
|
||||
case "revenue":
|
||||
return analytics.totalData.value.revenue;
|
||||
case 'downloads':
|
||||
return analytics.totalData.value.downloads
|
||||
case 'views':
|
||||
return analytics.totalData.value.views
|
||||
case 'revenue':
|
||||
return analytics.totalData.value.revenue
|
||||
default:
|
||||
throw new Error(`Unknown chart ${selectedChart.value}`);
|
||||
throw new Error(`Unknown chart ${selectedChart.value}`)
|
||||
}
|
||||
});
|
||||
})
|
||||
const selectedDataSetProjects = computed(() => {
|
||||
return selectedDataSet.value.projectIds
|
||||
.map((id) => props.projects?.find((p) => p?.id === id))
|
||||
.filter(Boolean);
|
||||
});
|
||||
.filter(Boolean)
|
||||
})
|
||||
|
||||
const downloadSelectedSetAsCSV = () => {
|
||||
const selectedChartName = selectedChart.value;
|
||||
const selectedChartName = selectedChart.value
|
||||
|
||||
const csv = analyticsSetToCSVString(selectedDataSet.value);
|
||||
const csv = analyticsSetToCSVString(selectedDataSet.value)
|
||||
|
||||
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
|
||||
|
||||
const link = document.createElement("a");
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.setAttribute("href", url);
|
||||
link.setAttribute("download", `${selectedChartName}-data.csv`);
|
||||
link.style.visibility = "hidden";
|
||||
document.body.appendChild(link);
|
||||
const link = document.createElement('a')
|
||||
const url = URL.createObjectURL(blob)
|
||||
link.setAttribute('href', url)
|
||||
link.setAttribute('download', `${selectedChartName}-data.csv`)
|
||||
link.style.visibility = 'hidden'
|
||||
document.body.appendChild(link)
|
||||
|
||||
link.click();
|
||||
};
|
||||
link.click()
|
||||
}
|
||||
|
||||
const onDownloadSetAsCSV = useClientTry(async () => await downloadSelectedSetAsCSV());
|
||||
const onDownloadSetAsCSV = useClientTry(async () => await downloadSelectedSetAsCSV())
|
||||
const onToggleColors = () => {
|
||||
isUsingProjectColors.value = !isUsingProjectColors.value;
|
||||
};
|
||||
isUsingProjectColors.value = !isUsingProjectColors.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@ -529,177 +529,177 @@ const onToggleColors = () => {
|
||||
* @deprecated Use `ranges` instead
|
||||
*/
|
||||
const defaultResoloutions: Record<string, number> = {
|
||||
"5 minutes": 5,
|
||||
"30 minutes": 30,
|
||||
"An hour": 60,
|
||||
"12 hours": 720,
|
||||
"A day": 1440,
|
||||
"A week": 10080,
|
||||
};
|
||||
'5 minutes': 5,
|
||||
'30 minutes': 30,
|
||||
'An hour': 60,
|
||||
'12 hours': 720,
|
||||
'A day': 1440,
|
||||
'A week': 10080,
|
||||
}
|
||||
|
||||
type DateRange = { startDate: dayjs.Dayjs; endDate: dayjs.Dayjs };
|
||||
type DateRange = { startDate: dayjs.Dayjs; endDate: dayjs.Dayjs }
|
||||
|
||||
type RangeObject = {
|
||||
getLabel: (dateRange: [dayjs.Dayjs, dayjs.Dayjs]) => string;
|
||||
getDates: (currentDate: dayjs.Dayjs) => DateRange;
|
||||
getLabel: (dateRange: [dayjs.Dayjs, dayjs.Dayjs]) => string
|
||||
getDates: (currentDate: dayjs.Dayjs) => DateRange
|
||||
// A time resolution in minutes.
|
||||
timeResolution: number;
|
||||
};
|
||||
timeResolution: number
|
||||
}
|
||||
|
||||
const defaultRanges: RangeObject[] = [
|
||||
{
|
||||
getLabel: () => "Previous 30 minutes",
|
||||
getLabel: () => 'Previous 30 minutes',
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).subtract(30, "minute"),
|
||||
startDate: dayjs(currentDate).subtract(30, 'minute'),
|
||||
endDate: currentDate,
|
||||
}),
|
||||
timeResolution: 1,
|
||||
},
|
||||
{
|
||||
getLabel: () => "Previous hour",
|
||||
getLabel: () => 'Previous hour',
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).subtract(1, "hour"),
|
||||
startDate: dayjs(currentDate).subtract(1, 'hour'),
|
||||
endDate: currentDate,
|
||||
}),
|
||||
timeResolution: 5,
|
||||
},
|
||||
{
|
||||
getLabel: () => "Previous 12 hours",
|
||||
getLabel: () => 'Previous 12 hours',
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).subtract(12, "hour"),
|
||||
startDate: dayjs(currentDate).subtract(12, 'hour'),
|
||||
endDate: currentDate,
|
||||
}),
|
||||
timeResolution: 12,
|
||||
},
|
||||
{
|
||||
getLabel: () => "Previous 24 hours",
|
||||
getLabel: () => 'Previous 24 hours',
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).subtract(1, "day"),
|
||||
startDate: dayjs(currentDate).subtract(1, 'day'),
|
||||
endDate: currentDate,
|
||||
}),
|
||||
timeResolution: 30,
|
||||
},
|
||||
{
|
||||
getLabel: () => "Today",
|
||||
getLabel: () => 'Today',
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).startOf("day"),
|
||||
startDate: dayjs(currentDate).startOf('day'),
|
||||
endDate: currentDate,
|
||||
}),
|
||||
timeResolution: 30,
|
||||
},
|
||||
{
|
||||
getLabel: () => "Yesterday",
|
||||
getLabel: () => 'Yesterday',
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).subtract(1, "day").startOf("day"),
|
||||
endDate: dayjs(currentDate).startOf("day").subtract(1, "second"),
|
||||
startDate: dayjs(currentDate).subtract(1, 'day').startOf('day'),
|
||||
endDate: dayjs(currentDate).startOf('day').subtract(1, 'second'),
|
||||
}),
|
||||
timeResolution: 30,
|
||||
},
|
||||
{
|
||||
getLabel: () => "This week",
|
||||
getLabel: () => 'This week',
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).startOf("week").add(1, "hour"),
|
||||
startDate: dayjs(currentDate).startOf('week').add(1, 'hour'),
|
||||
endDate: currentDate,
|
||||
}),
|
||||
timeResolution: 360,
|
||||
},
|
||||
{
|
||||
getLabel: () => "Last week",
|
||||
getLabel: () => 'Last week',
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).subtract(1, "week").startOf("week").add(1, "hour"),
|
||||
endDate: dayjs(currentDate).startOf("week").subtract(1, "second"),
|
||||
startDate: dayjs(currentDate).subtract(1, 'week').startOf('week').add(1, 'hour'),
|
||||
endDate: dayjs(currentDate).startOf('week').subtract(1, 'second'),
|
||||
}),
|
||||
timeResolution: 1440,
|
||||
},
|
||||
{
|
||||
getLabel: () => "Previous 7 days",
|
||||
getLabel: () => 'Previous 7 days',
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).startOf("day").subtract(7, "day").add(1, "hour"),
|
||||
endDate: currentDate.startOf("day"),
|
||||
startDate: dayjs(currentDate).startOf('day').subtract(7, 'day').add(1, 'hour'),
|
||||
endDate: currentDate.startOf('day'),
|
||||
}),
|
||||
timeResolution: 720,
|
||||
},
|
||||
{
|
||||
getLabel: () => "This month",
|
||||
getLabel: () => 'This month',
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).startOf("month").add(1, "hour"),
|
||||
startDate: dayjs(currentDate).startOf('month').add(1, 'hour'),
|
||||
endDate: currentDate,
|
||||
}),
|
||||
timeResolution: 1440,
|
||||
},
|
||||
{
|
||||
getLabel: () => "Last month",
|
||||
getLabel: () => 'Last month',
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).subtract(1, "month").startOf("month").add(1, "hour"),
|
||||
endDate: dayjs(currentDate).startOf("month").subtract(1, "second"),
|
||||
startDate: dayjs(currentDate).subtract(1, 'month').startOf('month').add(1, 'hour'),
|
||||
endDate: dayjs(currentDate).startOf('month').subtract(1, 'second'),
|
||||
}),
|
||||
timeResolution: 1440,
|
||||
},
|
||||
{
|
||||
getLabel: () => "Previous 30 days",
|
||||
getLabel: () => 'Previous 30 days',
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).startOf("day").subtract(30, "day").add(1, "hour"),
|
||||
endDate: currentDate.startOf("day"),
|
||||
startDate: dayjs(currentDate).startOf('day').subtract(30, 'day').add(1, 'hour'),
|
||||
endDate: currentDate.startOf('day'),
|
||||
}),
|
||||
timeResolution: 1440,
|
||||
},
|
||||
{
|
||||
getLabel: () => "This quarter",
|
||||
getLabel: () => 'This quarter',
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).startOf("quarter").add(1, "hour"),
|
||||
startDate: dayjs(currentDate).startOf('quarter').add(1, 'hour'),
|
||||
endDate: currentDate,
|
||||
}),
|
||||
timeResolution: 1440,
|
||||
},
|
||||
{
|
||||
getLabel: () => "Last quarter",
|
||||
getLabel: () => 'Last quarter',
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).subtract(1, "quarter").startOf("quarter").add(1, "hour"),
|
||||
endDate: dayjs(currentDate).startOf("quarter").subtract(1, "second"),
|
||||
startDate: dayjs(currentDate).subtract(1, 'quarter').startOf('quarter').add(1, 'hour'),
|
||||
endDate: dayjs(currentDate).startOf('quarter').subtract(1, 'second'),
|
||||
}),
|
||||
timeResolution: 1440,
|
||||
},
|
||||
{
|
||||
getLabel: () => "This year",
|
||||
getLabel: () => 'This year',
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).startOf("year"),
|
||||
startDate: dayjs(currentDate).startOf('year'),
|
||||
endDate: currentDate,
|
||||
}),
|
||||
timeResolution: 20160,
|
||||
},
|
||||
{
|
||||
getLabel: () => "Last year",
|
||||
getLabel: () => 'Last year',
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).subtract(1, "year").startOf("year"),
|
||||
endDate: dayjs(currentDate).startOf("year").subtract(1, "second"),
|
||||
startDate: dayjs(currentDate).subtract(1, 'year').startOf('year'),
|
||||
endDate: dayjs(currentDate).startOf('year').subtract(1, 'second'),
|
||||
}),
|
||||
timeResolution: 20160,
|
||||
},
|
||||
{
|
||||
getLabel: () => "Previous year",
|
||||
getLabel: () => 'Previous year',
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).subtract(1, "year"),
|
||||
startDate: dayjs(currentDate).subtract(1, 'year'),
|
||||
endDate: dayjs(currentDate),
|
||||
}),
|
||||
timeResolution: 40320,
|
||||
},
|
||||
{
|
||||
getLabel: () => "Previous two years",
|
||||
getLabel: () => 'Previous two years',
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).subtract(2, "year"),
|
||||
startDate: dayjs(currentDate).subtract(2, 'year'),
|
||||
endDate: currentDate,
|
||||
}),
|
||||
timeResolution: 40320,
|
||||
},
|
||||
{
|
||||
getLabel: () => "All Time",
|
||||
getLabel: () => 'All Time',
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(0),
|
||||
endDate: currentDate,
|
||||
}),
|
||||
timeResolution: 40320,
|
||||
},
|
||||
];
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@ -885,7 +885,7 @@ const defaultRanges: RangeObject[] = [
|
||||
|
||||
.country-value {
|
||||
display: grid;
|
||||
grid-template-areas: "flag text bar";
|
||||
grid-template-areas: 'flag text bar';
|
||||
grid-template-columns: auto 1fr 10rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { Card } from "@modrinth/ui";
|
||||
import VueApexCharts from "vue3-apexcharts";
|
||||
import { Card } from '@modrinth/ui'
|
||||
import VueApexCharts from 'vue3-apexcharts'
|
||||
|
||||
// let VueApexCharts
|
||||
// if (import.meta.client) {
|
||||
@ -10,11 +10,11 @@ import VueApexCharts from "vue3-apexcharts";
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
@ -26,11 +26,11 @@ const props = defineProps({
|
||||
},
|
||||
prefix: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
suffix: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
isMoney: {
|
||||
type: Boolean,
|
||||
@ -38,17 +38,17 @@ const props = defineProps({
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "var(--color-brand)",
|
||||
default: 'var(--color-brand)',
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
// no grid lines, no toolbar, no legend, no data labels
|
||||
const chartOptions = {
|
||||
chart: {
|
||||
id: props.title,
|
||||
fontFamily:
|
||||
"Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif",
|
||||
foreColor: "var(--color-base)",
|
||||
'Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif',
|
||||
foreColor: 'var(--color-base)',
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
@ -61,16 +61,16 @@ const chartOptions = {
|
||||
parentHeightOffset: 0,
|
||||
},
|
||||
stroke: {
|
||||
curve: "smooth",
|
||||
curve: 'smooth',
|
||||
width: 2,
|
||||
},
|
||||
fill: {
|
||||
colors: [props.color],
|
||||
type: "gradient",
|
||||
type: 'gradient',
|
||||
opacity: 1,
|
||||
gradient: {
|
||||
shade: "light",
|
||||
type: "vertical",
|
||||
shade: 'light',
|
||||
type: 'vertical',
|
||||
shadeIntensity: 0,
|
||||
gradientToColors: [props.color],
|
||||
inverseColors: true,
|
||||
@ -91,7 +91,7 @@ const chartOptions = {
|
||||
enabled: false,
|
||||
},
|
||||
xaxis: {
|
||||
type: "datetime",
|
||||
type: 'datetime',
|
||||
categories: props.labels,
|
||||
labels: {
|
||||
show: false,
|
||||
@ -120,23 +120,23 @@ const chartOptions = {
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const chart = ref(null);
|
||||
const chart = ref(null)
|
||||
|
||||
const resetChart = () => {
|
||||
chart.value?.updateSeries([...props.data]);
|
||||
chart.value?.updateSeries([...props.data])
|
||||
chart.value?.updateOptions({
|
||||
xaxis: {
|
||||
categories: props.labels,
|
||||
},
|
||||
});
|
||||
chart.value?.resetSeries();
|
||||
};
|
||||
})
|
||||
chart.value?.resetSeries()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
resetChart,
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@ -47,7 +47,7 @@
|
||||
<span class="max-w-[200px] truncate font-mono text-xs sm:max-w-none">
|
||||
{{
|
||||
report.version.files.find((file) => file.primary)?.filename ||
|
||||
"Unknown primary file"
|
||||
'Unknown primary file'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
@ -124,56 +124,56 @@ import {
|
||||
EyeIcon,
|
||||
LinkIcon,
|
||||
OrganizationIcon,
|
||||
} from "@modrinth/assets";
|
||||
import type { ExtendedDelphiReport } from "@modrinth/moderation";
|
||||
} from '@modrinth/assets'
|
||||
import type { ExtendedDelphiReport } from '@modrinth/moderation'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
OverflowMenu,
|
||||
type OverflowMenuOption,
|
||||
useRelativeTime,
|
||||
} from "@modrinth/ui";
|
||||
import dayjs from "dayjs";
|
||||
} from '@modrinth/ui'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const props = defineProps<{
|
||||
report: ExtendedDelphiReport;
|
||||
}>();
|
||||
report: ExtendedDelphiReport
|
||||
}>()
|
||||
|
||||
const formatRelativeTime = useRelativeTime();
|
||||
const isPending = computed(() => props.report.status === "pending");
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
const isPending = computed(() => props.report.status === 'pending')
|
||||
|
||||
const quickActions: OverflowMenuOption[] = [
|
||||
{
|
||||
id: "copy-link",
|
||||
id: 'copy-link',
|
||||
action: () => {
|
||||
const base = window.location.origin;
|
||||
const reviewUrl = `${base}/moderation/tech-reviews?q=${props.report.version.id}`;
|
||||
const base = window.location.origin
|
||||
const reviewUrl = `${base}/moderation/tech-reviews?q=${props.report.version.id}`
|
||||
navigator.clipboard.writeText(reviewUrl).then(() => {
|
||||
addNotification({
|
||||
type: "success",
|
||||
title: "Tech review link copied",
|
||||
text: "The link to this tech review has been copied to your clipboard.",
|
||||
});
|
||||
});
|
||||
type: 'success',
|
||||
title: 'Tech review link copied',
|
||||
text: 'The link to this tech review has been copied to your clipboard.',
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "copy-id",
|
||||
id: 'copy-id',
|
||||
action: () => {
|
||||
navigator.clipboard.writeText(props.report.version.id).then(() => {
|
||||
addNotification({
|
||||
type: "success",
|
||||
title: "Version ID copied",
|
||||
text: "The ID of this version has been copied to your clipboard.",
|
||||
});
|
||||
});
|
||||
type: 'success',
|
||||
title: 'Version ID copied',
|
||||
text: 'The ID of this version has been copied to your clipboard.',
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const versionUrl = computed(() => {
|
||||
return `/${props.report.project.project_type}/${props.report.project.slug}/versions/${props.report.version.id}`;
|
||||
});
|
||||
return `/${props.report.project.project_type}/${props.report.project.slug}/versions/${props.report.version.id}`
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@ -29,56 +29,56 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { type KeybindListener, keybinds, normalizeKeybind } from "@modrinth/moderation";
|
||||
import NewModal from "@modrinth/ui/src/components/modal/NewModal.vue";
|
||||
import { ref } from "vue";
|
||||
import { type KeybindListener, keybinds, normalizeKeybind } from '@modrinth/moderation'
|
||||
import NewModal from '@modrinth/ui/src/components/modal/NewModal.vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>();
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
|
||||
function parseKeybindDisplay(keybind: KeybindListener["keybind"]): string[] {
|
||||
const keybinds = Array.isArray(keybind) ? keybind : [keybind];
|
||||
const normalized = keybinds[0];
|
||||
const def = normalizeKeybind(normalized);
|
||||
function parseKeybindDisplay(keybind: KeybindListener['keybind']): string[] {
|
||||
const keybinds = Array.isArray(keybind) ? keybind : [keybind]
|
||||
const normalized = keybinds[0]
|
||||
const def = normalizeKeybind(normalized)
|
||||
|
||||
const keys = [];
|
||||
const keys = []
|
||||
|
||||
if (def.ctrl || def.meta) {
|
||||
keys.push(isMac() ? "CMD" : "CTRL");
|
||||
keys.push(isMac() ? 'CMD' : 'CTRL')
|
||||
}
|
||||
if (def.shift) keys.push("SHIFT");
|
||||
if (def.alt) keys.push("ALT");
|
||||
if (def.shift) keys.push('SHIFT')
|
||||
if (def.alt) keys.push('ALT')
|
||||
|
||||
const mainKey = def.key
|
||||
.replace("ArrowLeft", "←")
|
||||
.replace("ArrowRight", "→")
|
||||
.replace("ArrowUp", "↑")
|
||||
.replace("ArrowDown", "↓")
|
||||
.replace("Enter", "↵")
|
||||
.replace("Space", "SPACE")
|
||||
.replace("Escape", "ESC")
|
||||
.toUpperCase();
|
||||
.replace('ArrowLeft', '←')
|
||||
.replace('ArrowRight', '→')
|
||||
.replace('ArrowUp', '↑')
|
||||
.replace('ArrowDown', '↓')
|
||||
.replace('Enter', '↵')
|
||||
.replace('Space', 'SPACE')
|
||||
.replace('Escape', 'ESC')
|
||||
.toUpperCase()
|
||||
|
||||
keys.push(mainKey);
|
||||
keys.push(mainKey)
|
||||
|
||||
return keys;
|
||||
return keys
|
||||
}
|
||||
|
||||
function isMac() {
|
||||
return navigator.platform.toUpperCase().includes("MAC");
|
||||
return navigator.platform.toUpperCase().includes('MAC')
|
||||
}
|
||||
|
||||
function show(event?: MouseEvent) {
|
||||
modal.value?.show(event);
|
||||
modal.value?.show(event)
|
||||
}
|
||||
|
||||
function hide() {
|
||||
modal.value?.hide();
|
||||
modal.value?.hide()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
hide,
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@ -326,9 +326,14 @@ import {
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
type Action,
|
||||
type ButtonAction,
|
||||
checklist,
|
||||
type ConditionalButtonAction,
|
||||
deserializeActionStates,
|
||||
type DropdownAction,
|
||||
expandVariables,
|
||||
finalPermissionMessages,
|
||||
findMatchingVariant,
|
||||
flattenProjectVariables,
|
||||
getActionIdForStage,
|
||||
@ -338,16 +343,9 @@ import {
|
||||
initializeActionState,
|
||||
kebabToTitleCase,
|
||||
keybinds,
|
||||
type MultiSelectChipsAction,
|
||||
processMessage,
|
||||
serializeActionStates,
|
||||
} from '@modrinth/moderation'
|
||||
import {
|
||||
type Action,
|
||||
type ButtonAction,
|
||||
type ConditionalButtonAction,
|
||||
type DropdownAction,
|
||||
finalPermissionMessages,
|
||||
type MultiSelectChipsAction,
|
||||
type Stage,
|
||||
type ToggleAction,
|
||||
} from '@modrinth/moderation'
|
||||
@ -363,8 +361,8 @@ import {
|
||||
import {
|
||||
type ModerationJudgements,
|
||||
type ModerationModpackItem,
|
||||
type ProjectStatus,
|
||||
type Project,
|
||||
type ProjectStatus,
|
||||
renderHighlightedString,
|
||||
} from '@modrinth/utils'
|
||||
import { computedAsync, useLocalStorage } from '@vueuse/core'
|
||||
|
||||
@ -137,7 +137,7 @@
|
||||
<ButtonStyled v-if="modPackData && currentIndex < modPackData.length" color="blue">
|
||||
<button :disabled="!canGoNext" @click="goToNext">
|
||||
<RightArrowIcon aria-hidden="true" />
|
||||
{{ currentIndex + 1 >= modPackData.length ? "Complete" : "Next" }}
|
||||
{{ currentIndex + 1 >= modPackData.length ? 'Complete' : 'Next' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@ -145,8 +145,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LeftArrowIcon, RightArrowIcon } from "@modrinth/assets";
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import { LeftArrowIcon, RightArrowIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled } from '@modrinth/ui'
|
||||
import type {
|
||||
ModerationFlameModpackItem,
|
||||
ModerationJudgements,
|
||||
@ -155,19 +155,19 @@ import type {
|
||||
ModerationModpackResponse,
|
||||
ModerationPermissionType,
|
||||
ModerationUnknownModpackItem,
|
||||
} from "@modrinth/utils";
|
||||
import { useLocalStorage, useSessionStorage } from "@vueuse/core";
|
||||
import { computed, onMounted,ref, watch } from "vue";
|
||||
} from '@modrinth/utils'
|
||||
import { useLocalStorage, useSessionStorage } from '@vueuse/core'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
projectId: string;
|
||||
modelValue?: ModerationJudgements;
|
||||
}>();
|
||||
projectId: string
|
||||
modelValue?: ModerationJudgements
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
complete: [];
|
||||
"update:modelValue": [judgements: ModerationJudgements];
|
||||
}>();
|
||||
complete: []
|
||||
'update:modelValue': [judgements: ModerationJudgements]
|
||||
}>()
|
||||
|
||||
const persistedModPackData = useLocalStorage<ModerationModpackItem[] | null>(
|
||||
`modpack-permissions-${props.projectId}`,
|
||||
@ -178,9 +178,9 @@ const persistedModPackData = useLocalStorage<ModerationModpackItem[] | null>(
|
||||
write: (v: any) => JSON.stringify(v),
|
||||
},
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
const persistedIndex = useLocalStorage<number>(`modpack-permissions-index-${props.projectId}`, 0);
|
||||
const persistedIndex = useLocalStorage<number>(`modpack-permissions-index-${props.projectId}`, 0)
|
||||
|
||||
const modPackData = useSessionStorage<ModerationModpackItem[] | null>(
|
||||
`modpack-permissions-data-${props.projectId}`,
|
||||
@ -191,7 +191,7 @@ const modPackData = useSessionStorage<ModerationModpackItem[] | null>(
|
||||
write: (v: any) => JSON.stringify(v),
|
||||
},
|
||||
},
|
||||
);
|
||||
)
|
||||
const permanentNoFiles = useSessionStorage<ModerationModpackItem[]>(
|
||||
`modpack-permissions-permanent-no-${props.projectId}`,
|
||||
[],
|
||||
@ -201,110 +201,110 @@ const permanentNoFiles = useSessionStorage<ModerationModpackItem[]>(
|
||||
write: (v: any) => JSON.stringify(v),
|
||||
},
|
||||
},
|
||||
);
|
||||
const currentIndex = ref(0);
|
||||
)
|
||||
const currentIndex = ref(0)
|
||||
|
||||
const fileApprovalTypes: ModerationModpackPermissionApprovalType[] = [
|
||||
{
|
||||
id: "yes",
|
||||
name: "Yes",
|
||||
id: 'yes',
|
||||
name: 'Yes',
|
||||
},
|
||||
{
|
||||
id: "with-attribution-and-source",
|
||||
name: "With attribution and source",
|
||||
id: 'with-attribution-and-source',
|
||||
name: 'With attribution and source',
|
||||
},
|
||||
{
|
||||
id: "with-attribution",
|
||||
name: "With attribution",
|
||||
id: 'with-attribution',
|
||||
name: 'With attribution',
|
||||
},
|
||||
{
|
||||
id: "no",
|
||||
name: "No",
|
||||
id: 'no',
|
||||
name: 'No',
|
||||
},
|
||||
{
|
||||
id: "permanent-no",
|
||||
name: "Permanent no",
|
||||
id: 'permanent-no',
|
||||
name: 'Permanent no',
|
||||
},
|
||||
{
|
||||
id: "unidentified",
|
||||
name: "Unidentified",
|
||||
id: 'unidentified',
|
||||
name: 'Unidentified',
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const filePermissionTypes: ModerationPermissionType[] = [
|
||||
{ id: "yes", name: "Yes" },
|
||||
{ id: "no", name: "No" },
|
||||
];
|
||||
{ id: 'yes', name: 'Yes' },
|
||||
{ id: 'no', name: 'No' },
|
||||
]
|
||||
|
||||
function persistAll() {
|
||||
persistedModPackData.value = modPackData.value;
|
||||
persistedIndex.value = currentIndex.value;
|
||||
persistedModPackData.value = modPackData.value
|
||||
persistedIndex.value = currentIndex.value
|
||||
}
|
||||
|
||||
watch(
|
||||
modPackData,
|
||||
(newValue) => {
|
||||
persistedModPackData.value = newValue;
|
||||
persistedModPackData.value = newValue
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
)
|
||||
|
||||
watch(currentIndex, (newValue) => {
|
||||
persistedIndex.value = newValue;
|
||||
});
|
||||
persistedIndex.value = newValue
|
||||
})
|
||||
|
||||
function loadPersistedData(): void {
|
||||
if (persistedModPackData.value) {
|
||||
modPackData.value = persistedModPackData.value;
|
||||
modPackData.value = persistedModPackData.value
|
||||
}
|
||||
currentIndex.value = persistedIndex.value;
|
||||
currentIndex.value = persistedIndex.value
|
||||
}
|
||||
|
||||
function clearPersistedData(): void {
|
||||
persistedModPackData.value = null;
|
||||
persistedIndex.value = 0;
|
||||
persistedModPackData.value = null
|
||||
persistedIndex.value = 0
|
||||
}
|
||||
|
||||
async function fetchModPackData(): Promise<void> {
|
||||
try {
|
||||
const data = (await useBaseFetch(`moderation/project/${props.projectId}`, {
|
||||
internal: true,
|
||||
})) as ModerationModpackResponse;
|
||||
})) as ModerationModpackResponse
|
||||
|
||||
const permanentNoItems: ModerationModpackItem[] = Object.entries(data.identified || {})
|
||||
.filter(([_, file]) => file.status === "permanent-no")
|
||||
.filter(([_, file]) => file.status === 'permanent-no')
|
||||
.map(
|
||||
([sha1, file]): ModerationModpackItem => ({
|
||||
sha1,
|
||||
file_name: file.file_name,
|
||||
type: "identified",
|
||||
type: 'identified',
|
||||
status: file.status,
|
||||
approved: null,
|
||||
}),
|
||||
)
|
||||
.sort((a, b) => a.file_name.localeCompare(b.file_name));
|
||||
.sort((a, b) => a.file_name.localeCompare(b.file_name))
|
||||
|
||||
permanentNoFiles.value = permanentNoItems;
|
||||
permanentNoFiles.value = permanentNoItems
|
||||
|
||||
const sortedData: ModerationModpackItem[] = [
|
||||
...Object.entries(data.identified || {})
|
||||
.filter(
|
||||
([_, file]) =>
|
||||
file.status !== "yes" &&
|
||||
file.status !== "with-attribution-and-source" &&
|
||||
file.status !== "permanent-no",
|
||||
file.status !== 'yes' &&
|
||||
file.status !== 'with-attribution-and-source' &&
|
||||
file.status !== 'permanent-no',
|
||||
)
|
||||
.map(
|
||||
([sha1, file]): ModerationModpackItem => ({
|
||||
sha1,
|
||||
file_name: file.file_name,
|
||||
type: "identified",
|
||||
type: 'identified',
|
||||
status: file.status,
|
||||
approved: null,
|
||||
...(file.status === "unidentified" && {
|
||||
proof: "",
|
||||
url: "",
|
||||
title: "",
|
||||
...(file.status === 'unidentified' && {
|
||||
proof: '',
|
||||
url: '',
|
||||
title: '',
|
||||
}),
|
||||
}),
|
||||
)
|
||||
@ -314,12 +314,12 @@ async function fetchModPackData(): Promise<void> {
|
||||
([sha1, fileName]): ModerationUnknownModpackItem => ({
|
||||
sha1,
|
||||
file_name: fileName,
|
||||
type: "unknown",
|
||||
type: 'unknown',
|
||||
status: null,
|
||||
approved: null,
|
||||
proof: "",
|
||||
url: "",
|
||||
title: "",
|
||||
proof: '',
|
||||
url: '',
|
||||
title: '',
|
||||
}),
|
||||
)
|
||||
.sort((a, b) => a.file_name.localeCompare(b.file_name)),
|
||||
@ -328,7 +328,7 @@ async function fetchModPackData(): Promise<void> {
|
||||
([sha1, info]): ModerationFlameModpackItem => ({
|
||||
sha1,
|
||||
file_name: info.file_name,
|
||||
type: "flame",
|
||||
type: 'flame',
|
||||
status: null,
|
||||
approved: null,
|
||||
id: info.id,
|
||||
@ -337,166 +337,166 @@ async function fetchModPackData(): Promise<void> {
|
||||
}),
|
||||
)
|
||||
.sort((a, b) => a.file_name.localeCompare(b.file_name)),
|
||||
];
|
||||
]
|
||||
|
||||
if (modPackData.value) {
|
||||
const existingMap = new Map(modPackData.value.map((item) => [item.sha1, item]));
|
||||
const existingMap = new Map(modPackData.value.map((item) => [item.sha1, item]))
|
||||
|
||||
sortedData.forEach((item) => {
|
||||
const existing = existingMap.get(item.sha1);
|
||||
const existing = existingMap.get(item.sha1)
|
||||
if (existing) {
|
||||
Object.assign(item, {
|
||||
status: existing.status,
|
||||
approved: existing.approved,
|
||||
...(item.type === "unknown" && {
|
||||
proof: (existing as ModerationUnknownModpackItem).proof || "",
|
||||
url: (existing as ModerationUnknownModpackItem).url || "",
|
||||
title: (existing as ModerationUnknownModpackItem).title || "",
|
||||
...(item.type === 'unknown' && {
|
||||
proof: (existing as ModerationUnknownModpackItem).proof || '',
|
||||
url: (existing as ModerationUnknownModpackItem).url || '',
|
||||
title: (existing as ModerationUnknownModpackItem).title || '',
|
||||
}),
|
||||
...(item.type === "flame" && {
|
||||
...(item.type === 'flame' && {
|
||||
url: (existing as ModerationFlameModpackItem).url || item.url,
|
||||
title: (existing as ModerationFlameModpackItem).title || item.title,
|
||||
}),
|
||||
});
|
||||
})
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
modPackData.value = sortedData;
|
||||
persistAll();
|
||||
modPackData.value = sortedData
|
||||
persistAll()
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch modpack data:", error);
|
||||
modPackData.value = [];
|
||||
permanentNoFiles.value = [];
|
||||
persistAll();
|
||||
console.error('Failed to fetch modpack data:', error)
|
||||
modPackData.value = []
|
||||
permanentNoFiles.value = []
|
||||
persistAll()
|
||||
}
|
||||
}
|
||||
|
||||
function goToPrevious(): void {
|
||||
if (currentIndex.value > 0) {
|
||||
currentIndex.value--;
|
||||
persistAll();
|
||||
currentIndex.value--
|
||||
persistAll()
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
modPackData,
|
||||
(newValue) => {
|
||||
persistedModPackData.value = newValue;
|
||||
persistedModPackData.value = newValue
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
)
|
||||
|
||||
function goToNext(): void {
|
||||
if (modPackData.value && currentIndex.value < modPackData.value.length) {
|
||||
currentIndex.value++;
|
||||
currentIndex.value++
|
||||
|
||||
if (currentIndex.value >= modPackData.value.length) {
|
||||
const judgements = getJudgements();
|
||||
emit("update:modelValue", judgements);
|
||||
emit("complete");
|
||||
clearPersistedData();
|
||||
const judgements = getJudgements()
|
||||
emit('update:modelValue', judgements)
|
||||
emit('complete')
|
||||
clearPersistedData()
|
||||
} else {
|
||||
persistAll();
|
||||
persistAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setStatus(index: number, status: ModerationModpackPermissionApprovalType["id"]): void {
|
||||
function setStatus(index: number, status: ModerationModpackPermissionApprovalType['id']): void {
|
||||
if (modPackData.value && modPackData.value[index]) {
|
||||
modPackData.value[index].status = status;
|
||||
modPackData.value[index].approved = null;
|
||||
persistAll();
|
||||
emit("update:modelValue", getJudgements());
|
||||
modPackData.value[index].status = status
|
||||
modPackData.value[index].approved = null
|
||||
persistAll()
|
||||
emit('update:modelValue', getJudgements())
|
||||
}
|
||||
}
|
||||
|
||||
function setApproval(index: number, approved: ModerationPermissionType["id"]): void {
|
||||
function setApproval(index: number, approved: ModerationPermissionType['id']): void {
|
||||
if (modPackData.value && modPackData.value[index]) {
|
||||
modPackData.value[index].approved = approved;
|
||||
persistAll();
|
||||
emit("update:modelValue", getJudgements());
|
||||
modPackData.value[index].approved = approved
|
||||
persistAll()
|
||||
emit('update:modelValue', getJudgements())
|
||||
}
|
||||
}
|
||||
|
||||
const canGoNext = computed(() => {
|
||||
if (!modPackData.value || !modPackData.value[currentIndex.value]) return false;
|
||||
const current = modPackData.value[currentIndex.value];
|
||||
return current.status !== null;
|
||||
});
|
||||
if (!modPackData.value || !modPackData.value[currentIndex.value]) return false
|
||||
const current = modPackData.value[currentIndex.value]
|
||||
return current.status !== null
|
||||
})
|
||||
|
||||
function getJudgements(): ModerationJudgements {
|
||||
if (!modPackData.value) return {};
|
||||
if (!modPackData.value) return {}
|
||||
|
||||
const judgements: ModerationJudgements = {};
|
||||
const judgements: ModerationJudgements = {}
|
||||
|
||||
modPackData.value.forEach((item) => {
|
||||
if (item.type === "flame") {
|
||||
if (item.type === 'flame') {
|
||||
judgements[item.sha1] = {
|
||||
type: "flame",
|
||||
type: 'flame',
|
||||
id: item.id,
|
||||
status: item.status,
|
||||
link: item.url,
|
||||
title: item.title,
|
||||
file_name: item.file_name,
|
||||
};
|
||||
} else if (item.type === "unknown") {
|
||||
}
|
||||
} else if (item.type === 'unknown') {
|
||||
judgements[item.sha1] = {
|
||||
type: "unknown",
|
||||
type: 'unknown',
|
||||
status: item.status,
|
||||
proof: item.proof,
|
||||
link: item.url,
|
||||
title: item.title,
|
||||
file_name: item.file_name,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
return judgements;
|
||||
return judgements
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadPersistedData();
|
||||
loadPersistedData()
|
||||
if (!modPackData.value) {
|
||||
fetchModPackData();
|
||||
fetchModPackData()
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
watch(
|
||||
modPackData,
|
||||
(newValue) => {
|
||||
if (newValue && newValue.length === 0) {
|
||||
emit("complete");
|
||||
clearPersistedData();
|
||||
emit('complete')
|
||||
clearPersistedData()
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.projectId,
|
||||
() => {
|
||||
clearPersistedData();
|
||||
loadPersistedData();
|
||||
clearPersistedData()
|
||||
loadPersistedData()
|
||||
if (!modPackData.value) {
|
||||
fetchModPackData();
|
||||
fetchModPackData()
|
||||
}
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
function getModpackFiles(): {
|
||||
interactive: ModerationModpackItem[];
|
||||
permanentNo: ModerationModpackItem[];
|
||||
interactive: ModerationModpackItem[]
|
||||
permanentNo: ModerationModpackItem[]
|
||||
} {
|
||||
return {
|
||||
interactive: modPackData.value || [],
|
||||
permanentNo: permanentNoFiles.value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
getModpackFiles,
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { computed, defineComponent, h, onBeforeUnmount, ref, watch } from "vue";
|
||||
import { computed, defineComponent, h, onBeforeUnmount, ref, watch } from 'vue'
|
||||
|
||||
import { startLoading, stopLoading, useNuxtApp } from "#imports";
|
||||
import { startLoading, stopLoading, useNuxtApp } from '#imports'
|
||||
|
||||
export default defineComponent({
|
||||
name: "ModrinthLoadingIndicator",
|
||||
name: 'ModrinthLoadingIndicator',
|
||||
props: {
|
||||
throttle: {
|
||||
type: Number,
|
||||
@ -20,115 +20,115 @@ export default defineComponent({
|
||||
color: {
|
||||
type: [String, Boolean],
|
||||
default:
|
||||
"repeating-linear-gradient(to right, var(--color-green) 0%, var(--landing-green-label) 100%)",
|
||||
'repeating-linear-gradient(to right, var(--color-green) 0%, var(--landing-green-label) 100%)',
|
||||
},
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
const indicator = useLoadingIndicator({
|
||||
duration: props.duration,
|
||||
throttle: props.throttle,
|
||||
});
|
||||
})
|
||||
|
||||
const nuxtApp = useNuxtApp();
|
||||
nuxtApp.hook("page:start", () => {
|
||||
startLoading();
|
||||
indicator.start();
|
||||
});
|
||||
nuxtApp.hook("page:finish", () => {
|
||||
stopLoading();
|
||||
indicator.finish();
|
||||
});
|
||||
onBeforeUnmount(() => indicator.clear);
|
||||
const nuxtApp = useNuxtApp()
|
||||
nuxtApp.hook('page:start', () => {
|
||||
startLoading()
|
||||
indicator.start()
|
||||
})
|
||||
nuxtApp.hook('page:finish', () => {
|
||||
stopLoading()
|
||||
indicator.finish()
|
||||
})
|
||||
onBeforeUnmount(() => indicator.clear)
|
||||
|
||||
const loading = useLoading();
|
||||
const loading = useLoading()
|
||||
|
||||
watch(loading, (newValue) => {
|
||||
if (newValue) {
|
||||
indicator.start();
|
||||
indicator.start()
|
||||
} else {
|
||||
indicator.finish();
|
||||
indicator.finish()
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
return () =>
|
||||
h(
|
||||
"div",
|
||||
'div',
|
||||
{
|
||||
class: "nuxt-loading-indicator",
|
||||
class: 'nuxt-loading-indicator',
|
||||
style: {
|
||||
position: "fixed",
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
pointerEvents: "none",
|
||||
pointerEvents: 'none',
|
||||
width: `${indicator.progress.value}%`,
|
||||
height: `${props.height}px`,
|
||||
opacity: indicator.isLoading.value ? 1 : 0,
|
||||
background: props.color || undefined,
|
||||
backgroundSize: `${(100 / indicator.progress.value) * 100}% auto`,
|
||||
transition: "width 0.1s, height 0.4s, opacity 0.4s",
|
||||
transition: 'width 0.1s, height 0.4s, opacity 0.4s',
|
||||
zIndex: 999999,
|
||||
},
|
||||
},
|
||||
slots,
|
||||
);
|
||||
)
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
function useLoadingIndicator(opts: { duration: number; throttle: number }) {
|
||||
const progress = ref(0);
|
||||
const isLoading = ref(false);
|
||||
const step = computed(() => 10000 / opts.duration);
|
||||
const progress = ref(0)
|
||||
const isLoading = ref(false)
|
||||
const step = computed(() => 10000 / opts.duration)
|
||||
|
||||
let _timer: any = null;
|
||||
let _throttle: any = null;
|
||||
let _timer: any = null
|
||||
let _throttle: any = null
|
||||
|
||||
function start() {
|
||||
clear();
|
||||
progress.value = 0;
|
||||
clear()
|
||||
progress.value = 0
|
||||
if (opts.throttle && import.meta.client) {
|
||||
_throttle = setTimeout(() => {
|
||||
isLoading.value = true;
|
||||
_startTimer();
|
||||
}, opts.throttle);
|
||||
isLoading.value = true
|
||||
_startTimer()
|
||||
}, opts.throttle)
|
||||
} else {
|
||||
isLoading.value = true;
|
||||
_startTimer();
|
||||
isLoading.value = true
|
||||
_startTimer()
|
||||
}
|
||||
}
|
||||
function finish() {
|
||||
progress.value = 100;
|
||||
_hide();
|
||||
progress.value = 100
|
||||
_hide()
|
||||
}
|
||||
|
||||
function clear() {
|
||||
clearInterval(_timer);
|
||||
clearTimeout(_throttle);
|
||||
_timer = null;
|
||||
_throttle = null;
|
||||
clearInterval(_timer)
|
||||
clearTimeout(_throttle)
|
||||
_timer = null
|
||||
_throttle = null
|
||||
}
|
||||
|
||||
function _increase(num: number) {
|
||||
progress.value = Math.min(100, progress.value + num);
|
||||
progress.value = Math.min(100, progress.value + num)
|
||||
}
|
||||
|
||||
function _hide() {
|
||||
clear();
|
||||
clear()
|
||||
if (import.meta.client) {
|
||||
setTimeout(() => {
|
||||
isLoading.value = false;
|
||||
isLoading.value = false
|
||||
setTimeout(() => {
|
||||
progress.value = 0;
|
||||
}, 400);
|
||||
}, 500);
|
||||
progress.value = 0
|
||||
}, 400)
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
function _startTimer() {
|
||||
if (import.meta.client) {
|
||||
_timer = setInterval(() => {
|
||||
_increase(step.value);
|
||||
}, 100);
|
||||
_increase(step.value)
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
@ -138,5 +138,5 @@ function useLoadingIndicator(opts: { duration: number; throttle: number }) {
|
||||
start,
|
||||
finish,
|
||||
clear,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,10 +25,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NewspaperIcon } from "@modrinth/assets";
|
||||
import { articles as rawArticles } from "@modrinth/blog";
|
||||
import { ButtonStyled, NewsArticleCard } from "@modrinth/ui";
|
||||
import { computed,ref } from "vue";
|
||||
import { NewspaperIcon } from '@modrinth/assets'
|
||||
import { articles as rawArticles } from '@modrinth/blog'
|
||||
import { ButtonStyled, NewsArticleCard } from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const articles = ref(
|
||||
rawArticles
|
||||
@ -43,7 +43,7 @@ const articles = ref(
|
||||
date: article.date,
|
||||
}))
|
||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()),
|
||||
);
|
||||
)
|
||||
|
||||
const latestArticles = computed(() => articles.value.slice(0, 3));
|
||||
const latestArticles = computed(() => articles.value.slice(0, 3))
|
||||
</script>
|
||||
|
||||
@ -103,15 +103,15 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ReportIcon, UnknownIcon, VersionIcon } from "@modrinth/assets";
|
||||
import { Avatar, Badge, CopyCode, useRelativeTime } from "@modrinth/ui";
|
||||
import { formatProjectType } from "@modrinth/utils";
|
||||
import { ReportIcon, UnknownIcon, VersionIcon } from '@modrinth/assets'
|
||||
import { Avatar, Badge, CopyCode, useRelativeTime } from '@modrinth/ui'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
|
||||
import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
|
||||
import { renderHighlightedString } from "~/helpers/highlight.js";
|
||||
import { getProjectTypeForUrl } from "~/helpers/projects.js";
|
||||
import ThreadSummary from '~/components/ui/thread/ThreadSummary.vue'
|
||||
import { renderHighlightedString } from '~/helpers/highlight.js'
|
||||
import { getProjectTypeForUrl } from '~/helpers/projects.js'
|
||||
|
||||
const formatRelativeTime = useRelativeTime();
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
|
||||
defineProps({
|
||||
report: {
|
||||
@ -138,9 +138,9 @@ defineProps({
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const flags = useFeatureFlags();
|
||||
const flags = useFeatureFlags()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -21,10 +21,10 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
|
||||
import ReportInfo from "~/components/ui/report/ReportInfo.vue";
|
||||
import ConversationThread from "~/components/ui/thread/ConversationThread.vue";
|
||||
import { addReportMessage } from "~/helpers/threads.js";
|
||||
import Breadcrumbs from '~/components/ui/Breadcrumbs.vue'
|
||||
import ReportInfo from '~/components/ui/report/ReportInfo.vue'
|
||||
import ConversationThread from '~/components/ui/thread/ConversationThread.vue'
|
||||
import { addReportMessage } from '~/helpers/threads.js'
|
||||
|
||||
const props = defineProps({
|
||||
reportId: {
|
||||
@ -39,76 +39,76 @@ const props = defineProps({
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const report = ref(null);
|
||||
const report = ref(null)
|
||||
|
||||
await fetchReport().then((result) => {
|
||||
report.value = result;
|
||||
});
|
||||
report.value = result
|
||||
})
|
||||
|
||||
const { data: rawThread } = await useAsyncData(`thread/${report.value.thread_id}`, () =>
|
||||
useBaseFetch(`thread/${report.value.thread_id}`),
|
||||
);
|
||||
const thread = computed(() => addReportMessage(rawThread.value, report.value));
|
||||
)
|
||||
const thread = computed(() => addReportMessage(rawThread.value, report.value))
|
||||
|
||||
async function updateThread(newThread) {
|
||||
rawThread.value = newThread;
|
||||
report.value = await fetchReport();
|
||||
rawThread.value = newThread
|
||||
report.value = await fetchReport()
|
||||
}
|
||||
|
||||
async function fetchReport() {
|
||||
const { data: rawReport } = await useAsyncData(`report/${props.reportId}`, () =>
|
||||
useBaseFetch(`report/${props.reportId}`),
|
||||
);
|
||||
rawReport.value.item_id = rawReport.value.item_id.replace(/"/g, "");
|
||||
)
|
||||
rawReport.value.item_id = rawReport.value.item_id.replace(/"/g, '')
|
||||
|
||||
const userIds = [];
|
||||
userIds.push(rawReport.value.reporter);
|
||||
if (rawReport.value.item_type === "user") {
|
||||
userIds.push(rawReport.value.item_id);
|
||||
const userIds = []
|
||||
userIds.push(rawReport.value.reporter)
|
||||
if (rawReport.value.item_type === 'user') {
|
||||
userIds.push(rawReport.value.item_id)
|
||||
}
|
||||
|
||||
const versionId = rawReport.value.item_type === "version" ? rawReport.value.item_id : null;
|
||||
const versionId = rawReport.value.item_type === 'version' ? rawReport.value.item_id : null
|
||||
|
||||
let users = [];
|
||||
let users = []
|
||||
if (userIds.length > 0) {
|
||||
const { data: usersVal } = await useAsyncData(`users?ids=${JSON.stringify(userIds)}`, () =>
|
||||
useBaseFetch(`users?ids=${encodeURIComponent(JSON.stringify(userIds))}`),
|
||||
);
|
||||
users = usersVal.value;
|
||||
)
|
||||
users = usersVal.value
|
||||
}
|
||||
|
||||
let version = null;
|
||||
let version = null
|
||||
if (versionId) {
|
||||
const { data: versionVal } = await useAsyncData(`version/${versionId}`, () =>
|
||||
useBaseFetch(`version/${versionId}`),
|
||||
);
|
||||
version = versionVal.value;
|
||||
)
|
||||
version = versionVal.value
|
||||
}
|
||||
|
||||
const projectId = version
|
||||
? version.project_id
|
||||
: rawReport.value.item_type === "project"
|
||||
: rawReport.value.item_type === 'project'
|
||||
? rawReport.value.item_id
|
||||
: null;
|
||||
: null
|
||||
|
||||
let project = null;
|
||||
let project = null
|
||||
if (projectId) {
|
||||
const { data: projectVal } = await useAsyncData(`project/${projectId}`, () =>
|
||||
useBaseFetch(`project/${projectId}`),
|
||||
);
|
||||
project = projectVal.value;
|
||||
)
|
||||
project = projectVal.value
|
||||
}
|
||||
|
||||
const reportData = rawReport.value;
|
||||
reportData.project = project;
|
||||
reportData.version = version;
|
||||
reportData.reporterUser = users.find((user) => user.id === rawReport.value.reporter);
|
||||
if (rawReport.value.item_type === "user") {
|
||||
reportData.user = users.find((user) => user.id === rawReport.value.item_id);
|
||||
const reportData = rawReport.value
|
||||
reportData.project = project
|
||||
reportData.version = version
|
||||
reportData.reporterUser = users.find((user) => user.id === rawReport.value.reporter)
|
||||
if (rawReport.value.item_type === 'user') {
|
||||
reportData.user = users.find((user) => user.id === rawReport.value.item_id)
|
||||
}
|
||||
return reportData;
|
||||
return reportData
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -24,11 +24,11 @@
|
||||
<p v-if="reports.length === 0">You don't have any active reports.</p>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Chips } from "@modrinth/ui";
|
||||
import { Chips } from '@modrinth/ui'
|
||||
|
||||
import ReportInfo from "~/components/ui/report/ReportInfo.vue";
|
||||
import { addReportMessage } from "~/helpers/threads.js";
|
||||
import { asEncodedJsonArray, fetchSegmented } from "~/utils/fetch-helpers.ts";
|
||||
import ReportInfo from '~/components/ui/report/ReportInfo.vue'
|
||||
import { addReportMessage } from '~/helpers/threads.js'
|
||||
import { asEncodedJsonArray, fetchSegmented } from '~/utils/fetch-helpers.ts'
|
||||
|
||||
const props = defineProps({
|
||||
moderation: {
|
||||
@ -39,34 +39,34 @@ const props = defineProps({
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const viewMode = ref("open");
|
||||
const reasonFilter = ref("All");
|
||||
const reports = ref([]);
|
||||
const viewMode = ref('open')
|
||||
const reasonFilter = ref('All')
|
||||
const reports = ref([])
|
||||
|
||||
const MAX_REPORTS = 1500;
|
||||
const MAX_REPORTS = 1500
|
||||
|
||||
let { data: rawReports } = await useAsyncData("report", () =>
|
||||
let { data: rawReports } = await useAsyncData('report', () =>
|
||||
useBaseFetch(`report?count=${MAX_REPORTS}`),
|
||||
);
|
||||
)
|
||||
|
||||
rawReports = rawReports.value.map((report) => {
|
||||
report.item_id = report.item_id.replace(/"/g, "");
|
||||
return report;
|
||||
});
|
||||
report.item_id = report.item_id.replace(/"/g, '')
|
||||
return report
|
||||
})
|
||||
|
||||
const reporterUsers = rawReports.map((report) => report.reporter);
|
||||
const reporterUsers = rawReports.map((report) => report.reporter)
|
||||
const reportedUsers = rawReports
|
||||
.filter((report) => report.item_type === "user")
|
||||
.map((report) => report.item_id);
|
||||
const versionReports = rawReports.filter((report) => report.item_type === "version");
|
||||
const versionIds = [...new Set(versionReports.map((report) => report.item_id))];
|
||||
const userIds = [...new Set(reporterUsers.concat(reportedUsers))];
|
||||
.filter((report) => report.item_type === 'user')
|
||||
.map((report) => report.item_id)
|
||||
const versionReports = rawReports.filter((report) => report.item_type === 'version')
|
||||
const versionIds = [...new Set(versionReports.map((report) => report.item_id))]
|
||||
const userIds = [...new Set(reporterUsers.concat(reportedUsers))]
|
||||
const threadIds = [
|
||||
...new Set(rawReports.filter((report) => report.thread_id).map((report) => report.thread_id)),
|
||||
];
|
||||
const reasons = ["All", ...new Set(rawReports.map((report) => report.report_type))];
|
||||
]
|
||||
const reasons = ['All', ...new Set(rawReports.map((report) => report.report_type))]
|
||||
|
||||
const [{ data: users }, { data: versions }, { data: threads }] = await Promise.all([
|
||||
await useAsyncData(`users?ids=${JSON.stringify(userIds)}`, () =>
|
||||
@ -78,44 +78,44 @@ const [{ data: users }, { data: versions }, { data: threads }] = await Promise.a
|
||||
await useAsyncData(`threads?ids=${JSON.stringify(threadIds)}`, () =>
|
||||
fetchSegmented(threadIds, (ids) => `threads?ids=${asEncodedJsonArray(ids)}`),
|
||||
),
|
||||
]);
|
||||
])
|
||||
|
||||
const reportedProjects = rawReports
|
||||
.filter((report) => report.item_type === "project")
|
||||
.map((report) => report.item_id);
|
||||
const versionProjects = versions.value.map((version) => version.project_id);
|
||||
const projectIds = [...new Set(reportedProjects.concat(versionProjects))];
|
||||
.filter((report) => report.item_type === 'project')
|
||||
.map((report) => report.item_id)
|
||||
const versionProjects = versions.value.map((version) => version.project_id)
|
||||
const projectIds = [...new Set(reportedProjects.concat(versionProjects))]
|
||||
|
||||
const { data: projects } = await useAsyncData(`projects?ids=${JSON.stringify(projectIds)}`, () =>
|
||||
fetchSegmented(projectIds, (ids) => `projects?ids=${asEncodedJsonArray(ids)}`),
|
||||
);
|
||||
)
|
||||
|
||||
reports.value = rawReports.map((report) => {
|
||||
report.reporterUser = users.value.find((user) => user.id === report.reporter);
|
||||
if (report.item_type === "user") {
|
||||
report.user = users.value.find((user) => user.id === report.item_id);
|
||||
} else if (report.item_type === "project") {
|
||||
report.project = projects.value.find((project) => project.id === report.item_id);
|
||||
} else if (report.item_type === "version") {
|
||||
report.version = versions.value.find((version) => version.id === report.item_id);
|
||||
report.project = projects.value.find((project) => project.id === report.version.project_id);
|
||||
report.reporterUser = users.value.find((user) => user.id === report.reporter)
|
||||
if (report.item_type === 'user') {
|
||||
report.user = users.value.find((user) => user.id === report.item_id)
|
||||
} else if (report.item_type === 'project') {
|
||||
report.project = projects.value.find((project) => project.id === report.item_id)
|
||||
} else if (report.item_type === 'version') {
|
||||
report.version = versions.value.find((version) => version.id === report.item_id)
|
||||
report.project = projects.value.find((project) => project.id === report.version.project_id)
|
||||
}
|
||||
if (report.thread_id) {
|
||||
report.thread = addReportMessage(
|
||||
threads.value.find((thread) => report.thread_id === thread.id),
|
||||
report,
|
||||
);
|
||||
)
|
||||
}
|
||||
report.open = true;
|
||||
return report;
|
||||
});
|
||||
report.open = true
|
||||
return report
|
||||
})
|
||||
|
||||
const filteredReports = computed(() =>
|
||||
reports.value?.filter(
|
||||
(x) =>
|
||||
(props.moderation || x.reporterUser.id === props.auth.user.id) &&
|
||||
(viewMode.value === "open" ? x.open : !x.open) &&
|
||||
(reasonFilter.value === "All" || reasonFilter.value === x.report_type),
|
||||
(viewMode.value === 'open' ? x.open : !x.open) &&
|
||||
(reasonFilter.value === 'All' || reasonFilter.value === x.report_type),
|
||||
),
|
||||
);
|
||||
)
|
||||
</script>
|
||||
|
||||
@ -10,14 +10,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { formatCategory } from "@modrinth/utils";
|
||||
import { formatCategory } from '@modrinth/utils'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
categories: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
return []
|
||||
},
|
||||
},
|
||||
type: {
|
||||
@ -26,9 +26,9 @@ export default {
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const tags = useTags();
|
||||
const tags = useTags()
|
||||
|
||||
return { tags };
|
||||
return { tags }
|
||||
},
|
||||
computed: {
|
||||
categoriesFiltered() {
|
||||
@ -37,11 +37,11 @@ export default {
|
||||
.filter(
|
||||
(x) =>
|
||||
this.categories.includes(x.name) && (!x.project_type || x.project_type === this.type),
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
methods: { formatCategory },
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Checkbox from "~/components/ui/Checkbox.vue";
|
||||
import Checkbox from '~/components/ui/Checkbox.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -25,30 +25,30 @@ export default {
|
||||
props: {
|
||||
facetName: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
displayName: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
activeFilters: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
return []
|
||||
},
|
||||
},
|
||||
},
|
||||
emits: ["toggle"],
|
||||
emits: ['toggle'],
|
||||
methods: {
|
||||
toggle() {
|
||||
this.$emit("toggle", this.facetName);
|
||||
this.$emit('toggle', this.facetName)
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -42,83 +42,83 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { IssuesIcon, PlusIcon, XIcon } from "@modrinth/assets";
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { ModrinthServersFetchError, type ServerBackup } from "@modrinth/utils";
|
||||
import { computed,nextTick, ref } from "vue";
|
||||
import { IssuesIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, NewModal } from '@modrinth/ui'
|
||||
import { ModrinthServersFetchError, type ServerBackup } from '@modrinth/utils'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
|
||||
import type { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||
|
||||
const props = defineProps<{
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
server: ModrinthServer
|
||||
}>()
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>();
|
||||
const input = ref<HTMLInputElement>();
|
||||
const isCreating = ref(false);
|
||||
const isRateLimited = ref(false);
|
||||
const backupName = ref("");
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
const input = ref<HTMLInputElement>()
|
||||
const isCreating = ref(false)
|
||||
const isRateLimited = ref(false)
|
||||
const backupName = ref('')
|
||||
const newBackupAmount = computed(() =>
|
||||
props.server.backups?.data?.length === undefined ? 1 : props.server.backups?.data?.length + 1,
|
||||
);
|
||||
)
|
||||
|
||||
const trimmedName = computed(() => backupName.value.trim());
|
||||
const trimmedName = computed(() => backupName.value.trim())
|
||||
|
||||
const nameExists = computed(() => {
|
||||
if (!props.server.backups?.data) return false;
|
||||
if (!props.server.backups?.data) return false
|
||||
return props.server.backups.data.some(
|
||||
(backup: ServerBackup) => backup.name.trim().toLowerCase() === trimmedName.value.toLowerCase(),
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
const focusInput = () => {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
input.value?.focus();
|
||||
}, 100);
|
||||
});
|
||||
};
|
||||
input.value?.focus()
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
function show() {
|
||||
backupName.value = "";
|
||||
isCreating.value = false;
|
||||
modal.value?.show();
|
||||
backupName.value = ''
|
||||
isCreating.value = false
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
modal.value?.hide();
|
||||
};
|
||||
modal.value?.hide()
|
||||
}
|
||||
|
||||
const createBackup = async () => {
|
||||
if (backupName.value.trim().length === 0) {
|
||||
backupName.value = `Backup #${newBackupAmount.value}`;
|
||||
backupName.value = `Backup #${newBackupAmount.value}`
|
||||
}
|
||||
|
||||
isCreating.value = true;
|
||||
isRateLimited.value = false;
|
||||
isCreating.value = true
|
||||
isRateLimited.value = false
|
||||
try {
|
||||
await props.server.backups?.create(trimmedName.value);
|
||||
hideModal();
|
||||
await props.server.refresh();
|
||||
await props.server.backups?.create(trimmedName.value)
|
||||
hideModal()
|
||||
await props.server.refresh()
|
||||
} catch (error) {
|
||||
if (error instanceof ModrinthServersFetchError && error?.statusCode === 429) {
|
||||
isRateLimited.value = true;
|
||||
isRateLimited.value = true
|
||||
addNotification({
|
||||
type: "error",
|
||||
title: "Error creating backup",
|
||||
type: 'error',
|
||||
title: 'Error creating backup',
|
||||
text: "You're creating backups too fast.",
|
||||
});
|
||||
})
|
||||
} else {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
addNotification({ type: "error", title: "Error creating backup", text: message });
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
addNotification({ type: 'error', title: 'Error creating backup', text: message })
|
||||
}
|
||||
} finally {
|
||||
isCreating.value = false;
|
||||
isCreating.value = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
hide: hideModal,
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -18,25 +18,25 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ConfirmModal } from "@modrinth/ui";
|
||||
import type { Backup } from "@modrinth/utils";
|
||||
import { ref } from "vue";
|
||||
import { ConfirmModal } from '@modrinth/ui'
|
||||
import type { Backup } from '@modrinth/utils'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import BackupItem from "~/components/ui/servers/BackupItem.vue";
|
||||
import BackupItem from '~/components/ui/servers/BackupItem.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "delete", backup: Backup | undefined): void;
|
||||
}>();
|
||||
(e: 'delete', backup: Backup | undefined): void
|
||||
}>()
|
||||
|
||||
const modal = ref<InstanceType<typeof ConfirmModal>>();
|
||||
const currentBackup = ref<Backup | undefined>(undefined);
|
||||
const modal = ref<InstanceType<typeof ConfirmModal>>()
|
||||
const currentBackup = ref<Backup | undefined>(undefined)
|
||||
|
||||
function show(backup: Backup) {
|
||||
currentBackup.value = backup;
|
||||
modal.value?.show();
|
||||
currentBackup.value = backup
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -12,160 +12,160 @@ import {
|
||||
SpinnerIcon,
|
||||
TrashIcon,
|
||||
XIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { ButtonStyled, commonMessages, OverflowMenu, ProgressBar } from "@modrinth/ui";
|
||||
import type { Backup } from "@modrinth/utils";
|
||||
import { defineMessages, useVIntl } from "@vintl/vintl";
|
||||
import dayjs from "dayjs";
|
||||
import { computed,ref } from "vue";
|
||||
} from '@modrinth/assets'
|
||||
import { ButtonStyled, commonMessages, OverflowMenu, ProgressBar } from '@modrinth/ui'
|
||||
import type { Backup } from '@modrinth/utils'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const flags = useFeatureFlags();
|
||||
const { formatMessage } = useVIntl();
|
||||
const flags = useFeatureFlags()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "prepare" | "download" | "rename" | "restore" | "lock" | "retry"): void;
|
||||
(e: "delete", skipConfirmation?: boolean): void;
|
||||
}>();
|
||||
(e: 'prepare' | 'download' | 'rename' | 'restore' | 'lock' | 'retry'): void
|
||||
(e: 'delete', skipConfirmation?: boolean): void
|
||||
}>()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
backup: Backup;
|
||||
preview?: boolean;
|
||||
kyrosUrl?: string;
|
||||
jwt?: string;
|
||||
backup: Backup
|
||||
preview?: boolean
|
||||
kyrosUrl?: string
|
||||
jwt?: string
|
||||
}>(),
|
||||
{
|
||||
preview: false,
|
||||
kyrosUrl: undefined,
|
||||
jwt: undefined,
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
const backupQueued = computed(
|
||||
() =>
|
||||
props.backup.task?.create?.progress === 0 ||
|
||||
(props.backup.ongoing && !props.backup.task?.create),
|
||||
);
|
||||
const automated = computed(() => props.backup.automated);
|
||||
const failedToCreate = computed(() => props.backup.interrupted);
|
||||
)
|
||||
const automated = computed(() => props.backup.automated)
|
||||
const failedToCreate = computed(() => props.backup.interrupted)
|
||||
|
||||
const preparedDownloadStates = ["ready", "done"];
|
||||
const inactiveStates = ["failed", "cancelled"];
|
||||
const preparedDownloadStates = ['ready', 'done']
|
||||
const inactiveStates = ['failed', 'cancelled']
|
||||
|
||||
const hasPreparedDownload = computed(() => {
|
||||
const fileState = props.backup.task?.file?.state ?? "";
|
||||
return preparedDownloadStates.includes(fileState);
|
||||
});
|
||||
const fileState = props.backup.task?.file?.state ?? ''
|
||||
return preparedDownloadStates.includes(fileState)
|
||||
})
|
||||
|
||||
const creating = computed(() => {
|
||||
const task = props.backup.task?.create;
|
||||
const task = props.backup.task?.create
|
||||
if (task && task.progress < 1 && !inactiveStates.includes(task.state)) {
|
||||
return task;
|
||||
return task
|
||||
}
|
||||
if (props.backup.ongoing) {
|
||||
return {
|
||||
progress: 0,
|
||||
state: "ongoing",
|
||||
};
|
||||
state: 'ongoing',
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const restoring = computed(() => {
|
||||
const task = props.backup.task?.restore;
|
||||
const task = props.backup.task?.restore
|
||||
if (task && task.progress < 1 && !inactiveStates.includes(task.state)) {
|
||||
return task;
|
||||
return task
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
return undefined
|
||||
})
|
||||
|
||||
const initiatedPrepare = ref(false);
|
||||
const initiatedPrepare = ref(false)
|
||||
|
||||
const preparingFile = computed(() => {
|
||||
if (hasPreparedDownload.value) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
const task = props.backup.task?.file;
|
||||
const task = props.backup.task?.file
|
||||
return (
|
||||
(!task && initiatedPrepare.value) ||
|
||||
(task && task.progress < 1 && !inactiveStates.includes(task.state))
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
const failedToRestore = computed(() => props.backup.task?.restore?.state === "failed");
|
||||
const failedToPrepareFile = computed(() => props.backup.task?.file?.state === "failed");
|
||||
const failedToRestore = computed(() => props.backup.task?.restore?.state === 'failed')
|
||||
const failedToPrepareFile = computed(() => props.backup.task?.file?.state === 'failed')
|
||||
|
||||
const messages = defineMessages({
|
||||
locked: {
|
||||
id: "servers.backups.item.locked",
|
||||
defaultMessage: "Locked",
|
||||
id: 'servers.backups.item.locked',
|
||||
defaultMessage: 'Locked',
|
||||
},
|
||||
lock: {
|
||||
id: "servers.backups.item.lock",
|
||||
defaultMessage: "Lock",
|
||||
id: 'servers.backups.item.lock',
|
||||
defaultMessage: 'Lock',
|
||||
},
|
||||
unlock: {
|
||||
id: "servers.backups.item.unlock",
|
||||
defaultMessage: "Unlock",
|
||||
id: 'servers.backups.item.unlock',
|
||||
defaultMessage: 'Unlock',
|
||||
},
|
||||
restore: {
|
||||
id: "servers.backups.item.restore",
|
||||
defaultMessage: "Restore",
|
||||
id: 'servers.backups.item.restore',
|
||||
defaultMessage: 'Restore',
|
||||
},
|
||||
rename: {
|
||||
id: "servers.backups.item.rename",
|
||||
defaultMessage: "Rename",
|
||||
id: 'servers.backups.item.rename',
|
||||
defaultMessage: 'Rename',
|
||||
},
|
||||
queuedForBackup: {
|
||||
id: "servers.backups.item.queued-for-backup",
|
||||
defaultMessage: "Queued for backup",
|
||||
id: 'servers.backups.item.queued-for-backup',
|
||||
defaultMessage: 'Queued for backup',
|
||||
},
|
||||
preparingDownload: {
|
||||
id: "servers.backups.item.preparing-download",
|
||||
defaultMessage: "Preparing download...",
|
||||
id: 'servers.backups.item.preparing-download',
|
||||
defaultMessage: 'Preparing download...',
|
||||
},
|
||||
prepareDownload: {
|
||||
id: "servers.backups.item.prepare-download",
|
||||
defaultMessage: "Prepare download",
|
||||
id: 'servers.backups.item.prepare-download',
|
||||
defaultMessage: 'Prepare download',
|
||||
},
|
||||
prepareDownloadAgain: {
|
||||
id: "servers.backups.item.prepare-download-again",
|
||||
defaultMessage: "Try preparing again",
|
||||
id: 'servers.backups.item.prepare-download-again',
|
||||
defaultMessage: 'Try preparing again',
|
||||
},
|
||||
alreadyPreparing: {
|
||||
id: "servers.backups.item.already-preparing",
|
||||
defaultMessage: "Already preparing backup for download",
|
||||
id: 'servers.backups.item.already-preparing',
|
||||
defaultMessage: 'Already preparing backup for download',
|
||||
},
|
||||
creatingBackup: {
|
||||
id: "servers.backups.item.creating-backup",
|
||||
defaultMessage: "Creating backup...",
|
||||
id: 'servers.backups.item.creating-backup',
|
||||
defaultMessage: 'Creating backup...',
|
||||
},
|
||||
restoringBackup: {
|
||||
id: "servers.backups.item.restoring-backup",
|
||||
defaultMessage: "Restoring from backup...",
|
||||
id: 'servers.backups.item.restoring-backup',
|
||||
defaultMessage: 'Restoring from backup...',
|
||||
},
|
||||
failedToCreateBackup: {
|
||||
id: "servers.backups.item.failed-to-create-backup",
|
||||
defaultMessage: "Failed to create backup",
|
||||
id: 'servers.backups.item.failed-to-create-backup',
|
||||
defaultMessage: 'Failed to create backup',
|
||||
},
|
||||
failedToRestoreBackup: {
|
||||
id: "servers.backups.item.failed-to-restore-backup",
|
||||
defaultMessage: "Failed to restore from backup",
|
||||
id: 'servers.backups.item.failed-to-restore-backup',
|
||||
defaultMessage: 'Failed to restore from backup',
|
||||
},
|
||||
failedToPrepareFile: {
|
||||
id: "servers.backups.item.failed-to-prepare-backup",
|
||||
defaultMessage: "Failed to prepare download",
|
||||
id: 'servers.backups.item.failed-to-prepare-backup',
|
||||
defaultMessage: 'Failed to prepare download',
|
||||
},
|
||||
automated: {
|
||||
id: "servers.backups.item.automated",
|
||||
defaultMessage: "Automated",
|
||||
id: 'servers.backups.item.automated',
|
||||
defaultMessage: 'Automated',
|
||||
},
|
||||
retry: {
|
||||
id: "servers.backups.item.retry",
|
||||
defaultMessage: "Retry",
|
||||
id: 'servers.backups.item.retry',
|
||||
defaultMessage: 'Retry',
|
||||
},
|
||||
});
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
@ -239,7 +239,7 @@ const messages = defineMessages({
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="col-span-2">
|
||||
{{ dayjs(backup.created_at).format("MMMM D, YYYY [at] h:mm A") }}
|
||||
{{ dayjs(backup.created_at).format('MMMM D, YYYY [at] h:mm A') }}
|
||||
</div>
|
||||
<div v-if="false">{{ 245 }} MiB</div>
|
||||
</template>
|
||||
@ -285,8 +285,8 @@ const messages = defineMessages({
|
||||
:disabled="!!preparingFile"
|
||||
@click="
|
||||
() => {
|
||||
initiatedPrepare = true;
|
||||
emit('prepare');
|
||||
initiatedPrepare = true
|
||||
emit('prepare')
|
||||
}
|
||||
"
|
||||
>
|
||||
|
||||
@ -45,98 +45,98 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { IssuesIcon,SaveIcon, SpinnerIcon, XIcon } from "@modrinth/assets";
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import type { Backup } from "@modrinth/utils";
|
||||
import { computed,nextTick, ref } from "vue";
|
||||
import { IssuesIcon, SaveIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, NewModal } from '@modrinth/ui'
|
||||
import type { Backup } from '@modrinth/utils'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
|
||||
import type { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||
|
||||
const props = defineProps<{
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
server: ModrinthServer
|
||||
}>()
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>();
|
||||
const input = ref<HTMLInputElement>();
|
||||
const backupName = ref("");
|
||||
const originalName = ref("");
|
||||
const isRenaming = ref(false);
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
const input = ref<HTMLInputElement>()
|
||||
const backupName = ref('')
|
||||
const originalName = ref('')
|
||||
const isRenaming = ref(false)
|
||||
|
||||
const currentBackup = ref<Backup | null>(null);
|
||||
const currentBackup = ref<Backup | null>(null)
|
||||
|
||||
const trimmedName = computed(() => backupName.value.trim());
|
||||
const trimmedName = computed(() => backupName.value.trim())
|
||||
|
||||
const nameExists = computed(() => {
|
||||
if (!props.server.backups?.data || trimmedName.value === originalName.value || isRenaming.value) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
return props.server.backups.data.some(
|
||||
(backup: Backup) => backup.name.trim().toLowerCase() === trimmedName.value.toLowerCase(),
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
const backupNumber = computed(
|
||||
() => (props.server.backups?.data?.findIndex((b) => b.id === currentBackup.value?.id) ?? 0) + 1,
|
||||
);
|
||||
)
|
||||
|
||||
const focusInput = () => {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
input.value?.focus();
|
||||
}, 100);
|
||||
});
|
||||
};
|
||||
input.value?.focus()
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
function show(backup: Backup) {
|
||||
currentBackup.value = backup;
|
||||
backupName.value = backup.name;
|
||||
originalName.value = backup.name;
|
||||
isRenaming.value = false;
|
||||
modal.value?.show();
|
||||
currentBackup.value = backup
|
||||
backupName.value = backup.name
|
||||
originalName.value = backup.name
|
||||
isRenaming.value = false
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
modal.value?.hide();
|
||||
modal.value?.hide()
|
||||
}
|
||||
|
||||
const renameBackup = async () => {
|
||||
if (!currentBackup.value) {
|
||||
addNotification({
|
||||
type: "error",
|
||||
title: "Error renaming backup",
|
||||
text: "Current backup is null",
|
||||
});
|
||||
return;
|
||||
type: 'error',
|
||||
title: 'Error renaming backup',
|
||||
text: 'Current backup is null',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (trimmedName.value === originalName.value) {
|
||||
hide();
|
||||
return;
|
||||
hide()
|
||||
return
|
||||
}
|
||||
|
||||
isRenaming.value = true;
|
||||
isRenaming.value = true
|
||||
try {
|
||||
let newName = trimmedName.value;
|
||||
let newName = trimmedName.value
|
||||
|
||||
if (newName.length === 0) {
|
||||
newName = `Backup #${backupNumber.value}`;
|
||||
newName = `Backup #${backupNumber.value}`
|
||||
}
|
||||
|
||||
await props.server.backups?.rename(currentBackup.value.id, newName);
|
||||
hide();
|
||||
await props.server.refresh();
|
||||
await props.server.backups?.rename(currentBackup.value.id, newName)
|
||||
hide()
|
||||
await props.server.refresh()
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
addNotification({ type: "error", title: "Error renaming backup", text: message });
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
addNotification({ type: 'error', title: 'Error renaming backup', text: message })
|
||||
} finally {
|
||||
hide();
|
||||
isRenaming.value = false;
|
||||
hide()
|
||||
isRenaming.value = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
hide,
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -17,45 +17,45 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { NewModal } from "@modrinth/ui";
|
||||
import { ConfirmModal } from "@modrinth/ui";
|
||||
import type { Backup } from "@modrinth/utils";
|
||||
import { ref } from "vue";
|
||||
import type { NewModal } from '@modrinth/ui'
|
||||
import { ConfirmModal } from '@modrinth/ui'
|
||||
import type { Backup } from '@modrinth/utils'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import BackupItem from "~/components/ui/servers/BackupItem.vue";
|
||||
import type { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
import BackupItem from '~/components/ui/servers/BackupItem.vue'
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||
|
||||
const props = defineProps<{
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
server: ModrinthServer
|
||||
}>()
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>();
|
||||
const currentBackup = ref<Backup | null>(null);
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
const currentBackup = ref<Backup | null>(null)
|
||||
|
||||
function show(backup: Backup) {
|
||||
currentBackup.value = backup;
|
||||
modal.value?.show();
|
||||
currentBackup.value = backup
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
const restoreBackup = async () => {
|
||||
if (!currentBackup.value) {
|
||||
addNotification({
|
||||
type: "error",
|
||||
title: "Failed to restore backup",
|
||||
text: "Current backup is null",
|
||||
});
|
||||
return;
|
||||
type: 'error',
|
||||
title: 'Failed to restore backup',
|
||||
text: 'Current backup is null',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await props.server.backups?.restore(currentBackup.value.id);
|
||||
await props.server.backups?.restore(currentBackup.value.id)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
addNotification({ type: "error", title: "Failed to restore backup", text: message });
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
addNotification({ type: 'error', title: 'Failed to restore backup', text: message })
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -40,7 +40,7 @@
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="!hasChanges || isSaving" @click="saveSettings">
|
||||
<SaveIcon class="h-5 w-5" />
|
||||
{{ isSaving ? "Saving..." : "Save changes" }}
|
||||
{{ isSaving ? 'Saving...' : 'Save changes' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
@ -56,114 +56,114 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SaveIcon,XIcon } from "@modrinth/assets";
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { computed,ref } from "vue";
|
||||
import { SaveIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, NewModal } from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import type { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||
|
||||
const props = defineProps<{
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
server: ModrinthServer
|
||||
}>()
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>();
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
|
||||
const initialSettings = ref<{ interval: number; enabled: boolean } | null>(null);
|
||||
const autoBackupEnabled = ref(false);
|
||||
const isLoadingSettings = ref(true);
|
||||
const isSaving = ref(false);
|
||||
const initialSettings = ref<{ interval: number; enabled: boolean } | null>(null)
|
||||
const autoBackupEnabled = ref(false)
|
||||
const isLoadingSettings = ref(true)
|
||||
const isSaving = ref(false)
|
||||
|
||||
const backupIntervals = {
|
||||
"Every 3 hours": 3,
|
||||
"Every 6 hours": 6,
|
||||
"Every 12 hours": 12,
|
||||
'Every 3 hours': 3,
|
||||
'Every 6 hours': 6,
|
||||
'Every 12 hours': 12,
|
||||
Daily: 24,
|
||||
};
|
||||
}
|
||||
|
||||
const backupIntervalsLabel = ref<keyof typeof backupIntervals>("Every 6 hours");
|
||||
const backupIntervalsLabel = ref<keyof typeof backupIntervals>('Every 6 hours')
|
||||
|
||||
const autoBackupInterval = computed({
|
||||
get: () => backupIntervals[backupIntervalsLabel.value],
|
||||
set: (value) => {
|
||||
const [label] =
|
||||
Object.entries(backupIntervals).find(([_, interval]) => interval === value) || [];
|
||||
if (label) backupIntervalsLabel.value = label as keyof typeof backupIntervals;
|
||||
Object.entries(backupIntervals).find(([_, interval]) => interval === value) || []
|
||||
if (label) backupIntervalsLabel.value = label as keyof typeof backupIntervals
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const hasChanges = computed(() => {
|
||||
if (!initialSettings.value) return false;
|
||||
if (!initialSettings.value) return false
|
||||
|
||||
return (
|
||||
autoBackupEnabled.value !== initialSettings.value.enabled ||
|
||||
(initialSettings.value.enabled && autoBackupInterval.value !== initialSettings.value.interval)
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
const fetchSettings = async () => {
|
||||
isLoadingSettings.value = true;
|
||||
isLoadingSettings.value = true
|
||||
try {
|
||||
const settings = await props.server.backups?.getAutoBackup();
|
||||
initialSettings.value = settings as { interval: number; enabled: boolean };
|
||||
autoBackupEnabled.value = settings?.enabled ?? false;
|
||||
autoBackupInterval.value = settings?.interval || 6;
|
||||
return true;
|
||||
const settings = await props.server.backups?.getAutoBackup()
|
||||
initialSettings.value = settings as { interval: number; enabled: boolean }
|
||||
autoBackupEnabled.value = settings?.enabled ?? false
|
||||
autoBackupInterval.value = settings?.interval || 6
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error("Error fetching backup settings:", error);
|
||||
console.error('Error fetching backup settings:', error)
|
||||
addNotification({
|
||||
group: "server",
|
||||
title: "Error",
|
||||
text: "Failed to load backup settings",
|
||||
type: "error",
|
||||
});
|
||||
return false;
|
||||
group: 'server',
|
||||
title: 'Error',
|
||||
text: 'Failed to load backup settings',
|
||||
type: 'error',
|
||||
})
|
||||
return false
|
||||
} finally {
|
||||
isLoadingSettings.value = false;
|
||||
isLoadingSettings.value = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const saveSettings = async () => {
|
||||
isSaving.value = true;
|
||||
isSaving.value = true
|
||||
try {
|
||||
await props.server.backups?.updateAutoBackup(
|
||||
autoBackupEnabled.value ? "enable" : "disable",
|
||||
autoBackupEnabled.value ? 'enable' : 'disable',
|
||||
autoBackupInterval.value,
|
||||
);
|
||||
)
|
||||
|
||||
initialSettings.value = {
|
||||
enabled: autoBackupEnabled.value,
|
||||
interval: autoBackupInterval.value,
|
||||
};
|
||||
|
||||
addNotification({
|
||||
group: "server",
|
||||
title: "Success",
|
||||
text: "Backup settings updated successfully",
|
||||
type: "success",
|
||||
});
|
||||
|
||||
modal.value?.hide();
|
||||
} catch (error) {
|
||||
console.error("Error saving backup settings:", error);
|
||||
addNotification({
|
||||
group: "server",
|
||||
title: "Error",
|
||||
text: "Failed to save backup settings",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
addNotification({
|
||||
group: 'server',
|
||||
title: 'Success',
|
||||
text: 'Backup settings updated successfully',
|
||||
type: 'success',
|
||||
})
|
||||
|
||||
modal.value?.hide()
|
||||
} catch (error) {
|
||||
console.error('Error saving backup settings:', error)
|
||||
addNotification({
|
||||
group: 'server',
|
||||
title: 'Error',
|
||||
text: 'Failed to save backup settings',
|
||||
type: 'error',
|
||||
})
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show: async () => {
|
||||
const success = await fetchSettings();
|
||||
const success = await fetchSettings()
|
||||
if (success) {
|
||||
modal.value?.show();
|
||||
modal.value?.show()
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -89,8 +89,8 @@
|
||||
noCompatibleVersions
|
||||
? `No compatible versions of this ${type.toLowerCase()} were found`
|
||||
: versionFilter
|
||||
? "Game version and platform is provided by the server"
|
||||
: "Incompatible game version and platform versions are unlocked"
|
||||
? 'Game version and platform is provided by the server'
|
||||
: 'Incompatible game version and platform versions are unlocked'
|
||||
}}
|
||||
</span>
|
||||
</p>
|
||||
@ -133,8 +133,8 @@
|
||||
<div class="w-full truncate text-left">
|
||||
{{
|
||||
filtersRef?.selectedPlatforms.length === 0
|
||||
? "All platforms"
|
||||
: filtersRef?.selectedPlatforms.map((x) => formatCategory(x)).join(", ")
|
||||
? 'All platforms'
|
||||
: filtersRef?.selectedPlatforms.map((x) => formatCategory(x)).join(', ')
|
||||
}}
|
||||
</div>
|
||||
</template>
|
||||
@ -143,8 +143,8 @@
|
||||
<div class="w-full truncate text-left">
|
||||
{{
|
||||
filtersRef?.selectedGameVersions.length === 0
|
||||
? "All game versions"
|
||||
: filtersRef?.selectedGameVersions.join(", ")
|
||||
? 'All game versions'
|
||||
: filtersRef?.selectedGameVersions.join(', ')
|
||||
}}
|
||||
</div>
|
||||
</template>
|
||||
@ -156,19 +156,19 @@
|
||||
:disabled="gameVersions.length < 2 && platforms.length < 2"
|
||||
@click="
|
||||
() => {
|
||||
versionFilter = !versionFilter;
|
||||
setInitialFilters();
|
||||
updateFiltersToUi();
|
||||
versionFilter = !versionFilter
|
||||
setInitialFilters()
|
||||
updateFiltersToUi()
|
||||
}
|
||||
"
|
||||
>
|
||||
<LockOpenIcon />
|
||||
{{
|
||||
gameVersions.length < 2 && platforms.length < 2
|
||||
? "No other platforms or versions available"
|
||||
? 'No other platforms or versions available'
|
||||
: versionFilter
|
||||
? "Unlock"
|
||||
: "Return to compatibility"
|
||||
? 'Unlock'
|
||||
: 'Return to compatibility'
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
@ -235,81 +235,81 @@ import {
|
||||
GameIcon,
|
||||
LockOpenIcon,
|
||||
XIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { Admonition, Avatar, ButtonStyled, CopyCode, NewModal } from "@modrinth/ui";
|
||||
import TagItem from "@modrinth/ui/src/components/base/TagItem.vue";
|
||||
import { formatCategory, formatVersionsForDisplay, type Mod, type Version } from "@modrinth/utils";
|
||||
import { computed,ref } from "vue";
|
||||
} from '@modrinth/assets'
|
||||
import { Admonition, Avatar, ButtonStyled, CopyCode, NewModal } from '@modrinth/ui'
|
||||
import TagItem from '@modrinth/ui/src/components/base/TagItem.vue'
|
||||
import { formatCategory, formatVersionsForDisplay, type Mod, type Version } from '@modrinth/utils'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import Accordion from "~/components/ui/Accordion.vue";
|
||||
import Checkbox from "~/components/ui/Checkbox.vue";
|
||||
import Accordion from '~/components/ui/Accordion.vue'
|
||||
import Checkbox from '~/components/ui/Checkbox.vue'
|
||||
import ContentVersionFilter, {
|
||||
type ListedGameVersion,
|
||||
type ListedPlatform,
|
||||
} from "~/components/ui/servers/ContentVersionFilter.vue";
|
||||
import LoaderIcon from "~/components/ui/servers/icons/LoaderIcon.vue";
|
||||
} from '~/components/ui/servers/ContentVersionFilter.vue'
|
||||
import LoaderIcon from '~/components/ui/servers/icons/LoaderIcon.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
type: "Mod" | "Plugin";
|
||||
loader: string;
|
||||
gameVersion: string;
|
||||
modPack: boolean;
|
||||
serverId: string;
|
||||
}>();
|
||||
type: 'Mod' | 'Plugin'
|
||||
loader: string
|
||||
gameVersion: string
|
||||
modPack: boolean
|
||||
serverId: string
|
||||
}>()
|
||||
|
||||
interface ContentItem extends Mod {
|
||||
changing?: boolean;
|
||||
changing?: boolean
|
||||
}
|
||||
|
||||
interface EditVersion extends Version {
|
||||
installed: boolean;
|
||||
upgrade?: boolean;
|
||||
installed: boolean
|
||||
upgrade?: boolean
|
||||
}
|
||||
|
||||
const modModal = ref();
|
||||
const modDetails = ref<ContentItem>();
|
||||
const currentVersions = ref<EditVersion[] | null>(null);
|
||||
const versionsLoading = ref(false);
|
||||
const versionsError = ref("");
|
||||
const showBetaAlphaReleases = ref(false);
|
||||
const unlockFilterAccordion = ref();
|
||||
const versionFilter = ref(true);
|
||||
const tags = useTags();
|
||||
const noCompatibleVersions = ref(false);
|
||||
const modModal = ref()
|
||||
const modDetails = ref<ContentItem>()
|
||||
const currentVersions = ref<EditVersion[] | null>(null)
|
||||
const versionsLoading = ref(false)
|
||||
const versionsError = ref('')
|
||||
const showBetaAlphaReleases = ref(false)
|
||||
const unlockFilterAccordion = ref()
|
||||
const versionFilter = ref(true)
|
||||
const tags = useTags()
|
||||
const noCompatibleVersions = ref(false)
|
||||
|
||||
const { pluginLoaders, modLoaders } = tags.value.loaders.reduce(
|
||||
(acc, tag) => {
|
||||
if (tag.supported_project_types.includes("plugin")) {
|
||||
acc.pluginLoaders.push(tag.name);
|
||||
if (tag.supported_project_types.includes('plugin')) {
|
||||
acc.pluginLoaders.push(tag.name)
|
||||
}
|
||||
if (tag.supported_project_types.includes("mod")) {
|
||||
acc.modLoaders.push(tag.name);
|
||||
if (tag.supported_project_types.includes('mod')) {
|
||||
acc.modLoaders.push(tag.name)
|
||||
}
|
||||
return acc;
|
||||
return acc
|
||||
},
|
||||
{ pluginLoaders: [] as string[], modLoaders: [] as string[] },
|
||||
);
|
||||
)
|
||||
|
||||
const selectedVersion = ref();
|
||||
const filtersRef: Ref<InstanceType<typeof ContentVersionFilter> | null> = ref(null);
|
||||
const selectedVersion = ref()
|
||||
const filtersRef: Ref<InstanceType<typeof ContentVersionFilter> | null> = ref(null)
|
||||
interface SelectedContentFilters {
|
||||
selectedGameVersions: string[];
|
||||
selectedPlatforms: string[];
|
||||
selectedGameVersions: string[]
|
||||
selectedPlatforms: string[]
|
||||
}
|
||||
const selectedFilters = ref<SelectedContentFilters>({
|
||||
selectedGameVersions: [],
|
||||
selectedPlatforms: [],
|
||||
});
|
||||
})
|
||||
|
||||
const backwardCompatPlatformMap = {
|
||||
purpur: ["purpur", "paper", "spigot", "bukkit"],
|
||||
paper: ["paper", "spigot", "bukkit"],
|
||||
spigot: ["spigot", "bukkit"],
|
||||
};
|
||||
purpur: ['purpur', 'paper', 'spigot', 'bukkit'],
|
||||
paper: ['paper', 'spigot', 'bukkit'],
|
||||
spigot: ['spigot', 'bukkit'],
|
||||
}
|
||||
|
||||
const platforms = ref<ListedPlatform[]>([]);
|
||||
const gameVersions = ref<ListedGameVersion[]>([]);
|
||||
const initPlatform = ref<string>("");
|
||||
const platforms = ref<ListedPlatform[]>([])
|
||||
const gameVersions = ref<ListedGameVersion[]>([])
|
||||
const initPlatform = ref<string>('')
|
||||
|
||||
const setInitialFilters = () => {
|
||||
selectedFilters.value = {
|
||||
@ -319,29 +319,29 @@ const setInitialFilters = () => {
|
||||
gameVersions.value[0]?.name,
|
||||
],
|
||||
selectedPlatforms: [initPlatform.value],
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const updateFiltersToUi = () => {
|
||||
if (!filtersRef.value) return;
|
||||
filtersRef.value.selectedGameVersions = selectedFilters.value.selectedGameVersions;
|
||||
filtersRef.value.selectedPlatforms = selectedFilters.value.selectedPlatforms;
|
||||
if (!filtersRef.value) return
|
||||
filtersRef.value.selectedGameVersions = selectedFilters.value.selectedGameVersions
|
||||
filtersRef.value.selectedPlatforms = selectedFilters.value.selectedPlatforms
|
||||
|
||||
selectedVersion.value = filteredVersions.value[0];
|
||||
};
|
||||
selectedVersion.value = filteredVersions.value[0]
|
||||
}
|
||||
|
||||
const updateFiltersFromUi = (event: { g: string[]; l: string[] }) => {
|
||||
selectedFilters.value = {
|
||||
selectedGameVersions: event.g,
|
||||
selectedPlatforms: event.l,
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const filteredVersions = computed(() => {
|
||||
if (!currentVersions.value) return [];
|
||||
if (!currentVersions.value) return []
|
||||
|
||||
const versionsWithoutReleaseFilter = currentVersions.value.filter((version: EditVersion) => {
|
||||
if (version.installed) return true;
|
||||
if (version.installed) return true
|
||||
return (
|
||||
filtersRef.value?.selectedPlatforms.every((platform) =>
|
||||
(
|
||||
@ -353,42 +353,40 @@ const filteredVersions = computed(() => {
|
||||
filtersRef.value?.selectedGameVersions.every((gameVersion) =>
|
||||
version.game_versions.includes(gameVersion),
|
||||
)
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
const versionTypes = new Set(
|
||||
versionsWithoutReleaseFilter.map((v: EditVersion) => v.version_type),
|
||||
);
|
||||
const releaseVersions = versionTypes.has("release");
|
||||
const betaVersions = versionTypes.has("beta");
|
||||
const alphaVersions = versionTypes.has("alpha");
|
||||
const versionTypes = new Set(versionsWithoutReleaseFilter.map((v: EditVersion) => v.version_type))
|
||||
const releaseVersions = versionTypes.has('release')
|
||||
const betaVersions = versionTypes.has('beta')
|
||||
const alphaVersions = versionTypes.has('alpha')
|
||||
|
||||
const versions = versionsWithoutReleaseFilter.filter((version: EditVersion) => {
|
||||
if (showBetaAlphaReleases.value || version.installed) return true;
|
||||
if (showBetaAlphaReleases.value || version.installed) return true
|
||||
return releaseVersions
|
||||
? version.version_type === "release"
|
||||
? version.version_type === 'release'
|
||||
: betaVersions
|
||||
? version.version_type === "beta"
|
||||
? version.version_type === 'beta'
|
||||
: alphaVersions
|
||||
? version.version_type === "alpha"
|
||||
: false;
|
||||
});
|
||||
? version.version_type === 'alpha'
|
||||
: false
|
||||
})
|
||||
|
||||
return versions.map((version: EditVersion) => {
|
||||
let suffix = "";
|
||||
let suffix = ''
|
||||
|
||||
if (version.version_type === "alpha" && releaseVersions && betaVersions) {
|
||||
suffix += " (alpha)";
|
||||
} else if (version.version_type === "beta" && releaseVersions) {
|
||||
suffix += " (beta)";
|
||||
if (version.version_type === 'alpha' && releaseVersions && betaVersions) {
|
||||
suffix += ' (alpha)'
|
||||
} else if (version.version_type === 'beta' && releaseVersions) {
|
||||
suffix += ' (beta)'
|
||||
}
|
||||
|
||||
return {
|
||||
...version,
|
||||
version_number: version.version_number + suffix,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const formattedVersions = computed(() => {
|
||||
return {
|
||||
@ -400,134 +398,134 @@ const formattedVersions = computed(() => {
|
||||
.sort((firstLoader: string, secondLoader: string) => {
|
||||
const loaderList = backwardCompatPlatformMap[
|
||||
props.loader as keyof typeof backwardCompatPlatformMap
|
||||
] || [props.loader];
|
||||
] || [props.loader]
|
||||
|
||||
const firstLoaderPosition = loaderList.indexOf(firstLoader.toLowerCase());
|
||||
const secondLoaderPosition = loaderList.indexOf(secondLoader.toLowerCase());
|
||||
const firstLoaderPosition = loaderList.indexOf(firstLoader.toLowerCase())
|
||||
const secondLoaderPosition = loaderList.indexOf(secondLoader.toLowerCase())
|
||||
|
||||
if (firstLoaderPosition === -1 && secondLoaderPosition === -1) return 0;
|
||||
if (firstLoaderPosition === -1) return 1;
|
||||
if (secondLoaderPosition === -1) return -1;
|
||||
return firstLoaderPosition - secondLoaderPosition;
|
||||
if (firstLoaderPosition === -1 && secondLoaderPosition === -1) return 0
|
||||
if (firstLoaderPosition === -1) return 1
|
||||
if (secondLoaderPosition === -1) return -1
|
||||
return firstLoaderPosition - secondLoaderPosition
|
||||
})
|
||||
.map((loader: string) => formatCategory(loader)),
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
async function show(mod: ContentItem) {
|
||||
versionFilter.value = true;
|
||||
modModal.value.show();
|
||||
versionsLoading.value = true;
|
||||
modDetails.value = mod;
|
||||
versionsError.value = "";
|
||||
currentVersions.value = null;
|
||||
versionFilter.value = true
|
||||
modModal.value.show()
|
||||
versionsLoading.value = true
|
||||
modDetails.value = mod
|
||||
versionsError.value = ''
|
||||
currentVersions.value = null
|
||||
|
||||
try {
|
||||
const result = await useBaseFetch(`project/${mod.project_id}/version`, {}, false);
|
||||
const result = await useBaseFetch(`project/${mod.project_id}/version`, {}, false)
|
||||
if (
|
||||
Array.isArray(result) &&
|
||||
result.every(
|
||||
(item) =>
|
||||
"id" in item &&
|
||||
"version_number" in item &&
|
||||
"version_type" in item &&
|
||||
"loaders" in item &&
|
||||
"game_versions" in item,
|
||||
'id' in item &&
|
||||
'version_number' in item &&
|
||||
'version_type' in item &&
|
||||
'loaders' in item &&
|
||||
'game_versions' in item,
|
||||
)
|
||||
) {
|
||||
currentVersions.value = result as EditVersion[];
|
||||
currentVersions.value = result as EditVersion[]
|
||||
} else {
|
||||
throw new Error("Invalid version data received.");
|
||||
throw new Error('Invalid version data received.')
|
||||
}
|
||||
|
||||
// find the installed version and move it to the top of the list
|
||||
const currentModIndex = currentVersions.value.findIndex(
|
||||
(item: { id: string }) => item.id === mod.version_id,
|
||||
);
|
||||
)
|
||||
if (currentModIndex === -1) {
|
||||
currentVersions.value[currentModIndex] = {
|
||||
...currentVersions.value[currentModIndex],
|
||||
installed: true,
|
||||
version_number: `${mod.version_number} (current) (external)`,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
currentVersions.value[currentModIndex].version_number = `${mod.version_number} (current)`;
|
||||
currentVersions.value[currentModIndex].installed = true;
|
||||
currentVersions.value[currentModIndex].version_number = `${mod.version_number} (current)`
|
||||
currentVersions.value[currentModIndex].installed = true
|
||||
}
|
||||
|
||||
// initially filter the platform and game versions for the server config
|
||||
const platformSet = new Set<string>();
|
||||
const gameVersionSet = new Set<string>();
|
||||
const platformSet = new Set<string>()
|
||||
const gameVersionSet = new Set<string>()
|
||||
for (const version of currentVersions.value) {
|
||||
for (const loader of version.loaders) {
|
||||
platformSet.add(loader);
|
||||
platformSet.add(loader)
|
||||
}
|
||||
for (const gameVersion of version.game_versions) {
|
||||
gameVersionSet.add(gameVersion);
|
||||
gameVersionSet.add(gameVersion)
|
||||
}
|
||||
}
|
||||
if (gameVersionSet.size > 0) {
|
||||
const filteredGameVersions = tags.value.gameVersions.filter((x) =>
|
||||
gameVersionSet.has(x.version),
|
||||
);
|
||||
)
|
||||
|
||||
gameVersions.value = filteredGameVersions.map((x) => ({
|
||||
name: x.version,
|
||||
release: x.version_type === "release",
|
||||
}));
|
||||
release: x.version_type === 'release',
|
||||
}))
|
||||
}
|
||||
if (platformSet.size > 0) {
|
||||
const tempPlatforms = Array.from(platformSet).map((platform) => ({
|
||||
name: platform,
|
||||
isType:
|
||||
props.type === "Plugin"
|
||||
props.type === 'Plugin'
|
||||
? pluginLoaders.includes(platform)
|
||||
: props.type === "Mod"
|
||||
: props.type === 'Mod'
|
||||
? modLoaders.includes(platform)
|
||||
: false,
|
||||
}));
|
||||
platforms.value = tempPlatforms;
|
||||
}))
|
||||
platforms.value = tempPlatforms
|
||||
}
|
||||
|
||||
// set default platform
|
||||
const defaultPlatform = Array.from(platformSet)[0];
|
||||
const defaultPlatform = Array.from(platformSet)[0]
|
||||
initPlatform.value = platformSet.has(props.loader)
|
||||
? props.loader
|
||||
: props.loader in backwardCompatPlatformMap
|
||||
? backwardCompatPlatformMap[props.loader as keyof typeof backwardCompatPlatformMap].find(
|
||||
(p) => platformSet.has(p),
|
||||
) || defaultPlatform
|
||||
: defaultPlatform;
|
||||
: defaultPlatform
|
||||
|
||||
// check if there's nothing compatible with the server config
|
||||
noCompatibleVersions.value =
|
||||
!platforms.value.some((p) => p.isType) ||
|
||||
!gameVersions.value.some((v) => v.name === props.gameVersion);
|
||||
!gameVersions.value.some((v) => v.name === props.gameVersion)
|
||||
|
||||
if (noCompatibleVersions.value) {
|
||||
unlockFilterAccordion.value.open();
|
||||
versionFilter.value = false;
|
||||
unlockFilterAccordion.value.open()
|
||||
versionFilter.value = false
|
||||
}
|
||||
|
||||
setInitialFilters();
|
||||
versionsLoading.value = false;
|
||||
setInitialFilters()
|
||||
versionsLoading.value = false
|
||||
} catch (error) {
|
||||
console.error("Error loading versions:", error);
|
||||
versionsError.value = error instanceof Error ? error.message : "Unknown";
|
||||
console.error('Error loading versions:', error)
|
||||
versionsError.value = error instanceof Error ? error.message : 'Unknown'
|
||||
}
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
changeVersion: [string];
|
||||
}>();
|
||||
changeVersion: [string]
|
||||
}>()
|
||||
|
||||
function emitChangeModVersion() {
|
||||
if (!selectedVersion.value) return;
|
||||
emit("changeVersion", selectedVersion.value.id.toString());
|
||||
if (!selectedVersion.value) return
|
||||
emit('changeVersion', selectedVersion.value.id.toString())
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
hide: () => modModal.value.hide(),
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -57,114 +57,114 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FilterIcon } from "@modrinth/assets";
|
||||
import Checkbox from "@modrinth/ui/src/components/base/Checkbox.vue";
|
||||
import ManySelect from "@modrinth/ui/src/components/base/ManySelect.vue";
|
||||
import { formatCategory, type GameVersionTag,type Version } from "@modrinth/utils";
|
||||
import { computed,ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { FilterIcon } from '@modrinth/assets'
|
||||
import Checkbox from '@modrinth/ui/src/components/base/Checkbox.vue'
|
||||
import ManySelect from '@modrinth/ui/src/components/base/ManySelect.vue'
|
||||
import { formatCategory, type GameVersionTag, type Version } from '@modrinth/utils'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
export type ListedGameVersion = {
|
||||
name: string;
|
||||
release: boolean;
|
||||
};
|
||||
name: string
|
||||
release: boolean
|
||||
}
|
||||
|
||||
export type ListedPlatform = {
|
||||
name: string;
|
||||
isType: boolean;
|
||||
};
|
||||
name: string
|
||||
isType: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
versions: Version[];
|
||||
gameVersions: GameVersionTag[];
|
||||
listedGameVersions: ListedGameVersion[];
|
||||
listedPlatforms: ListedPlatform[];
|
||||
baseId?: string;
|
||||
type: "Mod" | "Plugin";
|
||||
versions: Version[]
|
||||
gameVersions: GameVersionTag[]
|
||||
listedGameVersions: ListedGameVersion[]
|
||||
listedPlatforms: ListedPlatform[]
|
||||
baseId?: string
|
||||
type: 'Mod' | 'Plugin'
|
||||
platformTags: {
|
||||
name: string;
|
||||
supported_project_types: string[];
|
||||
}[];
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
name: string
|
||||
supported_project_types: string[]
|
||||
}[]
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(["update:query"]);
|
||||
const route = useRoute();
|
||||
const emit = defineEmits(['update:query'])
|
||||
const route = useRoute()
|
||||
|
||||
const showSnapshots = ref(false);
|
||||
const showSnapshots = ref(false)
|
||||
const hasAnySnapshots = computed(() => {
|
||||
return props.versions.some((x) =>
|
||||
props.gameVersions.some(
|
||||
(y) => y.version_type !== "release" && x.game_versions.includes(y.version),
|
||||
(y) => y.version_type !== 'release' && x.game_versions.includes(y.version),
|
||||
),
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
const hasOnlySnapshots = computed(() => {
|
||||
return props.versions.every((version) => {
|
||||
return version.game_versions.every((gv) => {
|
||||
const matched = props.gameVersions.find((tag) => tag.version === gv);
|
||||
return matched && matched.version_type !== "release";
|
||||
});
|
||||
});
|
||||
});
|
||||
const matched = props.gameVersions.find((tag) => tag.version === gv)
|
||||
return matched && matched.version_type !== 'release'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const hasAnyUnsupportedPlatforms = computed(() => {
|
||||
return props.listedPlatforms.some((x) => !x.isType);
|
||||
});
|
||||
return props.listedPlatforms.some((x) => !x.isType)
|
||||
})
|
||||
|
||||
const hasOnlyUnsupportedPlatforms = computed(() => {
|
||||
return props.listedPlatforms.every((x) => !x.isType);
|
||||
});
|
||||
return props.listedPlatforms.every((x) => !x.isType)
|
||||
})
|
||||
|
||||
const showSupportedPlatformsOnly = ref(true);
|
||||
const showSupportedPlatformsOnly = ref(true)
|
||||
|
||||
const filterOptions = computed(() => {
|
||||
const filters: Record<"gameVersion" | "platform", string[]> = {
|
||||
const filters: Record<'gameVersion' | 'platform', string[]> = {
|
||||
gameVersion: [],
|
||||
platform: [],
|
||||
};
|
||||
}
|
||||
|
||||
filters.gameVersion = props.listedGameVersions
|
||||
.filter((x) => {
|
||||
return showSnapshots.value || hasOnlySnapshots.value ? true : x.release;
|
||||
return showSnapshots.value || hasOnlySnapshots.value ? true : x.release
|
||||
})
|
||||
.map((x) => x.name);
|
||||
.map((x) => x.name)
|
||||
|
||||
filters.platform = props.listedPlatforms
|
||||
.filter((x) => {
|
||||
return !showSupportedPlatformsOnly.value || hasOnlyUnsupportedPlatforms.value
|
||||
? true
|
||||
: x.isType;
|
||||
: x.isType
|
||||
})
|
||||
.map((x) => x.name);
|
||||
.map((x) => x.name)
|
||||
|
||||
return filters;
|
||||
});
|
||||
return filters
|
||||
})
|
||||
|
||||
const selectedGameVersions = ref<string[]>([]);
|
||||
const selectedPlatforms = ref<string[]>([]);
|
||||
const selectedGameVersions = ref<string[]>([])
|
||||
const selectedPlatforms = ref<string[]>([])
|
||||
|
||||
selectedGameVersions.value = route.query.g ? getArrayOrString(route.query.g) : [];
|
||||
selectedPlatforms.value = route.query.l ? getArrayOrString(route.query.l) : [];
|
||||
selectedGameVersions.value = route.query.g ? getArrayOrString(route.query.g) : []
|
||||
selectedPlatforms.value = route.query.l ? getArrayOrString(route.query.l) : []
|
||||
|
||||
function updateFilters() {
|
||||
emit("update:query", {
|
||||
emit('update:query', {
|
||||
g: selectedGameVersions.value,
|
||||
l: selectedPlatforms.value,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
selectedGameVersions,
|
||||
selectedPlatforms,
|
||||
});
|
||||
})
|
||||
|
||||
function getArrayOrString(x: string | (string | null)[]): string[] {
|
||||
if (typeof x === "string") {
|
||||
return [x];
|
||||
if (typeof x === 'string') {
|
||||
return [x]
|
||||
} else {
|
||||
return x.filter((item): item is string => item !== null);
|
||||
return x.filter((item): item is string => item !== null)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -75,11 +75,11 @@ import {
|
||||
PackageOpenIcon,
|
||||
RightArrowIcon,
|
||||
TrashIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import { computed, ref,shallowRef } from "vue";
|
||||
import { renderToString } from "vue/server-renderer";
|
||||
import { useRoute,useRouter } from "vue-router";
|
||||
} from '@modrinth/assets'
|
||||
import { ButtonStyled } from '@modrinth/ui'
|
||||
import { computed, ref, shallowRef } from 'vue'
|
||||
import { renderToString } from 'vue/server-renderer'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import {
|
||||
UiServersIconsCodeFileIcon,
|
||||
@ -87,327 +87,324 @@ import {
|
||||
UiServersIconsEarthIcon,
|
||||
UiServersIconsImageFileIcon,
|
||||
UiServersIconsTextFileIcon,
|
||||
} from "#components";
|
||||
import PaletteIcon from "~/assets/icons/palette.svg?component";
|
||||
} from '#components'
|
||||
import PaletteIcon from '~/assets/icons/palette.svg?component'
|
||||
|
||||
interface FileItemProps {
|
||||
name: string;
|
||||
type: "directory" | "file";
|
||||
size?: number;
|
||||
count?: number;
|
||||
modified: number;
|
||||
created: number;
|
||||
path: string;
|
||||
name: string
|
||||
type: 'directory' | 'file'
|
||||
size?: number
|
||||
count?: number
|
||||
modified: number
|
||||
created: number
|
||||
path: string
|
||||
}
|
||||
|
||||
const props = defineProps<FileItemProps>();
|
||||
const props = defineProps<FileItemProps>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
e: "rename" | "move" | "download" | "delete" | "edit" | "extract",
|
||||
e: 'rename' | 'move' | 'download' | 'delete' | 'edit' | 'extract',
|
||||
item: { name: string; type: string; path: string },
|
||||
): void;
|
||||
(
|
||||
e: "moveDirectTo",
|
||||
item: { name: string; type: string; path: string; destination: string },
|
||||
): void;
|
||||
(e: "contextmenu", x: number, y: number): void;
|
||||
}>();
|
||||
): void
|
||||
(e: 'moveDirectTo', item: { name: string; type: string; path: string; destination: string }): void
|
||||
(e: 'contextmenu', x: number, y: number): void
|
||||
}>()
|
||||
|
||||
const isDragOver = ref(false);
|
||||
const isDragging = ref(false);
|
||||
const isDragOver = ref(false)
|
||||
const isDragging = ref(false)
|
||||
|
||||
const codeExtensions = Object.freeze([
|
||||
"json",
|
||||
"json5",
|
||||
"jsonc",
|
||||
"java",
|
||||
"kt",
|
||||
"kts",
|
||||
"sh",
|
||||
"bat",
|
||||
"ps1",
|
||||
"yml",
|
||||
"yaml",
|
||||
"toml",
|
||||
"js",
|
||||
"ts",
|
||||
"py",
|
||||
"rb",
|
||||
"php",
|
||||
"html",
|
||||
"css",
|
||||
"cpp",
|
||||
"c",
|
||||
"h",
|
||||
"rs",
|
||||
"go",
|
||||
]);
|
||||
'json',
|
||||
'json5',
|
||||
'jsonc',
|
||||
'java',
|
||||
'kt',
|
||||
'kts',
|
||||
'sh',
|
||||
'bat',
|
||||
'ps1',
|
||||
'yml',
|
||||
'yaml',
|
||||
'toml',
|
||||
'js',
|
||||
'ts',
|
||||
'py',
|
||||
'rb',
|
||||
'php',
|
||||
'html',
|
||||
'css',
|
||||
'cpp',
|
||||
'c',
|
||||
'h',
|
||||
'rs',
|
||||
'go',
|
||||
])
|
||||
|
||||
const textExtensions = Object.freeze(["txt", "md", "log", "cfg", "conf", "properties", "ini"]);
|
||||
const imageExtensions = Object.freeze(["png", "jpg", "jpeg", "gif", "svg", "webp"]);
|
||||
const supportedArchiveExtensions = Object.freeze(["zip"]);
|
||||
const units = Object.freeze(["B", "KB", "MB", "GB", "TB", "PB", "EB"]);
|
||||
const textExtensions = Object.freeze(['txt', 'md', 'log', 'cfg', 'conf', 'properties', 'ini'])
|
||||
const imageExtensions = Object.freeze(['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp'])
|
||||
const supportedArchiveExtensions = Object.freeze(['zip'])
|
||||
const units = Object.freeze(['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'])
|
||||
|
||||
const route = shallowRef(useRoute());
|
||||
const router = useRouter();
|
||||
const route = shallowRef(useRoute())
|
||||
const router = useRouter()
|
||||
|
||||
const containerClasses = computed(() => [
|
||||
"group m-0 p-0 focus:!outline-none flex w-full select-none items-center justify-between overflow-hidden border-0 border-b border-solid border-bg-raised p-3 last:border-none hover:bg-bg-raised focus:bg-bg-raised",
|
||||
isEditableFile.value ? "cursor-pointer" : props.type === "directory" ? "cursor-pointer" : "",
|
||||
isDragOver.value ? "bg-brand-highlight" : "",
|
||||
]);
|
||||
'group m-0 p-0 focus:!outline-none flex w-full select-none items-center justify-between overflow-hidden border-0 border-b border-solid border-bg-raised p-3 last:border-none hover:bg-bg-raised focus:bg-bg-raised',
|
||||
isEditableFile.value ? 'cursor-pointer' : props.type === 'directory' ? 'cursor-pointer' : '',
|
||||
isDragOver.value ? 'bg-brand-highlight' : '',
|
||||
])
|
||||
|
||||
const fileExtension = computed(() => props.name.split(".").pop()?.toLowerCase() || "");
|
||||
const fileExtension = computed(() => props.name.split('.').pop()?.toLowerCase() || '')
|
||||
|
||||
const isZip = computed(() => fileExtension.value === "zip");
|
||||
const isZip = computed(() => fileExtension.value === 'zip')
|
||||
|
||||
const menuOptions = computed(() => [
|
||||
{
|
||||
id: "extract",
|
||||
id: 'extract',
|
||||
shown: isZip.value,
|
||||
action: () => emit("extract", { name: props.name, type: props.type, path: props.path }),
|
||||
action: () => emit('extract', { name: props.name, type: props.type, path: props.path }),
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: isZip.value,
|
||||
},
|
||||
{
|
||||
id: "rename",
|
||||
action: () => emit("rename", { name: props.name, type: props.type, path: props.path }),
|
||||
id: 'rename',
|
||||
action: () => emit('rename', { name: props.name, type: props.type, path: props.path }),
|
||||
},
|
||||
{
|
||||
id: "move",
|
||||
action: () => emit("move", { name: props.name, type: props.type, path: props.path }),
|
||||
id: 'move',
|
||||
action: () => emit('move', { name: props.name, type: props.type, path: props.path }),
|
||||
},
|
||||
{
|
||||
id: "download",
|
||||
action: () => emit("download", { name: props.name, type: props.type, path: props.path }),
|
||||
shown: props.type !== "directory",
|
||||
id: 'download',
|
||||
action: () => emit('download', { name: props.name, type: props.type, path: props.path }),
|
||||
shown: props.type !== 'directory',
|
||||
},
|
||||
{
|
||||
id: "delete",
|
||||
action: () => emit("delete", { name: props.name, type: props.type, path: props.path }),
|
||||
color: "red" as const,
|
||||
id: 'delete',
|
||||
action: () => emit('delete', { name: props.name, type: props.type, path: props.path }),
|
||||
color: 'red' as const,
|
||||
},
|
||||
]);
|
||||
])
|
||||
|
||||
const iconComponent = computed(() => {
|
||||
if (props.type === "directory") {
|
||||
if (props.name === "config") return UiServersIconsCogFolderIcon;
|
||||
if (props.name === "world") return UiServersIconsEarthIcon;
|
||||
if (props.name === "resourcepacks") return PaletteIcon;
|
||||
return FolderOpenIcon;
|
||||
if (props.type === 'directory') {
|
||||
if (props.name === 'config') return UiServersIconsCogFolderIcon
|
||||
if (props.name === 'world') return UiServersIconsEarthIcon
|
||||
if (props.name === 'resourcepacks') return PaletteIcon
|
||||
return FolderOpenIcon
|
||||
}
|
||||
|
||||
const ext = fileExtension.value;
|
||||
if (codeExtensions.includes(ext)) return UiServersIconsCodeFileIcon;
|
||||
if (textExtensions.includes(ext)) return UiServersIconsTextFileIcon;
|
||||
if (imageExtensions.includes(ext)) return UiServersIconsImageFileIcon;
|
||||
if (supportedArchiveExtensions.includes(ext)) return FileArchiveIcon;
|
||||
return FileIcon;
|
||||
});
|
||||
const ext = fileExtension.value
|
||||
if (codeExtensions.includes(ext)) return UiServersIconsCodeFileIcon
|
||||
if (textExtensions.includes(ext)) return UiServersIconsTextFileIcon
|
||||
if (imageExtensions.includes(ext)) return UiServersIconsImageFileIcon
|
||||
if (supportedArchiveExtensions.includes(ext)) return FileArchiveIcon
|
||||
return FileIcon
|
||||
})
|
||||
|
||||
const subText = computed(() => {
|
||||
if (props.type === "directory") {
|
||||
return `${props.count} ${props.count === 1 ? "item" : "items"}`;
|
||||
if (props.type === 'directory') {
|
||||
return `${props.count} ${props.count === 1 ? 'item' : 'items'}`
|
||||
}
|
||||
return formattedSize.value;
|
||||
});
|
||||
return formattedSize.value
|
||||
})
|
||||
|
||||
const formattedModifiedDate = computed(() => {
|
||||
const date = new Date(props.modified * 1000);
|
||||
return `${date.toLocaleDateString("en-US", {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
year: "2-digit",
|
||||
})}, ${date.toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
const date = new Date(props.modified * 1000)
|
||||
return `${date.toLocaleDateString('en-US', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
year: '2-digit',
|
||||
})}, ${date.toLocaleTimeString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
hour12: true,
|
||||
})}`;
|
||||
});
|
||||
})}`
|
||||
})
|
||||
|
||||
const formattedCreationDate = computed(() => {
|
||||
const date = new Date(props.created * 1000);
|
||||
return `${date.toLocaleDateString("en-US", {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
year: "2-digit",
|
||||
})}, ${date.toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
const date = new Date(props.created * 1000)
|
||||
return `${date.toLocaleDateString('en-US', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
year: '2-digit',
|
||||
})}, ${date.toLocaleTimeString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
hour12: true,
|
||||
})}`;
|
||||
});
|
||||
})}`
|
||||
})
|
||||
|
||||
const isEditableFile = computed(() => {
|
||||
if (props.type === "file") {
|
||||
const ext = fileExtension.value;
|
||||
if (props.type === 'file') {
|
||||
const ext = fileExtension.value
|
||||
return (
|
||||
!props.name.includes(".") ||
|
||||
!props.name.includes('.') ||
|
||||
textExtensions.includes(ext) ||
|
||||
codeExtensions.includes(ext) ||
|
||||
imageExtensions.includes(ext)
|
||||
);
|
||||
)
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return false
|
||||
})
|
||||
|
||||
const formattedSize = computed(() => {
|
||||
if (props.size === undefined) return "";
|
||||
const bytes = props.size;
|
||||
if (bytes === 0) return "0 B";
|
||||
if (props.size === undefined) return ''
|
||||
const bytes = props.size
|
||||
if (bytes === 0) return '0 B'
|
||||
|
||||
const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
||||
const size = (bytes / Math.pow(1024, exponent)).toFixed(2);
|
||||
return `${size} ${units[exponent]}`;
|
||||
});
|
||||
const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
|
||||
const size = (bytes / Math.pow(1024, exponent)).toFixed(2)
|
||||
return `${size} ${units[exponent]}`
|
||||
})
|
||||
|
||||
const openContextMenu = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
emit("contextmenu", event.clientX, event.clientY);
|
||||
};
|
||||
event.preventDefault()
|
||||
emit('contextmenu', event.clientX, event.clientY)
|
||||
}
|
||||
|
||||
const navigateToFolder = () => {
|
||||
const currentPath = route.value.query.path?.toString() || "";
|
||||
const newPath = currentPath.endsWith("/")
|
||||
const currentPath = route.value.query.path?.toString() || ''
|
||||
const newPath = currentPath.endsWith('/')
|
||||
? `${currentPath}${props.name}`
|
||||
: `${currentPath}/${props.name}`;
|
||||
router.push({ query: { path: newPath, page: 1 } });
|
||||
};
|
||||
: `${currentPath}/${props.name}`
|
||||
router.push({ query: { path: newPath, page: 1 } })
|
||||
}
|
||||
|
||||
const isNavigating = ref(false);
|
||||
const isNavigating = ref(false)
|
||||
|
||||
const selectItem = () => {
|
||||
if (isNavigating.value) return;
|
||||
isNavigating.value = true;
|
||||
if (isNavigating.value) return
|
||||
isNavigating.value = true
|
||||
|
||||
if (props.type === "directory") {
|
||||
navigateToFolder();
|
||||
} else if (props.type === "file" && isEditableFile.value) {
|
||||
emit("edit", { name: props.name, type: props.type, path: props.path });
|
||||
if (props.type === 'directory') {
|
||||
navigateToFolder()
|
||||
} else if (props.type === 'file' && isEditableFile.value) {
|
||||
emit('edit', { name: props.name, type: props.type, path: props.path })
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
isNavigating.value = false;
|
||||
}, 500);
|
||||
};
|
||||
isNavigating.value = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const getDragIcon = async () => {
|
||||
let iconToUse;
|
||||
let iconToUse
|
||||
|
||||
if (props.type === "directory") {
|
||||
if (props.name === "config") {
|
||||
iconToUse = UiServersIconsCogFolderIcon;
|
||||
} else if (props.name === "world") {
|
||||
iconToUse = UiServersIconsEarthIcon;
|
||||
} else if (props.name === "resourcepacks") {
|
||||
iconToUse = PaletteIcon;
|
||||
if (props.type === 'directory') {
|
||||
if (props.name === 'config') {
|
||||
iconToUse = UiServersIconsCogFolderIcon
|
||||
} else if (props.name === 'world') {
|
||||
iconToUse = UiServersIconsEarthIcon
|
||||
} else if (props.name === 'resourcepacks') {
|
||||
iconToUse = PaletteIcon
|
||||
} else {
|
||||
iconToUse = FolderOpenIcon;
|
||||
iconToUse = FolderOpenIcon
|
||||
}
|
||||
} else {
|
||||
const ext = fileExtension.value;
|
||||
const ext = fileExtension.value
|
||||
if (codeExtensions.includes(ext)) {
|
||||
iconToUse = UiServersIconsCodeFileIcon;
|
||||
iconToUse = UiServersIconsCodeFileIcon
|
||||
} else if (textExtensions.includes(ext)) {
|
||||
iconToUse = UiServersIconsTextFileIcon;
|
||||
iconToUse = UiServersIconsTextFileIcon
|
||||
} else if (imageExtensions.includes(ext)) {
|
||||
iconToUse = UiServersIconsImageFileIcon;
|
||||
iconToUse = UiServersIconsImageFileIcon
|
||||
} else {
|
||||
iconToUse = FileIcon;
|
||||
iconToUse = FileIcon
|
||||
}
|
||||
}
|
||||
|
||||
return await renderToString(h(iconToUse));
|
||||
};
|
||||
return await renderToString(h(iconToUse))
|
||||
}
|
||||
|
||||
const handleDragStart = async (event: DragEvent) => {
|
||||
if (!event.dataTransfer) return;
|
||||
isDragging.value = true;
|
||||
if (!event.dataTransfer) return
|
||||
isDragging.value = true
|
||||
|
||||
const dragGhost = document.createElement("div");
|
||||
const dragGhost = document.createElement('div')
|
||||
dragGhost.className =
|
||||
"fixed left-0 top-0 flex items-center max-w-[500px] flex-row gap-3 rounded-lg bg-bg-raised p-3 shadow-lg pointer-events-none";
|
||||
'fixed left-0 top-0 flex items-center max-w-[500px] flex-row gap-3 rounded-lg bg-bg-raised p-3 shadow-lg pointer-events-none'
|
||||
|
||||
const iconContainer = document.createElement("div");
|
||||
iconContainer.className = "flex size-6 items-center justify-center";
|
||||
const iconContainer = document.createElement('div')
|
||||
iconContainer.className = 'flex size-6 items-center justify-center'
|
||||
|
||||
const icon = document.createElement("div");
|
||||
icon.className = "size-4";
|
||||
icon.innerHTML = await getDragIcon();
|
||||
iconContainer.appendChild(icon);
|
||||
const icon = document.createElement('div')
|
||||
icon.className = 'size-4'
|
||||
icon.innerHTML = await getDragIcon()
|
||||
iconContainer.appendChild(icon)
|
||||
|
||||
const nameSpan = document.createElement("span");
|
||||
nameSpan.className = "font-bold truncate text-contrast";
|
||||
nameSpan.textContent = props.name;
|
||||
const nameSpan = document.createElement('span')
|
||||
nameSpan.className = 'font-bold truncate text-contrast'
|
||||
nameSpan.textContent = props.name
|
||||
|
||||
dragGhost.appendChild(iconContainer);
|
||||
dragGhost.appendChild(nameSpan);
|
||||
document.body.appendChild(dragGhost);
|
||||
dragGhost.appendChild(iconContainer)
|
||||
dragGhost.appendChild(nameSpan)
|
||||
document.body.appendChild(dragGhost)
|
||||
|
||||
event.dataTransfer.setDragImage(dragGhost, 0, 0);
|
||||
event.dataTransfer.setDragImage(dragGhost, 0, 0)
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
document.body.removeChild(dragGhost);
|
||||
});
|
||||
document.body.removeChild(dragGhost)
|
||||
})
|
||||
|
||||
event.dataTransfer.setData(
|
||||
"application/pyro-file-move",
|
||||
'application/pyro-file-move',
|
||||
JSON.stringify({
|
||||
name: props.name,
|
||||
type: props.type,
|
||||
path: props.path,
|
||||
}),
|
||||
);
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
};
|
||||
)
|
||||
event.dataTransfer.effectAllowed = 'move'
|
||||
}
|
||||
|
||||
const isChildPath = (parentPath: string, childPath: string) => {
|
||||
return childPath.startsWith(parentPath + "/");
|
||||
};
|
||||
return childPath.startsWith(parentPath + '/')
|
||||
}
|
||||
|
||||
const handleDragEnd = () => {
|
||||
isDragging.value = false;
|
||||
};
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
const handleDragEnter = () => {
|
||||
if (props.type !== "directory") return;
|
||||
isDragOver.value = true;
|
||||
};
|
||||
if (props.type !== 'directory') return
|
||||
isDragOver.value = true
|
||||
}
|
||||
|
||||
const handleDragOver = (event: DragEvent) => {
|
||||
if (props.type !== "directory" || !event.dataTransfer) return;
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
};
|
||||
if (props.type !== 'directory' || !event.dataTransfer) return
|
||||
event.dataTransfer.dropEffect = 'move'
|
||||
}
|
||||
|
||||
const handleDragLeave = () => {
|
||||
isDragOver.value = false;
|
||||
};
|
||||
isDragOver.value = false
|
||||
}
|
||||
|
||||
const handleDrop = (event: DragEvent) => {
|
||||
isDragOver.value = false;
|
||||
if (props.type !== "directory" || !event.dataTransfer) return;
|
||||
isDragOver.value = false
|
||||
if (props.type !== 'directory' || !event.dataTransfer) return
|
||||
|
||||
try {
|
||||
const dragData = JSON.parse(event.dataTransfer.getData("application/pyro-file-move"));
|
||||
const dragData = JSON.parse(event.dataTransfer.getData('application/pyro-file-move'))
|
||||
|
||||
if (dragData.path === props.path) return;
|
||||
if (dragData.path === props.path) return
|
||||
|
||||
if (dragData.type === "directory" && isChildPath(dragData.path, props.path)) {
|
||||
console.error("Cannot move a folder into its own subfolder");
|
||||
return;
|
||||
if (dragData.type === 'directory' && isChildPath(dragData.path, props.path)) {
|
||||
console.error('Cannot move a folder into its own subfolder')
|
||||
return
|
||||
}
|
||||
|
||||
emit("moveDirectTo", {
|
||||
emit('moveDirectTo', {
|
||||
name: dragData.name,
|
||||
type: dragData.type,
|
||||
path: dragData.path,
|
||||
destination: props.path,
|
||||
});
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error handling file drop:", error);
|
||||
console.error('Error handling file drop:', error)
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -43,79 +43,79 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted,ref } from "vue";
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
items: any[];
|
||||
}>();
|
||||
items: any[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
e: "delete" | "rename" | "download" | "move" | "edit" | "moveDirectTo" | "extract",
|
||||
e: 'delete' | 'rename' | 'download' | 'move' | 'edit' | 'moveDirectTo' | 'extract',
|
||||
item: any,
|
||||
): void;
|
||||
(e: "contextmenu", item: any, x: number, y: number): void;
|
||||
(e: "loadMore"): void;
|
||||
}>();
|
||||
): void
|
||||
(e: 'contextmenu', item: any, x: number, y: number): void
|
||||
(e: 'loadMore'): void
|
||||
}>()
|
||||
|
||||
const ITEM_HEIGHT = 61;
|
||||
const BUFFER_SIZE = 5;
|
||||
const ITEM_HEIGHT = 61
|
||||
const BUFFER_SIZE = 5
|
||||
|
||||
const listContainer = ref<HTMLElement | null>(null);
|
||||
const windowScrollY = ref(0);
|
||||
const windowHeight = ref(0);
|
||||
const listContainer = ref<HTMLElement | null>(null)
|
||||
const windowScrollY = ref(0)
|
||||
const windowHeight = ref(0)
|
||||
|
||||
const totalHeight = computed(() => props.items.length * ITEM_HEIGHT);
|
||||
const totalHeight = computed(() => props.items.length * ITEM_HEIGHT)
|
||||
|
||||
const visibleRange = computed(() => {
|
||||
if (!listContainer.value) return { start: 0, end: 0 };
|
||||
if (!listContainer.value) return { start: 0, end: 0 }
|
||||
|
||||
const containerTop = listContainer.value.getBoundingClientRect().top + window.scrollY;
|
||||
const relativeScrollTop = Math.max(0, windowScrollY.value - containerTop);
|
||||
const containerTop = listContainer.value.getBoundingClientRect().top + window.scrollY
|
||||
const relativeScrollTop = Math.max(0, windowScrollY.value - containerTop)
|
||||
|
||||
const start = Math.floor(relativeScrollTop / ITEM_HEIGHT);
|
||||
const visibleCount = Math.ceil(windowHeight.value / ITEM_HEIGHT);
|
||||
const start = Math.floor(relativeScrollTop / ITEM_HEIGHT)
|
||||
const visibleCount = Math.ceil(windowHeight.value / ITEM_HEIGHT)
|
||||
|
||||
return {
|
||||
start: Math.max(0, start - BUFFER_SIZE),
|
||||
end: Math.min(props.items.length, start + visibleCount + BUFFER_SIZE * 2),
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
const visibleTop = computed(() => {
|
||||
return visibleRange.value.start * ITEM_HEIGHT;
|
||||
});
|
||||
return visibleRange.value.start * ITEM_HEIGHT
|
||||
})
|
||||
|
||||
const visibleItems = computed(() => {
|
||||
return props.items.slice(visibleRange.value.start, visibleRange.value.end);
|
||||
});
|
||||
return props.items.slice(visibleRange.value.start, visibleRange.value.end)
|
||||
})
|
||||
|
||||
const handleScroll = () => {
|
||||
windowScrollY.value = window.scrollY;
|
||||
windowScrollY.value = window.scrollY
|
||||
|
||||
if (!listContainer.value) return;
|
||||
if (!listContainer.value) return
|
||||
|
||||
const containerBottom = listContainer.value.getBoundingClientRect().bottom;
|
||||
const remainingScroll = containerBottom - window.innerHeight;
|
||||
const containerBottom = listContainer.value.getBoundingClientRect().bottom
|
||||
const remainingScroll = containerBottom - window.innerHeight
|
||||
|
||||
if (remainingScroll < windowHeight.value * 0.2) {
|
||||
emit("loadMore");
|
||||
emit('loadMore')
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
windowHeight.value = window.innerHeight;
|
||||
};
|
||||
windowHeight.value = window.innerHeight
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
windowHeight.value = window.innerHeight;
|
||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||
window.addEventListener("resize", handleResize, { passive: true });
|
||||
handleScroll();
|
||||
});
|
||||
windowHeight.value = window.innerHeight
|
||||
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||
window.addEventListener('resize', handleResize, { passive: true })
|
||||
handleScroll()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
window.removeEventListener("resize", handleResize);
|
||||
});
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -33,64 +33,64 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PlusIcon, XIcon } from "@modrinth/assets";
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { computed, nextTick,ref } from "vue";
|
||||
import { PlusIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, NewModal } from '@modrinth/ui'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
type: "file" | "directory";
|
||||
}>();
|
||||
type: 'file' | 'directory'
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "create", name: string): void;
|
||||
}>();
|
||||
(e: 'create', name: string): void
|
||||
}>()
|
||||
|
||||
const modal = ref<typeof NewModal>();
|
||||
const displayType = computed(() => (props.type === "directory" ? "folder" : props.type));
|
||||
const createInput = ref<HTMLInputElement | null>(null);
|
||||
const itemName = ref("");
|
||||
const submitted = ref(false);
|
||||
const modal = ref<typeof NewModal>()
|
||||
const displayType = computed(() => (props.type === 'directory' ? 'folder' : props.type))
|
||||
const createInput = ref<HTMLInputElement | null>(null)
|
||||
const itemName = ref('')
|
||||
const submitted = ref(false)
|
||||
|
||||
const error = computed(() => {
|
||||
if (!itemName.value) {
|
||||
return "Name is required.";
|
||||
return 'Name is required.'
|
||||
}
|
||||
if (props.type === "file") {
|
||||
const validPattern = /^[a-zA-Z0-9-_.\s]+$/;
|
||||
if (props.type === 'file') {
|
||||
const validPattern = /^[a-zA-Z0-9-_.\s]+$/
|
||||
if (!validPattern.test(itemName.value)) {
|
||||
return "Name must contain only alphanumeric characters, dashes, underscores, dots, or spaces.";
|
||||
return 'Name must contain only alphanumeric characters, dashes, underscores, dots, or spaces.'
|
||||
}
|
||||
} else {
|
||||
const validPattern = /^[a-zA-Z0-9-_\s]+$/;
|
||||
const validPattern = /^[a-zA-Z0-9-_\s]+$/
|
||||
if (!validPattern.test(itemName.value)) {
|
||||
return "Name must contain only alphanumeric characters, dashes, underscores, or spaces.";
|
||||
return 'Name must contain only alphanumeric characters, dashes, underscores, or spaces.'
|
||||
}
|
||||
}
|
||||
return "";
|
||||
});
|
||||
return ''
|
||||
})
|
||||
|
||||
const handleSubmit = () => {
|
||||
submitted.value = true;
|
||||
submitted.value = true
|
||||
if (!error.value) {
|
||||
emit("create", itemName.value);
|
||||
hide();
|
||||
emit('create', itemName.value)
|
||||
hide()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const show = () => {
|
||||
itemName.value = "";
|
||||
submitted.value = false;
|
||||
modal.value?.show();
|
||||
itemName.value = ''
|
||||
submitted.value = false
|
||||
modal.value?.show()
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
createInput.value?.focus();
|
||||
}, 100);
|
||||
});
|
||||
};
|
||||
createInput.value?.focus()
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
const hide = () => {
|
||||
modal.value?.hide();
|
||||
};
|
||||
modal.value?.hide()
|
||||
}
|
||||
|
||||
defineExpose({ show, hide });
|
||||
defineExpose({ show, hide })
|
||||
</script>
|
||||
|
||||
@ -42,36 +42,36 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FileIcon, FolderOpenIcon, TrashIcon, XIcon } from "@modrinth/assets";
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { FileIcon, FolderOpenIcon, TrashIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, NewModal } from '@modrinth/ui'
|
||||
|
||||
defineProps<{
|
||||
item: {
|
||||
name: string;
|
||||
type: string;
|
||||
count?: number;
|
||||
size?: number;
|
||||
} | null;
|
||||
}>();
|
||||
name: string
|
||||
type: string
|
||||
count?: number
|
||||
size?: number
|
||||
} | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "delete"): void;
|
||||
}>();
|
||||
(e: 'delete'): void
|
||||
}>()
|
||||
|
||||
const modal = ref<typeof NewModal>();
|
||||
const modal = ref<typeof NewModal>()
|
||||
|
||||
const handleSubmit = () => {
|
||||
emit("delete");
|
||||
hide();
|
||||
};
|
||||
emit('delete')
|
||||
hide()
|
||||
}
|
||||
|
||||
const show = () => {
|
||||
modal.value?.show();
|
||||
};
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
const hide = () => {
|
||||
modal.value?.hide();
|
||||
};
|
||||
modal.value?.hide()
|
||||
}
|
||||
|
||||
defineExpose({ show, hide });
|
||||
defineExpose({ show, hide })
|
||||
</script>
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
class="flex h-full w-full flex-col items-center justify-center gap-8"
|
||||
>
|
||||
<UiServersIconsPanelErrorIcon />
|
||||
<p class="m-0">{{ state.errorMessage || "Invalid or empty image file." }}</p>
|
||||
<p class="m-0">{{ state.errorMessage || 'Invalid or empty image file.' }}</p>
|
||||
</div>
|
||||
<img
|
||||
v-show="isReady"
|
||||
@ -53,20 +53,20 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ZoomInIcon, ZoomOutIcon } from "@modrinth/assets";
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
import { ZoomInIcon, ZoomOutIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled } from '@modrinth/ui'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
const ZOOM_MIN = 0.1;
|
||||
const ZOOM_MAX = 5;
|
||||
const ZOOM_IN_FACTOR = 1.2;
|
||||
const ZOOM_OUT_FACTOR = 0.8;
|
||||
const INITIAL_SCALE = 0.5;
|
||||
const MAX_IMAGE_DIMENSION = 4096;
|
||||
const ZOOM_MIN = 0.1
|
||||
const ZOOM_MAX = 5
|
||||
const ZOOM_IN_FACTOR = 1.2
|
||||
const ZOOM_OUT_FACTOR = 0.8
|
||||
const INITIAL_SCALE = 0.5
|
||||
const MAX_IMAGE_DIMENSION = 4096
|
||||
|
||||
const props = defineProps<{
|
||||
imageBlob: Blob;
|
||||
}>();
|
||||
imageBlob: Blob
|
||||
}>()
|
||||
|
||||
const state = ref({
|
||||
scale: INITIAL_SCALE,
|
||||
@ -77,102 +77,102 @@ const state = ref({
|
||||
startY: 0,
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
errorMessage: "",
|
||||
});
|
||||
errorMessage: '',
|
||||
})
|
||||
|
||||
const imageRef = ref<HTMLImageElement | null>(null);
|
||||
const container = ref<HTMLElement | null>(null);
|
||||
const imageObjectUrl = ref("");
|
||||
const rafId = ref(0);
|
||||
const imageRef = ref<HTMLImageElement | null>(null)
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
const imageObjectUrl = ref('')
|
||||
const rafId = ref(0)
|
||||
|
||||
const isReady = computed(() => !state.value.isLoading && !state.value.hasError);
|
||||
const isReady = computed(() => !state.value.isLoading && !state.value.hasError)
|
||||
|
||||
const imageStyle = computed(() => ({
|
||||
transform: `translate(-50%, -50%) scale(${state.value.scale}) translate(${state.value.translateX}px, ${state.value.translateY}px)`,
|
||||
transition: state.value.isPanning ? "none" : "transform 0.3s ease-out",
|
||||
}));
|
||||
transition: state.value.isPanning ? 'none' : 'transform 0.3s ease-out',
|
||||
}))
|
||||
|
||||
const validateImageDimensions = (img: HTMLImageElement): boolean => {
|
||||
if (img.naturalWidth > MAX_IMAGE_DIMENSION || img.naturalHeight > MAX_IMAGE_DIMENSION) {
|
||||
state.value.hasError = true;
|
||||
state.value.errorMessage = `Image too large to view (max ${MAX_IMAGE_DIMENSION}x${MAX_IMAGE_DIMENSION} pixels)`;
|
||||
return false;
|
||||
state.value.hasError = true
|
||||
state.value.errorMessage = `Image too large to view (max ${MAX_IMAGE_DIMENSION}x${MAX_IMAGE_DIMENSION} pixels)`
|
||||
return false
|
||||
}
|
||||
return true;
|
||||
};
|
||||
return true
|
||||
}
|
||||
|
||||
const updateImageUrl = (blob: Blob) => {
|
||||
if (imageObjectUrl.value) URL.revokeObjectURL(imageObjectUrl.value);
|
||||
imageObjectUrl.value = URL.createObjectURL(blob);
|
||||
};
|
||||
if (imageObjectUrl.value) URL.revokeObjectURL(imageObjectUrl.value)
|
||||
imageObjectUrl.value = URL.createObjectURL(blob)
|
||||
}
|
||||
|
||||
const handleImageLoad = () => {
|
||||
if (!imageRef.value || !validateImageDimensions(imageRef.value)) {
|
||||
state.value.isLoading = false;
|
||||
return;
|
||||
state.value.isLoading = false
|
||||
return
|
||||
}
|
||||
state.value.isLoading = false;
|
||||
reset();
|
||||
};
|
||||
state.value.isLoading = false
|
||||
reset()
|
||||
}
|
||||
|
||||
const handleImageError = () => {
|
||||
state.value.isLoading = false;
|
||||
state.value.hasError = true;
|
||||
state.value.errorMessage = "Failed to load image";
|
||||
};
|
||||
state.value.isLoading = false
|
||||
state.value.hasError = true
|
||||
state.value.errorMessage = 'Failed to load image'
|
||||
}
|
||||
|
||||
const zoom = (factor: number) => {
|
||||
const newScale = state.value.scale * factor;
|
||||
state.value.scale = Math.max(ZOOM_MIN, Math.min(newScale, ZOOM_MAX));
|
||||
};
|
||||
const newScale = state.value.scale * factor
|
||||
state.value.scale = Math.max(ZOOM_MIN, Math.min(newScale, ZOOM_MAX))
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
state.value.scale = INITIAL_SCALE;
|
||||
state.value.translateX = 0;
|
||||
state.value.translateY = 0;
|
||||
};
|
||||
state.value.scale = INITIAL_SCALE
|
||||
state.value.translateX = 0
|
||||
state.value.translateY = 0
|
||||
}
|
||||
|
||||
const startPan = (e: MouseEvent) => {
|
||||
state.value.isPanning = true;
|
||||
state.value.startX = e.clientX - state.value.translateX;
|
||||
state.value.startY = e.clientY - state.value.translateY;
|
||||
};
|
||||
state.value.isPanning = true
|
||||
state.value.startX = e.clientX - state.value.translateX
|
||||
state.value.startY = e.clientY - state.value.translateY
|
||||
}
|
||||
|
||||
const handlePan = (e: MouseEvent) => {
|
||||
if (!state.value.isPanning) return;
|
||||
cancelAnimationFrame(rafId.value);
|
||||
if (!state.value.isPanning) return
|
||||
cancelAnimationFrame(rafId.value)
|
||||
rafId.value = requestAnimationFrame(() => {
|
||||
state.value.translateX = e.clientX - state.value.startX;
|
||||
state.value.translateY = e.clientY - state.value.startY;
|
||||
});
|
||||
};
|
||||
state.value.translateX = e.clientX - state.value.startX
|
||||
state.value.translateY = e.clientY - state.value.startY
|
||||
})
|
||||
}
|
||||
|
||||
const stopPan = () => {
|
||||
state.value.isPanning = false;
|
||||
};
|
||||
state.value.isPanning = false
|
||||
}
|
||||
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
const delta = e.deltaY * -0.001;
|
||||
const factor = 1 + delta;
|
||||
zoom(factor);
|
||||
};
|
||||
const delta = e.deltaY * -0.001
|
||||
const factor = 1 + delta
|
||||
zoom(factor)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.imageBlob,
|
||||
(newBlob) => {
|
||||
if (!newBlob) return;
|
||||
state.value.isLoading = true;
|
||||
state.value.hasError = false;
|
||||
updateImageUrl(newBlob);
|
||||
if (!newBlob) return
|
||||
state.value.isLoading = true
|
||||
state.value.hasError = false
|
||||
updateImageUrl(newBlob)
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (props.imageBlob) updateImageUrl(props.imageBlob);
|
||||
});
|
||||
if (props.imageBlob) updateImageUrl(props.imageBlob)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (imageObjectUrl.value) URL.revokeObjectURL(imageObjectUrl.value);
|
||||
cancelAnimationFrame(rafId.value);
|
||||
});
|
||||
if (imageObjectUrl.value) URL.revokeObjectURL(imageObjectUrl.value)
|
||||
cancelAnimationFrame(rafId.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -51,15 +51,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ChevronDownIcon from "./icons/ChevronDownIcon.vue";
|
||||
import ChevronUpIcon from "./icons/ChevronUpIcon.vue";
|
||||
import ChevronDownIcon from './icons/ChevronDownIcon.vue'
|
||||
import ChevronUpIcon from './icons/ChevronUpIcon.vue'
|
||||
|
||||
defineProps<{
|
||||
sortField: string;
|
||||
sortDesc: boolean;
|
||||
}>();
|
||||
sortField: string
|
||||
sortDesc: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: "sort", field: string): void;
|
||||
}>();
|
||||
(e: 'sort', field: string): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@ -37,46 +37,46 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ArrowBigUpDashIcon, XIcon } from "@modrinth/assets";
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { computed,nextTick, ref } from "vue";
|
||||
import { ArrowBigUpDashIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, NewModal } from '@modrinth/ui'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
|
||||
const destinationInput = ref<HTMLInputElement | null>(null);
|
||||
const destinationInput = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const props = defineProps<{
|
||||
item: { name: string } | null;
|
||||
currentPath: string;
|
||||
}>();
|
||||
item: { name: string } | null
|
||||
currentPath: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "move", destination: string): void;
|
||||
}>();
|
||||
(e: 'move', destination: string): void
|
||||
}>()
|
||||
|
||||
const modal = ref<typeof NewModal>();
|
||||
const destination = ref("");
|
||||
const modal = ref<typeof NewModal>()
|
||||
const destination = ref('')
|
||||
const newpath = computed(() => {
|
||||
const path = destination.value.replace("//", "/");
|
||||
return path.startsWith("/") ? path : `/${path}`;
|
||||
});
|
||||
const path = destination.value.replace('//', '/')
|
||||
return path.startsWith('/') ? path : `/${path}`
|
||||
})
|
||||
|
||||
const handleSubmit = () => {
|
||||
emit("move", newpath.value);
|
||||
hide();
|
||||
};
|
||||
emit('move', newpath.value)
|
||||
hide()
|
||||
}
|
||||
|
||||
const show = () => {
|
||||
destination.value = props.currentPath;
|
||||
modal.value?.show();
|
||||
destination.value = props.currentPath
|
||||
modal.value?.show()
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
destinationInput.value?.focus();
|
||||
}, 100);
|
||||
});
|
||||
};
|
||||
destinationInput.value?.focus()
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
const hide = () => {
|
||||
modal.value?.hide();
|
||||
};
|
||||
modal.value?.hide()
|
||||
}
|
||||
|
||||
defineExpose({ show, hide });
|
||||
defineExpose({ show, hide })
|
||||
</script>
|
||||
|
||||
@ -32,63 +32,63 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { EditIcon, XIcon } from "@modrinth/assets";
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { computed, nextTick,ref } from "vue";
|
||||
import { EditIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, NewModal } from '@modrinth/ui'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
item: { name: string; type: string } | null;
|
||||
}>();
|
||||
item: { name: string; type: string } | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "rename", newName: string): void;
|
||||
}>();
|
||||
(e: 'rename', newName: string): void
|
||||
}>()
|
||||
|
||||
const modal = ref<typeof NewModal>();
|
||||
const renameInput = ref<HTMLInputElement | null>(null);
|
||||
const itemName = ref("");
|
||||
const submitted = ref(false);
|
||||
const modal = ref<typeof NewModal>()
|
||||
const renameInput = ref<HTMLInputElement | null>(null)
|
||||
const itemName = ref('')
|
||||
const submitted = ref(false)
|
||||
|
||||
const error = computed(() => {
|
||||
if (!itemName.value) {
|
||||
return "Name is required.";
|
||||
return 'Name is required.'
|
||||
}
|
||||
if (props.item?.type === "file") {
|
||||
const validPattern = /^[a-zA-Z0-9-_.\s]+$/;
|
||||
if (props.item?.type === 'file') {
|
||||
const validPattern = /^[a-zA-Z0-9-_.\s]+$/
|
||||
if (!validPattern.test(itemName.value)) {
|
||||
return "Name must contain only alphanumeric characters, dashes, underscores, dots, or spaces.";
|
||||
return 'Name must contain only alphanumeric characters, dashes, underscores, dots, or spaces.'
|
||||
}
|
||||
} else {
|
||||
const validPattern = /^[a-zA-Z0-9-_\s]+$/;
|
||||
const validPattern = /^[a-zA-Z0-9-_\s]+$/
|
||||
if (!validPattern.test(itemName.value)) {
|
||||
return "Name must contain only alphanumeric characters, dashes, underscores, or spaces.";
|
||||
return 'Name must contain only alphanumeric characters, dashes, underscores, or spaces.'
|
||||
}
|
||||
}
|
||||
return "";
|
||||
});
|
||||
return ''
|
||||
})
|
||||
|
||||
const handleSubmit = () => {
|
||||
submitted.value = true;
|
||||
submitted.value = true
|
||||
if (!error.value) {
|
||||
emit("rename", itemName.value);
|
||||
hide();
|
||||
emit('rename', itemName.value)
|
||||
hide()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const show = (item: { name: string; type: string }) => {
|
||||
itemName.value = item.name;
|
||||
submitted.value = false;
|
||||
modal.value?.show();
|
||||
itemName.value = item.name
|
||||
submitted.value = false
|
||||
modal.value?.show()
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
renameInput.value?.focus();
|
||||
}, 100);
|
||||
});
|
||||
};
|
||||
renameInput.value?.focus()
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
const hide = () => {
|
||||
modal.value?.hide();
|
||||
};
|
||||
modal.value?.hide()
|
||||
}
|
||||
|
||||
defineExpose({ show, hide });
|
||||
defineExpose({ show, hide })
|
||||
</script>
|
||||
|
||||
@ -27,30 +27,30 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CheckIcon,XIcon } from "@modrinth/assets";
|
||||
import { ConfirmModal } from "@modrinth/ui";
|
||||
import { ref } from "vue";
|
||||
import { CheckIcon, XIcon } from '@modrinth/assets'
|
||||
import { ConfirmModal } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const path = ref("");
|
||||
const files = ref<string[]>([]);
|
||||
const path = ref('')
|
||||
const files = ref<string[]>([])
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "proceed", path: string): void;
|
||||
}>();
|
||||
(e: 'proceed', path: string): void
|
||||
}>()
|
||||
|
||||
const modal = ref<typeof ConfirmModal>();
|
||||
const modal = ref<typeof ConfirmModal>()
|
||||
|
||||
const hasMany = computed(() => files.value.length > 100);
|
||||
const hasMany = computed(() => files.value.length > 100)
|
||||
|
||||
const show = (zipPath: string, conflictingFiles: string[]) => {
|
||||
path.value = zipPath;
|
||||
files.value = conflictingFiles;
|
||||
modal.value?.show();
|
||||
};
|
||||
path.value = zipPath
|
||||
files.value = conflictingFiles
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
const proceed = () => {
|
||||
emit("proceed", path.value);
|
||||
};
|
||||
emit('proceed', path.value)
|
||||
}
|
||||
|
||||
defineExpose({ show });
|
||||
defineExpose({ show })
|
||||
</script>
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
<div class="text-center">
|
||||
<UploadIcon class="mx-auto h-16 w-16" />
|
||||
<p class="mt-2 text-xl">
|
||||
Drop {{ type ? type.toLocaleLowerCase() : "file" }}s here to upload
|
||||
Drop {{ type ? type.toLocaleLowerCase() : 'file' }}s here to upload
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -24,52 +24,52 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { UploadIcon } from "@modrinth/assets";
|
||||
import { ref } from "vue";
|
||||
import { UploadIcon } from '@modrinth/assets'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "filesDropped", files: File[]): void;
|
||||
}>();
|
||||
(event: 'filesDropped', files: File[]): void
|
||||
}>()
|
||||
|
||||
defineProps<{
|
||||
overlayClass?: string;
|
||||
type?: string;
|
||||
}>();
|
||||
overlayClass?: string
|
||||
type?: string
|
||||
}>()
|
||||
|
||||
const isDragging = ref(false);
|
||||
const dragCounter = ref(0);
|
||||
const isDragging = ref(false)
|
||||
const dragCounter = ref(0)
|
||||
|
||||
const handleDragEnter = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
if (!event.dataTransfer?.types.includes("application/pyro-file-move")) {
|
||||
dragCounter.value++;
|
||||
isDragging.value = true;
|
||||
event.preventDefault()
|
||||
if (!event.dataTransfer?.types.includes('application/pyro-file-move')) {
|
||||
dragCounter.value++
|
||||
isDragging.value = true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleDragOver = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
};
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
const handleDragLeave = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
dragCounter.value--;
|
||||
event.preventDefault()
|
||||
dragCounter.value--
|
||||
if (dragCounter.value === 0) {
|
||||
isDragging.value = false;
|
||||
isDragging.value = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleDrop = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
isDragging.value = false;
|
||||
dragCounter.value = 0;
|
||||
event.preventDefault()
|
||||
isDragging.value = false
|
||||
dragCounter.value = 0
|
||||
|
||||
const isInternalMove = event.dataTransfer?.types.includes("application/pyro-file-move");
|
||||
if (isInternalMove) return;
|
||||
const isInternalMove = event.dataTransfer?.types.includes('application/pyro-file-move')
|
||||
if (isInternalMove) return
|
||||
|
||||
const files = event.dataTransfer?.files;
|
||||
const files = event.dataTransfer?.files
|
||||
if (files) {
|
||||
emit("filesDropped", Array.from(files));
|
||||
emit('filesDropped', Array.from(files))
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -12,9 +12,9 @@
|
||||
<FolderOpenIcon class="size-4" />
|
||||
<span>
|
||||
<span class="capitalize">
|
||||
{{ props.fileType ? props.fileType : "File" }} uploads
|
||||
{{ props.fileType ? props.fileType : 'File' }} uploads
|
||||
</span>
|
||||
<span>{{ activeUploads.length > 0 ? ` - ${activeUploads.length} left` : "" }}</span>
|
||||
<span>{{ activeUploads.length > 0 ? ` - ${activeUploads.length} left` : '' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -59,7 +59,7 @@
|
||||
</template>
|
||||
<template v-else-if="item.status === 'error-generic'">
|
||||
<span class="text-red"
|
||||
>Failed - {{ item.error?.message || "An unexpected error occured." }}</span
|
||||
>Failed - {{ item.error?.message || 'An unexpected error occured.' }}</span
|
||||
>
|
||||
</template>
|
||||
<template v-else-if="item.status === 'incorrect-type'">
|
||||
@ -101,118 +101,118 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CheckCircleIcon, FolderOpenIcon, XCircleIcon } from "@modrinth/assets";
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import { computed, nextTick,ref, watch } from "vue";
|
||||
import { CheckCircleIcon, FolderOpenIcon, XCircleIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled } from '@modrinth/ui'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
import type { FSModule } from "~/composables/servers/modules/fs.ts";
|
||||
import type { FSModule } from '~/composables/servers/modules/fs.ts'
|
||||
|
||||
interface UploadItem {
|
||||
file: File;
|
||||
progress: number;
|
||||
file: File
|
||||
progress: number
|
||||
status:
|
||||
| "pending"
|
||||
| "uploading"
|
||||
| "completed"
|
||||
| "error-file-exists"
|
||||
| "error-generic"
|
||||
| "cancelled"
|
||||
| "incorrect-type";
|
||||
size: string;
|
||||
uploader?: any;
|
||||
error?: Error;
|
||||
| 'pending'
|
||||
| 'uploading'
|
||||
| 'completed'
|
||||
| 'error-file-exists'
|
||||
| 'error-generic'
|
||||
| 'cancelled'
|
||||
| 'incorrect-type'
|
||||
size: string
|
||||
uploader?: any
|
||||
error?: Error
|
||||
}
|
||||
|
||||
interface Props {
|
||||
currentPath: string;
|
||||
fileType?: string;
|
||||
marginBottom?: number;
|
||||
acceptedTypes?: Array<string>;
|
||||
fs: FSModule;
|
||||
currentPath: string
|
||||
fileType?: string
|
||||
marginBottom?: number
|
||||
acceptedTypes?: Array<string>
|
||||
fs: FSModule
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
})
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "uploadComplete"): void;
|
||||
}>();
|
||||
(e: 'uploadComplete'): void
|
||||
}>()
|
||||
|
||||
const uploadStatusRef = ref<HTMLElement | null>(null);
|
||||
const statusContentRef = ref<HTMLElement | null>(null);
|
||||
const uploadQueue = ref<UploadItem[]>([]);
|
||||
const uploadStatusRef = ref<HTMLElement | null>(null)
|
||||
const statusContentRef = ref<HTMLElement | null>(null)
|
||||
const uploadQueue = ref<UploadItem[]>([])
|
||||
|
||||
const isUploading = computed(() => uploadQueue.value.length > 0);
|
||||
const isUploading = computed(() => uploadQueue.value.length > 0)
|
||||
const activeUploads = computed(() =>
|
||||
uploadQueue.value.filter((item) => item.status === "pending" || item.status === "uploading"),
|
||||
);
|
||||
uploadQueue.value.filter((item) => item.status === 'pending' || item.status === 'uploading'),
|
||||
)
|
||||
|
||||
const onUploadStatusEnter = (el: Element) => {
|
||||
const height = (el as HTMLElement).scrollHeight + (props.marginBottom || 0);
|
||||
(el as HTMLElement).style.height = "0";
|
||||
const height = (el as HTMLElement).scrollHeight + (props.marginBottom || 0)
|
||||
;(el as HTMLElement).style.height = '0'
|
||||
|
||||
void (el as HTMLElement).offsetHeight;
|
||||
(el as HTMLElement).style.height = `${height}px`;
|
||||
};
|
||||
void (el as HTMLElement).offsetHeight
|
||||
;(el as HTMLElement).style.height = `${height}px`
|
||||
}
|
||||
|
||||
const onUploadStatusLeave = (el: Element) => {
|
||||
const height = (el as HTMLElement).scrollHeight + (props.marginBottom || 0);
|
||||
(el as HTMLElement).style.height = `${height}px`;
|
||||
const height = (el as HTMLElement).scrollHeight + (props.marginBottom || 0)
|
||||
;(el as HTMLElement).style.height = `${height}px`
|
||||
|
||||
void (el as HTMLElement).offsetHeight;
|
||||
(el as HTMLElement).style.height = "0";
|
||||
};
|
||||
void (el as HTMLElement).offsetHeight
|
||||
;(el as HTMLElement).style.height = '0'
|
||||
}
|
||||
|
||||
watch(
|
||||
uploadQueue,
|
||||
() => {
|
||||
if (!uploadStatusRef.value) return;
|
||||
const el = uploadStatusRef.value;
|
||||
const itemsHeight = uploadQueue.value.length * 32;
|
||||
const headerHeight = 12;
|
||||
const gap = 8;
|
||||
const padding = 32;
|
||||
const totalHeight = padding + headerHeight + gap + itemsHeight + (props.marginBottom || 0);
|
||||
el.style.height = `${totalHeight}px`;
|
||||
if (!uploadStatusRef.value) return
|
||||
const el = uploadStatusRef.value
|
||||
const itemsHeight = uploadQueue.value.length * 32
|
||||
const headerHeight = 12
|
||||
const gap = 8
|
||||
const padding = 32
|
||||
const totalHeight = padding + headerHeight + gap + itemsHeight + (props.marginBottom || 0)
|
||||
el.style.height = `${totalHeight}px`
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
)
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes < 1024) return bytes + " B";
|
||||
if (bytes < 1024 ** 2) return (bytes / 1024).toFixed(1) + " KB";
|
||||
if (bytes < 1024 ** 3) return (bytes / 1024 ** 2).toFixed(1) + " MB";
|
||||
return (bytes / 1024 ** 3).toFixed(1) + " GB";
|
||||
};
|
||||
if (bytes < 1024) return bytes + ' B'
|
||||
if (bytes < 1024 ** 2) return (bytes / 1024).toFixed(1) + ' KB'
|
||||
if (bytes < 1024 ** 3) return (bytes / 1024 ** 2).toFixed(1) + ' MB'
|
||||
return (bytes / 1024 ** 3).toFixed(1) + ' GB'
|
||||
}
|
||||
|
||||
const cancelUpload = (item: UploadItem) => {
|
||||
if (item.uploader && item.status === "uploading") {
|
||||
item.uploader.cancel();
|
||||
item.status = "cancelled";
|
||||
if (item.uploader && item.status === 'uploading') {
|
||||
item.uploader.cancel()
|
||||
item.status = 'cancelled'
|
||||
|
||||
setTimeout(async () => {
|
||||
const index = uploadQueue.value.findIndex((qItem) => qItem.file.name === item.file.name);
|
||||
const index = uploadQueue.value.findIndex((qItem) => qItem.file.name === item.file.name)
|
||||
if (index !== -1) {
|
||||
uploadQueue.value.splice(index, 1);
|
||||
await nextTick();
|
||||
uploadQueue.value.splice(index, 1)
|
||||
await nextTick()
|
||||
}
|
||||
}, 5000);
|
||||
}, 5000)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const badFileTypeMsg = "Upload had incorrect file type";
|
||||
const badFileTypeMsg = 'Upload had incorrect file type'
|
||||
const uploadFile = async (file: File) => {
|
||||
const uploadItem: UploadItem = {
|
||||
file,
|
||||
progress: 0,
|
||||
status: "pending",
|
||||
status: 'pending',
|
||||
size: formatFileSize(file.size),
|
||||
};
|
||||
}
|
||||
|
||||
uploadQueue.value.push(uploadItem);
|
||||
uploadQueue.value.push(uploadItem)
|
||||
|
||||
try {
|
||||
if (
|
||||
@ -220,82 +220,82 @@ const uploadFile = async (file: File) => {
|
||||
!props.acceptedTypes.includes(file.type) &&
|
||||
!props.acceptedTypes.some((type) => file.name.endsWith(type))
|
||||
) {
|
||||
throw new Error(badFileTypeMsg);
|
||||
throw new Error(badFileTypeMsg)
|
||||
}
|
||||
|
||||
uploadItem.status = "uploading";
|
||||
const filePath = `${props.currentPath}/${file.name}`.replace("//", "/");
|
||||
const uploader = await props.fs.uploadFile(filePath, file);
|
||||
uploadItem.uploader = uploader;
|
||||
uploadItem.status = 'uploading'
|
||||
const filePath = `${props.currentPath}/${file.name}`.replace('//', '/')
|
||||
const uploader = await props.fs.uploadFile(filePath, file)
|
||||
uploadItem.uploader = uploader
|
||||
|
||||
if (uploader?.onProgress) {
|
||||
uploader.onProgress(({ progress }: { progress: number }) => {
|
||||
const index = uploadQueue.value.findIndex((item) => item.file.name === file.name);
|
||||
const index = uploadQueue.value.findIndex((item) => item.file.name === file.name)
|
||||
if (index !== -1) {
|
||||
uploadQueue.value[index].progress = Math.round(progress);
|
||||
uploadQueue.value[index].progress = Math.round(progress)
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
await uploader?.promise;
|
||||
const index = uploadQueue.value.findIndex((item) => item.file.name === file.name);
|
||||
if (index !== -1 && uploadQueue.value[index].status !== "cancelled") {
|
||||
uploadQueue.value[index].status = "completed";
|
||||
uploadQueue.value[index].progress = 100;
|
||||
await uploader?.promise
|
||||
const index = uploadQueue.value.findIndex((item) => item.file.name === file.name)
|
||||
if (index !== -1 && uploadQueue.value[index].status !== 'cancelled') {
|
||||
uploadQueue.value[index].status = 'completed'
|
||||
uploadQueue.value[index].progress = 100
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
await nextTick()
|
||||
|
||||
setTimeout(async () => {
|
||||
const removeIndex = uploadQueue.value.findIndex((item) => item.file.name === file.name);
|
||||
const removeIndex = uploadQueue.value.findIndex((item) => item.file.name === file.name)
|
||||
if (removeIndex !== -1) {
|
||||
uploadQueue.value.splice(removeIndex, 1);
|
||||
await nextTick();
|
||||
uploadQueue.value.splice(removeIndex, 1)
|
||||
await nextTick()
|
||||
}
|
||||
}, 5000);
|
||||
}, 5000)
|
||||
|
||||
emit("uploadComplete");
|
||||
emit('uploadComplete')
|
||||
} catch (error) {
|
||||
console.error("Error uploading file:", error);
|
||||
const index = uploadQueue.value.findIndex((item) => item.file.name === file.name);
|
||||
if (index !== -1 && uploadQueue.value[index].status !== "cancelled") {
|
||||
const target = uploadQueue.value[index];
|
||||
console.error('Error uploading file:', error)
|
||||
const index = uploadQueue.value.findIndex((item) => item.file.name === file.name)
|
||||
if (index !== -1 && uploadQueue.value[index].status !== 'cancelled') {
|
||||
const target = uploadQueue.value[index]
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message === badFileTypeMsg) {
|
||||
target.status = "incorrect-type";
|
||||
} else if (target.progress === 100 && error.message.includes("401")) {
|
||||
target.status = "error-file-exists";
|
||||
target.status = 'incorrect-type'
|
||||
} else if (target.progress === 100 && error.message.includes('401')) {
|
||||
target.status = 'error-file-exists'
|
||||
} else {
|
||||
target.status = "error-generic";
|
||||
target.error = error;
|
||||
target.status = 'error-generic'
|
||||
target.error = error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
const removeIndex = uploadQueue.value.findIndex((item) => item.file.name === file.name);
|
||||
const removeIndex = uploadQueue.value.findIndex((item) => item.file.name === file.name)
|
||||
if (removeIndex !== -1) {
|
||||
uploadQueue.value.splice(removeIndex, 1);
|
||||
await nextTick();
|
||||
uploadQueue.value.splice(removeIndex, 1)
|
||||
await nextTick()
|
||||
}
|
||||
}, 5000);
|
||||
}, 5000)
|
||||
|
||||
if (error instanceof Error && error.message !== "Upload cancelled") {
|
||||
if (error instanceof Error && error.message !== 'Upload cancelled') {
|
||||
addNotification({
|
||||
group: "files",
|
||||
title: "Upload failed",
|
||||
group: 'files',
|
||||
title: 'Upload failed',
|
||||
text: `Failed to upload ${file.name}`,
|
||||
type: "error",
|
||||
});
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
uploadFile,
|
||||
cancelUpload,
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
<form class="flex flex-col gap-4 md:w-[600px]" @submit.prevent="handleSubmit">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="font-bold text-contrast">
|
||||
{{ cf ? `How to get the modpack version's URL` : "URL of .zip file" }}
|
||||
{{ cf ? `How to get the modpack version's URL` : 'URL of .zip file' }}
|
||||
</div>
|
||||
<ol v-if="cf" class="mb-1 mt-0 flex flex-col gap-1 pl-8 leading-normal text-secondary">
|
||||
<li>
|
||||
@ -58,13 +58,13 @@
|
||||
<button v-tooltip="error" :disabled="submitted || !!error" type="submit">
|
||||
<SpinnerIcon v-if="submitted" class="animate-spin" />
|
||||
<DownloadIcon v-else class="h-5 w-5" />
|
||||
{{ submitted ? "Installing..." : "Install" }}
|
||||
{{ submitted ? 'Installing...' : 'Install' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button type="button" @click="hide">
|
||||
<XIcon class="h-5 w-5" />
|
||||
{{ submitted ? "Close" : "Cancel" }}
|
||||
{{ submitted ? 'Close' : 'Cancel' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@ -73,84 +73,84 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DownloadIcon, ExternalIcon, SpinnerIcon, XIcon } from "@modrinth/assets";
|
||||
import { BackupWarning, ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { ModrinthServersFetchError } from "@modrinth/utils";
|
||||
import { computed, nextTick,ref } from "vue";
|
||||
import { DownloadIcon, ExternalIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
|
||||
import { BackupWarning, ButtonStyled, NewModal } from '@modrinth/ui'
|
||||
import { ModrinthServersFetchError } from '@modrinth/utils'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
|
||||
import type { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
import { handleError } from "~/composables/servers/modrinth-servers.ts";
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||
import { handleError } from '~/composables/servers/modrinth-servers.ts'
|
||||
|
||||
const cf = ref(false);
|
||||
const cf = ref(false)
|
||||
|
||||
const props = defineProps<{
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
server: ModrinthServer
|
||||
}>()
|
||||
|
||||
const modal = ref<typeof NewModal>();
|
||||
const urlInput = ref<HTMLInputElement | null>(null);
|
||||
const url = ref("");
|
||||
const submitted = ref(false);
|
||||
const modal = ref<typeof NewModal>()
|
||||
const urlInput = ref<HTMLInputElement | null>(null)
|
||||
const url = ref('')
|
||||
const submitted = ref(false)
|
||||
|
||||
const trimmedUrl = computed(() => url.value.trim());
|
||||
const trimmedUrl = computed(() => url.value.trim())
|
||||
|
||||
const regex = /https:\/\/(www\.)?curseforge\.com\/minecraft\/modpacks\/[^/]+\/files\/\d+/;
|
||||
const regex = /https:\/\/(www\.)?curseforge\.com\/minecraft\/modpacks\/[^/]+\/files\/\d+/
|
||||
|
||||
const error = computed(() => {
|
||||
if (trimmedUrl.value.length === 0) {
|
||||
return "URL is required.";
|
||||
return 'URL is required.'
|
||||
}
|
||||
if (cf.value && !regex.test(trimmedUrl.value)) {
|
||||
return "URL must be a CurseForge modpack version URL.";
|
||||
} else if (!cf.value && !trimmedUrl.value.includes("/")) {
|
||||
return "URL must be valid.";
|
||||
return 'URL must be a CurseForge modpack version URL.'
|
||||
} else if (!cf.value && !trimmedUrl.value.includes('/')) {
|
||||
return 'URL must be valid.'
|
||||
}
|
||||
return "";
|
||||
});
|
||||
return ''
|
||||
})
|
||||
|
||||
const handleSubmit = async () => {
|
||||
submitted.value = true;
|
||||
submitted.value = true
|
||||
if (!error.value) {
|
||||
// hide();
|
||||
try {
|
||||
const dry = await props.server.fs.extractFile(trimmedUrl.value, true, true);
|
||||
const dry = await props.server.fs.extractFile(trimmedUrl.value, true, true)
|
||||
|
||||
if (!cf.value || dry.modpack_name) {
|
||||
await props.server.fs.extractFile(trimmedUrl.value, true, false, true);
|
||||
hide();
|
||||
await props.server.fs.extractFile(trimmedUrl.value, true, false, true)
|
||||
hide()
|
||||
} else {
|
||||
submitted.value = false;
|
||||
submitted.value = false
|
||||
handleError(
|
||||
new ModrinthServersFetchError(
|
||||
"Could not find CurseForge modpack at that URL.",
|
||||
'Could not find CurseForge modpack at that URL.',
|
||||
404,
|
||||
new Error(`No modpack found at ${url.value}`),
|
||||
),
|
||||
);
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
submitted.value = false;
|
||||
console.error("Error installing:", error);
|
||||
handleError(error);
|
||||
submitted.value = false
|
||||
console.error('Error installing:', error)
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const show = (isCf: boolean) => {
|
||||
cf.value = isCf;
|
||||
url.value = "";
|
||||
submitted.value = false;
|
||||
modal.value?.show();
|
||||
cf.value = isCf
|
||||
url.value = ''
|
||||
submitted.value = false
|
||||
modal.value?.show()
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
urlInput.value?.focus();
|
||||
}, 100);
|
||||
});
|
||||
};
|
||||
urlInput.value?.focus()
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
const hide = () => {
|
||||
modal.value?.hide();
|
||||
};
|
||||
modal.value?.hide()
|
||||
}
|
||||
|
||||
defineExpose({ show, hide });
|
||||
defineExpose({ show, hide })
|
||||
</script>
|
||||
|
||||
@ -42,20 +42,20 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import * as THREE from "three";
|
||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
|
||||
import { onMounted, onUnmounted,ref } from "vue";
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const container = ref(null);
|
||||
const showLabels = ref(false);
|
||||
const container = ref(null)
|
||||
const showLabels = ref(false)
|
||||
|
||||
const locations = ref([
|
||||
// Active locations
|
||||
{ name: "New York", lat: 40.7128, lng: -74.006, active: true, clicked: false },
|
||||
{ name: "Los Angeles", lat: 34.0522, lng: -118.2437, active: true, clicked: false },
|
||||
{ name: "Miami", lat: 25.7617, lng: -80.1918, active: true, clicked: false },
|
||||
{ name: "Spokane", lat: 47.667309, lng: -117.411922, active: true, clicked: false },
|
||||
{ name: "Dallas", lat: 32.78372, lng: -96.7947, active: true, clicked: false },
|
||||
{ name: 'New York', lat: 40.7128, lng: -74.006, active: true, clicked: false },
|
||||
{ name: 'Los Angeles', lat: 34.0522, lng: -118.2437, active: true, clicked: false },
|
||||
{ name: 'Miami', lat: 25.7617, lng: -80.1918, active: true, clicked: false },
|
||||
{ name: 'Spokane', lat: 47.667309, lng: -117.411922, active: true, clicked: false },
|
||||
{ name: 'Dallas', lat: 32.78372, lng: -96.7947, active: true, clicked: false },
|
||||
// Future Locations
|
||||
// { name: "London", lat: 51.5074, lng: -0.1278, active: false, clicked: false },
|
||||
// { name: "Frankfurt", lat: 50.1109, lng: 8.6821, active: false, clicked: false },
|
||||
@ -66,60 +66,60 @@ const locations = ref([
|
||||
// { name: "Sydney", lat: -33.8688, lng: 151.2093, active: false, clicked: false },
|
||||
// { name: "São Paulo", lat: -23.5505, lng: -46.6333, active: false, clicked: false },
|
||||
// { name: "Toronto", lat: 43.6532, lng: -79.3832, active: false, clicked: false },
|
||||
]);
|
||||
])
|
||||
|
||||
const isLocationVisible = (location) => {
|
||||
if (!location.screenPosition || !globe) return false;
|
||||
if (!location.screenPosition || !globe) return false
|
||||
|
||||
const vector = latLngToVector3(location.lat, location.lng).clone();
|
||||
vector.applyMatrix4(globe.matrixWorld);
|
||||
const vector = latLngToVector3(location.lat, location.lng).clone()
|
||||
vector.applyMatrix4(globe.matrixWorld)
|
||||
|
||||
const cameraVector = new THREE.Vector3();
|
||||
camera.getWorldPosition(cameraVector);
|
||||
const cameraVector = new THREE.Vector3()
|
||||
camera.getWorldPosition(cameraVector)
|
||||
|
||||
const viewVector = vector.clone().sub(cameraVector).normalize();
|
||||
const viewVector = vector.clone().sub(cameraVector).normalize()
|
||||
|
||||
const normal = vector.clone().normalize();
|
||||
const normal = vector.clone().normalize()
|
||||
|
||||
const dotProduct = normal.dot(viewVector);
|
||||
const dotProduct = normal.dot(viewVector)
|
||||
|
||||
return dotProduct < -0.15;
|
||||
};
|
||||
return dotProduct < -0.15
|
||||
}
|
||||
|
||||
const toggleLocationClicked = (location) => {
|
||||
console.log("clicked", location.name);
|
||||
locations.value.find((loc) => loc.name === location.name).clicked = !location.clicked;
|
||||
};
|
||||
console.log('clicked', location.name)
|
||||
locations.value.find((loc) => loc.name === location.name).clicked = !location.clicked
|
||||
}
|
||||
|
||||
let scene, camera, renderer, globe, controls;
|
||||
let animationFrame;
|
||||
let scene, camera, renderer, globe, controls
|
||||
let animationFrame
|
||||
|
||||
const init = () => {
|
||||
scene = new THREE.Scene();
|
||||
scene = new THREE.Scene()
|
||||
camera = new THREE.PerspectiveCamera(
|
||||
45,
|
||||
container.value.clientWidth / container.value.clientHeight,
|
||||
0.1,
|
||||
1000,
|
||||
);
|
||||
)
|
||||
renderer = new THREE.WebGLRenderer({
|
||||
antialias: true,
|
||||
alpha: true,
|
||||
powerPreference: "low-power",
|
||||
});
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
renderer.setSize(container.value.clientWidth, container.value.clientHeight);
|
||||
container.value.appendChild(renderer.domElement);
|
||||
powerPreference: 'low-power',
|
||||
})
|
||||
renderer.setPixelRatio(window.devicePixelRatio)
|
||||
renderer.setSize(container.value.clientWidth, container.value.clientHeight)
|
||||
container.value.appendChild(renderer.domElement)
|
||||
|
||||
const geometry = new THREE.SphereGeometry(5, 64, 64);
|
||||
const outlineTexture = new THREE.TextureLoader().load("/earth-outline.png");
|
||||
outlineTexture.minFilter = THREE.LinearFilter;
|
||||
outlineTexture.magFilter = THREE.LinearFilter;
|
||||
const geometry = new THREE.SphereGeometry(5, 64, 64)
|
||||
const outlineTexture = new THREE.TextureLoader().load('/earth-outline.png')
|
||||
outlineTexture.minFilter = THREE.LinearFilter
|
||||
outlineTexture.magFilter = THREE.LinearFilter
|
||||
|
||||
const material = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
outlineTexture: { value: outlineTexture },
|
||||
globeColor: { value: new THREE.Color("#60fbb5") },
|
||||
globeColor: { value: new THREE.Color('#60fbb5') },
|
||||
},
|
||||
vertexShader: `
|
||||
varying vec2 vUv;
|
||||
@ -141,17 +141,17 @@ const init = () => {
|
||||
`,
|
||||
transparent: true,
|
||||
side: THREE.FrontSide,
|
||||
});
|
||||
})
|
||||
|
||||
globe = new THREE.Mesh(geometry, material);
|
||||
scene.add(globe);
|
||||
globe = new THREE.Mesh(geometry, material)
|
||||
scene.add(globe)
|
||||
|
||||
const atmosphereGeometry = new THREE.SphereGeometry(5.2, 64, 64);
|
||||
const atmosphereGeometry = new THREE.SphereGeometry(5.2, 64, 64)
|
||||
const atmosphereMaterial = new THREE.ShaderMaterial({
|
||||
transparent: true,
|
||||
side: THREE.BackSide,
|
||||
uniforms: {
|
||||
color: { value: new THREE.Color("#56f690") },
|
||||
color: { value: new THREE.Color('#56f690') },
|
||||
viewVector: { value: camera.position },
|
||||
},
|
||||
vertexShader: `
|
||||
@ -171,92 +171,92 @@ const init = () => {
|
||||
gl_FragColor = vec4(color, intensity * 0.4);
|
||||
}
|
||||
`,
|
||||
});
|
||||
})
|
||||
|
||||
const atmosphere = new THREE.Mesh(atmosphereGeometry, atmosphereMaterial);
|
||||
scene.add(atmosphere);
|
||||
const atmosphere = new THREE.Mesh(atmosphereGeometry, atmosphereMaterial)
|
||||
scene.add(atmosphere)
|
||||
|
||||
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
|
||||
scene.add(ambientLight);
|
||||
const ambientLight = new THREE.AmbientLight(0x404040, 0.5)
|
||||
scene.add(ambientLight)
|
||||
|
||||
camera.position.z = 15;
|
||||
camera.position.z = 15
|
||||
|
||||
controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.05;
|
||||
controls.rotateSpeed = 0.3;
|
||||
controls.enableZoom = false;
|
||||
controls.enablePan = false;
|
||||
controls.autoRotate = true;
|
||||
controls.autoRotateSpeed = 0.05;
|
||||
controls.minPolarAngle = Math.PI * 0.3;
|
||||
controls.maxPolarAngle = Math.PI * 0.7;
|
||||
controls = new OrbitControls(camera, renderer.domElement)
|
||||
controls.enableDamping = true
|
||||
controls.dampingFactor = 0.05
|
||||
controls.rotateSpeed = 0.3
|
||||
controls.enableZoom = false
|
||||
controls.enablePan = false
|
||||
controls.autoRotate = true
|
||||
controls.autoRotateSpeed = 0.05
|
||||
controls.minPolarAngle = Math.PI * 0.3
|
||||
controls.maxPolarAngle = Math.PI * 0.7
|
||||
|
||||
globe.rotation.y = Math.PI * 1.9;
|
||||
globe.rotation.x = Math.PI * 0.15;
|
||||
};
|
||||
globe.rotation.y = Math.PI * 1.9
|
||||
globe.rotation.x = Math.PI * 0.15
|
||||
}
|
||||
|
||||
const animate = () => {
|
||||
animationFrame = requestAnimationFrame(animate);
|
||||
controls.update();
|
||||
animationFrame = requestAnimationFrame(animate)
|
||||
controls.update()
|
||||
|
||||
locations.value.forEach((location) => {
|
||||
const position = latLngToVector3(location.lat, location.lng);
|
||||
const vector = position.clone();
|
||||
vector.applyMatrix4(globe.matrixWorld);
|
||||
const position = latLngToVector3(location.lat, location.lng)
|
||||
const vector = position.clone()
|
||||
vector.applyMatrix4(globe.matrixWorld)
|
||||
|
||||
const coords = vector.project(camera);
|
||||
const coords = vector.project(camera)
|
||||
const screenPosition = {
|
||||
x: (coords.x * 0.5 + 0.5) * container.value.clientWidth,
|
||||
y: (-coords.y * 0.5 + 0.5) * container.value.clientHeight,
|
||||
};
|
||||
location.screenPosition = screenPosition;
|
||||
});
|
||||
}
|
||||
location.screenPosition = screenPosition
|
||||
})
|
||||
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
renderer.render(scene, camera)
|
||||
}
|
||||
|
||||
const latLngToVector3 = (lat, lng) => {
|
||||
const phi = (90 - lat) * (Math.PI / 180);
|
||||
const theta = (lng + 180) * (Math.PI / 180);
|
||||
const radius = 5;
|
||||
const phi = (90 - lat) * (Math.PI / 180)
|
||||
const theta = (lng + 180) * (Math.PI / 180)
|
||||
const radius = 5
|
||||
|
||||
return new THREE.Vector3(
|
||||
-radius * Math.sin(phi) * Math.cos(theta),
|
||||
radius * Math.cos(phi),
|
||||
radius * Math.sin(phi) * Math.sin(theta),
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
if (!container.value) return;
|
||||
camera.aspect = container.value.clientWidth / container.value.clientHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(container.value.clientWidth, container.value.clientHeight);
|
||||
};
|
||||
if (!container.value) return
|
||||
camera.aspect = container.value.clientWidth / container.value.clientHeight
|
||||
camera.updateProjectionMatrix()
|
||||
renderer.setSize(container.value.clientWidth, container.value.clientHeight)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
animate();
|
||||
window.addEventListener("resize", handleResize);
|
||||
init()
|
||||
animate()
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
setTimeout(() => {
|
||||
showLabels.value = true;
|
||||
}, 1000);
|
||||
});
|
||||
showLabels.value = true
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animationFrame) {
|
||||
cancelAnimationFrame(animationFrame);
|
||||
cancelAnimationFrame(animationFrame)
|
||||
}
|
||||
window.removeEventListener("resize", handleResize);
|
||||
window.removeEventListener('resize', handleResize)
|
||||
if (renderer) {
|
||||
renderer.dispose();
|
||||
renderer.dispose()
|
||||
}
|
||||
if (container.value) {
|
||||
container.value.innerHTML = "";
|
||||
container.value.innerHTML = ''
|
||||
}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -14,31 +14,31 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted,ref } from "vue";
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const msgs = [
|
||||
"Organizing files...",
|
||||
"Downloading mods...",
|
||||
"Configuring server...",
|
||||
"Setting up environment...",
|
||||
"Adding Java...",
|
||||
];
|
||||
'Organizing files...',
|
||||
'Downloading mods...',
|
||||
'Configuring server...',
|
||||
'Setting up environment...',
|
||||
'Adding Java...',
|
||||
]
|
||||
|
||||
const currentIndex = ref(0);
|
||||
const currentIndex = ref(0)
|
||||
|
||||
let intervalId: NodeJS.Timeout | null = null;
|
||||
let intervalId: NodeJS.Timeout | null = null
|
||||
|
||||
onMounted(() => {
|
||||
intervalId = setInterval(() => {
|
||||
currentIndex.value = (currentIndex.value + 1) % msgs.length;
|
||||
}, 3000);
|
||||
});
|
||||
currentIndex.value = (currentIndex.value + 1) % msgs.length
|
||||
}, 3000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
clearInterval(intervalId)
|
||||
}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -60,36 +60,36 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
data: {
|
||||
loader: string | null;
|
||||
loader_version: string | null;
|
||||
};
|
||||
ignoreCurrentInstallation?: boolean;
|
||||
isInstalling?: boolean;
|
||||
}>();
|
||||
loader: string | null
|
||||
loader_version: string | null
|
||||
}
|
||||
ignoreCurrentInstallation?: boolean
|
||||
isInstalling?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "selectLoader", loader: string): void;
|
||||
}>();
|
||||
(e: 'selectLoader', loader: string): void
|
||||
}>()
|
||||
|
||||
const vanillaLoaders = [{ name: "Vanilla" as const, displayName: "Vanilla" }];
|
||||
const vanillaLoaders = [{ name: 'Vanilla' as const, displayName: 'Vanilla' }]
|
||||
|
||||
const modLoaders = [
|
||||
{ name: "Fabric" as const, displayName: "Fabric" },
|
||||
{ name: "Quilt" as const, displayName: "Quilt" },
|
||||
{ name: "Forge" as const, displayName: "Forge" },
|
||||
{ name: "NeoForge" as const, displayName: "NeoForge" },
|
||||
];
|
||||
{ name: 'Fabric' as const, displayName: 'Fabric' },
|
||||
{ name: 'Quilt' as const, displayName: 'Quilt' },
|
||||
{ name: 'Forge' as const, displayName: 'Forge' },
|
||||
{ name: 'NeoForge' as const, displayName: 'NeoForge' },
|
||||
]
|
||||
|
||||
const pluginLoaders = [
|
||||
{ name: "Paper" as const, displayName: "Paper" },
|
||||
{ name: "Purpur" as const, displayName: "Purpur" },
|
||||
];
|
||||
{ name: 'Paper' as const, displayName: 'Paper' },
|
||||
{ name: 'Purpur' as const, displayName: 'Purpur' },
|
||||
]
|
||||
|
||||
const isCurrentLoader = (loaderName: string) => {
|
||||
return props.data.loader?.toLowerCase() === loaderName.toLowerCase();
|
||||
};
|
||||
return props.data.loader?.toLowerCase() === loaderName.toLowerCase()
|
||||
}
|
||||
|
||||
const selectLoader = (loader: string) => {
|
||||
emit("selectLoader", loader);
|
||||
};
|
||||
emit('selectLoader', loader)
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -33,39 +33,39 @@
|
||||
<ButtonStyled>
|
||||
<button :disabled="isInstalling" @click="onSelect">
|
||||
<DownloadIcon class="h-5 w-5" />
|
||||
{{ isCurrentLoader ? "Reinstall" : "Install" }}
|
||||
{{ isCurrentLoader ? 'Reinstall' : 'Install' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CheckIcon, DownloadIcon } from "@modrinth/assets";
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import { CheckIcon, DownloadIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled } from '@modrinth/ui'
|
||||
|
||||
interface LoaderInfo {
|
||||
name: "Vanilla" | "Fabric" | "Forge" | "Quilt" | "Paper" | "NeoForge" | "Purpur";
|
||||
displayName: string;
|
||||
name: 'Vanilla' | 'Fabric' | 'Forge' | 'Quilt' | 'Paper' | 'NeoForge' | 'Purpur'
|
||||
displayName: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
loader: LoaderInfo;
|
||||
currentLoader: string | null;
|
||||
loaderVersion: string | null;
|
||||
isInstalling?: boolean;
|
||||
loader: LoaderInfo
|
||||
currentLoader: string | null
|
||||
loaderVersion: string | null
|
||||
isInstalling?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "select", loader: string): void;
|
||||
}>();
|
||||
(e: 'select', loader: string): void
|
||||
}>()
|
||||
|
||||
const isCurrentLoader = computed(() => {
|
||||
return props.currentLoader?.toLowerCase() === props.loader.name.toLowerCase();
|
||||
});
|
||||
return props.currentLoader?.toLowerCase() === props.loader.name.toLowerCase()
|
||||
})
|
||||
|
||||
const onSelect = () => {
|
||||
emit("select", props.loader.name);
|
||||
};
|
||||
emit('select', props.loader.name)
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -19,54 +19,54 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Convert from "ansi-to-html";
|
||||
import DOMPurify from "dompurify";
|
||||
import { computed, onMounted, onUnmounted,ref } from "vue";
|
||||
import Convert from 'ansi-to-html'
|
||||
import DOMPurify from 'dompurify'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
log: string;
|
||||
}>();
|
||||
log: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
"show-full-log": [log: string];
|
||||
}>();
|
||||
'show-full-log': [log: string]
|
||||
}>()
|
||||
|
||||
const logContent = ref<HTMLElement | null>(null);
|
||||
const isOverflowing = ref(false);
|
||||
const logContent = ref<HTMLElement | null>(null)
|
||||
const isOverflowing = ref(false)
|
||||
|
||||
const checkOverflow = () => {
|
||||
if (logContent.value && !isOverflowing.value) {
|
||||
isOverflowing.value = logContent.value.scrollWidth > logContent.value.clientWidth;
|
||||
isOverflowing.value = logContent.value.scrollWidth > logContent.value.clientWidth
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const convert = new Convert({
|
||||
fg: "#FFF",
|
||||
bg: "#000",
|
||||
fg: '#FFF',
|
||||
bg: '#000',
|
||||
newline: false,
|
||||
escapeXML: true,
|
||||
stream: false,
|
||||
});
|
||||
})
|
||||
|
||||
const sanitizedLog = computed(() =>
|
||||
DOMPurify.sanitize(convert.toHtml(props.log), {
|
||||
ALLOWED_TAGS: ["span"],
|
||||
ALLOWED_ATTR: ["style"],
|
||||
ALLOWED_TAGS: ['span'],
|
||||
ALLOWED_ATTR: ['style'],
|
||||
USE_PROFILES: { html: true },
|
||||
}),
|
||||
);
|
||||
)
|
||||
|
||||
const preventSelection = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
logContent.value?.addEventListener("mousedown", preventSelection);
|
||||
});
|
||||
logContent.value?.addEventListener('mousedown', preventSelection)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
logContent.value?.removeEventListener("mousedown", preventSelection);
|
||||
});
|
||||
logContent.value?.removeEventListener('mousedown', preventSelection)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -62,7 +62,7 @@
|
||||
<button :disabled="!canTakeAction" @click="initiateAction('Stop')">
|
||||
<div class="flex gap-1">
|
||||
<StopCircleIcon class="h-5 w-5" />
|
||||
<span>{{ isStoppingState ? "Stopping..." : "Stop" }}</span>
|
||||
<span>{{ isStoppingState ? 'Stopping...' : 'Stop' }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
@ -115,177 +115,177 @@ import {
|
||||
StopCircleIcon,
|
||||
UpdatedIcon,
|
||||
XIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import type { PowerAction as ServerPowerAction, ServerState } from "@modrinth/utils";
|
||||
import { useStorage } from "@vueuse/core";
|
||||
import { computed,ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
} from '@modrinth/assets'
|
||||
import { ButtonStyled, NewModal } from '@modrinth/ui'
|
||||
import type { PowerAction as ServerPowerAction, ServerState } from '@modrinth/utils'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const flags = useFeatureFlags();
|
||||
const flags = useFeatureFlags()
|
||||
|
||||
interface PowerAction {
|
||||
action: ServerPowerAction;
|
||||
nextState: ServerState;
|
||||
action: ServerPowerAction
|
||||
nextState: ServerState
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
isOnline: boolean;
|
||||
isActioning: boolean;
|
||||
isInstalling: boolean;
|
||||
disabled: boolean;
|
||||
serverName?: string;
|
||||
serverData: object;
|
||||
uptimeSeconds: number;
|
||||
}>();
|
||||
isOnline: boolean
|
||||
isActioning: boolean
|
||||
isInstalling: boolean
|
||||
disabled: boolean
|
||||
serverName?: string
|
||||
serverData: object
|
||||
uptimeSeconds: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "action", action: ServerPowerAction): void;
|
||||
}>();
|
||||
(e: 'action', action: ServerPowerAction): void
|
||||
}>()
|
||||
|
||||
const router = useRouter();
|
||||
const serverId = router.currentRoute.value.params.id;
|
||||
const confirmActionModal = ref<InstanceType<typeof NewModal> | null>(null);
|
||||
const detailsModal = ref<InstanceType<typeof NewModal> | null>(null);
|
||||
const router = useRouter()
|
||||
const serverId = router.currentRoute.value.params.id
|
||||
const confirmActionModal = ref<InstanceType<typeof NewModal> | null>(null)
|
||||
const detailsModal = ref<InstanceType<typeof NewModal> | null>(null)
|
||||
|
||||
const userPreferences = useStorage(`pyro-server-${serverId}-preferences`, {
|
||||
powerDontAskAgain: false,
|
||||
});
|
||||
})
|
||||
|
||||
const serverState = ref<ServerState>(props.isOnline ? "running" : "stopped");
|
||||
const powerAction = ref<PowerAction | null>(null);
|
||||
const dontAskAgain = ref(false);
|
||||
const startingDelay = ref(false);
|
||||
const serverState = ref<ServerState>(props.isOnline ? 'running' : 'stopped')
|
||||
const powerAction = ref<PowerAction | null>(null)
|
||||
const dontAskAgain = ref(false)
|
||||
const startingDelay = ref(false)
|
||||
|
||||
const canTakeAction = computed(
|
||||
() => !props.isActioning && !startingDelay.value && !isTransitionState.value,
|
||||
);
|
||||
const isRunning = computed(() => serverState.value === "running");
|
||||
)
|
||||
const isRunning = computed(() => serverState.value === 'running')
|
||||
const isTransitionState = computed(() =>
|
||||
["starting", "stopping", "restarting"].includes(serverState.value),
|
||||
);
|
||||
const isStoppingState = computed(() => serverState.value === "stopping");
|
||||
const showStopButton = computed(() => isRunning.value || isStoppingState.value);
|
||||
['starting', 'stopping', 'restarting'].includes(serverState.value),
|
||||
)
|
||||
const isStoppingState = computed(() => serverState.value === 'stopping')
|
||||
const showStopButton = computed(() => isRunning.value || isStoppingState.value)
|
||||
|
||||
const primaryActionText = computed(() => {
|
||||
const states: Partial<Record<ServerState, string>> = {
|
||||
starting: "Starting...",
|
||||
restarting: "Restarting...",
|
||||
running: "Restart",
|
||||
stopping: "Stopping...",
|
||||
stopped: "Start",
|
||||
};
|
||||
return states[serverState.value];
|
||||
});
|
||||
starting: 'Starting...',
|
||||
restarting: 'Restarting...',
|
||||
running: 'Restart',
|
||||
stopping: 'Stopping...',
|
||||
stopped: 'Start',
|
||||
}
|
||||
return states[serverState.value]
|
||||
})
|
||||
|
||||
const confirmActionText = computed(() => {
|
||||
if (!powerAction.value) return "";
|
||||
return powerAction.value.action.charAt(0).toUpperCase() + powerAction.value.action.slice(1);
|
||||
});
|
||||
if (!powerAction.value) return ''
|
||||
return powerAction.value.action.charAt(0).toUpperCase() + powerAction.value.action.slice(1)
|
||||
})
|
||||
|
||||
const menuOptions = computed(() => [
|
||||
...(props.isInstalling
|
||||
? []
|
||||
: [
|
||||
{
|
||||
id: "kill",
|
||||
label: "Kill server",
|
||||
id: 'kill',
|
||||
label: 'Kill server',
|
||||
icon: SlashIcon,
|
||||
action: () => initiateAction("Kill"),
|
||||
action: () => initiateAction('Kill'),
|
||||
},
|
||||
]),
|
||||
{
|
||||
id: "allServers",
|
||||
label: "All servers",
|
||||
id: 'allServers',
|
||||
label: 'All servers',
|
||||
icon: ServerIcon,
|
||||
action: () => router.push("/servers/manage"),
|
||||
action: () => router.push('/servers/manage'),
|
||||
},
|
||||
{
|
||||
id: "details",
|
||||
label: "Details",
|
||||
id: 'details',
|
||||
label: 'Details',
|
||||
icon: InfoIcon,
|
||||
action: () => detailsModal.value?.show(),
|
||||
},
|
||||
{
|
||||
id: "copy-id",
|
||||
label: "Copy ID",
|
||||
id: 'copy-id',
|
||||
label: 'Copy ID',
|
||||
icon: ClipboardCopyIcon,
|
||||
action: () => copyId(),
|
||||
shown: flags.value.developerMode,
|
||||
},
|
||||
]);
|
||||
])
|
||||
|
||||
async function copyId() {
|
||||
await navigator.clipboard.writeText(serverId as string);
|
||||
await navigator.clipboard.writeText(serverId as string)
|
||||
}
|
||||
|
||||
function initiateAction(action: ServerPowerAction) {
|
||||
if (!canTakeAction.value) return;
|
||||
if (!canTakeAction.value) return
|
||||
|
||||
const stateMap: Record<ServerPowerAction, ServerState> = {
|
||||
Start: "starting",
|
||||
Stop: "stopping",
|
||||
Restart: "restarting",
|
||||
Kill: "stopping",
|
||||
};
|
||||
|
||||
if (action === "Start") {
|
||||
emit("action", action);
|
||||
serverState.value = stateMap[action];
|
||||
startingDelay.value = true;
|
||||
setTimeout(() => (startingDelay.value = false), 5000);
|
||||
return;
|
||||
Start: 'starting',
|
||||
Stop: 'stopping',
|
||||
Restart: 'restarting',
|
||||
Kill: 'stopping',
|
||||
}
|
||||
|
||||
powerAction.value = { action, nextState: stateMap[action] };
|
||||
if (action === 'Start') {
|
||||
emit('action', action)
|
||||
serverState.value = stateMap[action]
|
||||
startingDelay.value = true
|
||||
setTimeout(() => (startingDelay.value = false), 5000)
|
||||
return
|
||||
}
|
||||
|
||||
powerAction.value = { action, nextState: stateMap[action] }
|
||||
|
||||
if (userPreferences.value.powerDontAskAgain) {
|
||||
executePowerAction();
|
||||
executePowerAction()
|
||||
} else {
|
||||
confirmActionModal.value?.show();
|
||||
confirmActionModal.value?.show()
|
||||
}
|
||||
}
|
||||
|
||||
function handlePrimaryAction() {
|
||||
initiateAction(isRunning.value ? "Restart" : "Start");
|
||||
initiateAction(isRunning.value ? 'Restart' : 'Start')
|
||||
}
|
||||
|
||||
function executePowerAction() {
|
||||
if (!powerAction.value) return;
|
||||
if (!powerAction.value) return
|
||||
|
||||
const { action, nextState } = powerAction.value;
|
||||
emit("action", action);
|
||||
serverState.value = nextState;
|
||||
const { action, nextState } = powerAction.value
|
||||
emit('action', action)
|
||||
serverState.value = nextState
|
||||
|
||||
if (dontAskAgain.value) {
|
||||
userPreferences.value.powerDontAskAgain = true;
|
||||
userPreferences.value.powerDontAskAgain = true
|
||||
}
|
||||
|
||||
if (action === "Start") {
|
||||
startingDelay.value = true;
|
||||
setTimeout(() => (startingDelay.value = false), 5000);
|
||||
if (action === 'Start') {
|
||||
startingDelay.value = true
|
||||
setTimeout(() => (startingDelay.value = false), 5000)
|
||||
}
|
||||
|
||||
resetPowerAction();
|
||||
resetPowerAction()
|
||||
}
|
||||
|
||||
function resetPowerAction() {
|
||||
confirmActionModal.value?.hide();
|
||||
powerAction.value = null;
|
||||
dontAskAgain.value = false;
|
||||
confirmActionModal.value?.hide()
|
||||
powerAction.value = null
|
||||
dontAskAgain.value = false
|
||||
}
|
||||
|
||||
function closeDetailsModal() {
|
||||
detailsModal.value?.hide();
|
||||
detailsModal.value?.hide()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.isOnline,
|
||||
(online) => (serverState.value = online ? "running" : "stopped"),
|
||||
);
|
||||
(online) => (serverState.value = online ? 'running' : 'stopped'),
|
||||
)
|
||||
|
||||
watch(
|
||||
() => router.currentRoute.value.fullPath,
|
||||
() => closeDetailsModal(),
|
||||
);
|
||||
)
|
||||
</script>
|
||||
|
||||
@ -39,37 +39,37 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ServerState } from "@modrinth/utils";
|
||||
import { ref } from "vue";
|
||||
import type { ServerState } from '@modrinth/utils'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const STATUS_CLASSES = {
|
||||
running: { main: "bg-brand", bg: "bg-bg-green" },
|
||||
stopped: { main: "", bg: "" },
|
||||
crashed: { main: "bg-brand-red", bg: "bg-bg-red" },
|
||||
unknown: { main: "", bg: "" },
|
||||
} as const;
|
||||
running: { main: 'bg-brand', bg: 'bg-bg-green' },
|
||||
stopped: { main: '', bg: '' },
|
||||
crashed: { main: 'bg-brand-red', bg: 'bg-bg-red' },
|
||||
unknown: { main: '', bg: '' },
|
||||
} as const
|
||||
|
||||
const STATUS_TEXTS: Partial<Record<ServerState, string>> = {
|
||||
running: "Running",
|
||||
stopped: "",
|
||||
crashed: "Crashed",
|
||||
unknown: "Unknown",
|
||||
} as const;
|
||||
running: 'Running',
|
||||
stopped: '',
|
||||
crashed: 'Crashed',
|
||||
unknown: 'Unknown',
|
||||
} as const
|
||||
|
||||
defineProps<{
|
||||
state: ServerState;
|
||||
}>();
|
||||
state: ServerState
|
||||
}>()
|
||||
|
||||
const isExpanded = ref(false);
|
||||
const isExpanded = ref(false)
|
||||
|
||||
function getStatusClass(state: ServerState) {
|
||||
if (state in STATUS_CLASSES) {
|
||||
return STATUS_CLASSES[state as keyof typeof STATUS_CLASSES];
|
||||
return STATUS_CLASSES[state as keyof typeof STATUS_CLASSES]
|
||||
}
|
||||
return STATUS_CLASSES.unknown;
|
||||
return STATUS_CLASSES.unknown
|
||||
}
|
||||
|
||||
function getStatusText(state: ServerState) {
|
||||
return STATUS_TEXTS[state] ?? STATUS_TEXTS.unknown;
|
||||
return STATUS_TEXTS[state] ?? STATUS_TEXTS.unknown
|
||||
}
|
||||
</script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,7 @@
|
||||
<div class="flex flex-col gap-4 md:w-[600px]">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="m-0">
|
||||
Select the version of {{ props.project?.title || "the modpack" }} you want to install on
|
||||
Select the version of {{ props.project?.title || 'the modpack' }} you want to install on
|
||||
your server.
|
||||
</p>
|
||||
<p v-if="props.currentVersion" class="m-0 text-sm text-secondary">
|
||||
@ -51,7 +51,7 @@
|
||||
@click="handleReinstall"
|
||||
>
|
||||
<DownloadIcon class="size-4" />
|
||||
{{ isLoading ? "Installing..." : hardReset ? "Erase and install" : "Install" }}
|
||||
{{ isLoading ? 'Installing...' : hardReset ? 'Erase and install' : 'Install' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
@ -67,38 +67,38 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DownloadIcon, XIcon } from "@modrinth/assets";
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { ModrinthServersFetchError } from "@modrinth/utils";
|
||||
import { DownloadIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, NewModal } from '@modrinth/ui'
|
||||
import { ModrinthServersFetchError } from '@modrinth/utils'
|
||||
|
||||
import type { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||
|
||||
const props = defineProps<{
|
||||
server: ModrinthServer;
|
||||
project: any;
|
||||
versions: any[];
|
||||
currentVersion?: any;
|
||||
currentVersionId?: string;
|
||||
serverStatus?: string;
|
||||
}>();
|
||||
server: ModrinthServer
|
||||
project: any
|
||||
versions: any[]
|
||||
currentVersion?: any
|
||||
currentVersionId?: string
|
||||
serverStatus?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
reinstall: [any?];
|
||||
}>();
|
||||
reinstall: [any?]
|
||||
}>()
|
||||
|
||||
const modal = ref();
|
||||
const hardReset = ref(false);
|
||||
const isLoading = ref(false);
|
||||
const selectedVersion = ref("");
|
||||
const modal = ref()
|
||||
const hardReset = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const selectedVersion = ref('')
|
||||
|
||||
const versionOptions = computed(() => props.versions?.map((v) => v.version_number) || []);
|
||||
const versionOptions = computed(() => props.versions?.map((v) => v.version_number) || [])
|
||||
|
||||
const handleReinstall = async () => {
|
||||
if (!selectedVersion.value || !props.project?.id) return;
|
||||
if (!selectedVersion.value || !props.project?.id) return
|
||||
|
||||
isLoading.value = true;
|
||||
isLoading.value = true
|
||||
try {
|
||||
const versionId = props.versions.find((v) => v.version_number === selectedVersion.value)?.id;
|
||||
const versionId = props.versions.find((v) => v.version_number === selectedVersion.value)?.id
|
||||
|
||||
await props.server.general.reinstall(
|
||||
false,
|
||||
@ -106,56 +106,56 @@ const handleReinstall = async () => {
|
||||
versionId,
|
||||
undefined,
|
||||
hardReset.value,
|
||||
);
|
||||
)
|
||||
|
||||
emit("reinstall");
|
||||
hide();
|
||||
emit('reinstall')
|
||||
hide()
|
||||
} catch (error) {
|
||||
if (error instanceof ModrinthServersFetchError && error.statusCode === 429) {
|
||||
addNotification({
|
||||
group: "server",
|
||||
title: "Cannot reinstall server",
|
||||
text: "You are being rate limited. Please try again later.",
|
||||
type: "error",
|
||||
});
|
||||
group: 'server',
|
||||
title: 'Cannot reinstall server',
|
||||
text: 'You are being rate limited. Please try again later.',
|
||||
type: 'error',
|
||||
})
|
||||
} else {
|
||||
addNotification({
|
||||
group: "server",
|
||||
title: "Reinstall Failed",
|
||||
text: "An unexpected error occurred while reinstalling. Please try again later.",
|
||||
type: "error",
|
||||
});
|
||||
group: 'server',
|
||||
title: 'Reinstall Failed',
|
||||
text: 'An unexpected error occurred while reinstalling. Please try again later.',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
isLoading.value = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.serverStatus,
|
||||
(newStatus) => {
|
||||
if (newStatus === "installing") {
|
||||
hide();
|
||||
if (newStatus === 'installing') {
|
||||
hide()
|
||||
}
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
const onShow = () => {
|
||||
hardReset.value = false;
|
||||
hardReset.value = false
|
||||
selectedVersion.value =
|
||||
props.currentVersion?.version_number ?? props.versions?.[0]?.version_number ?? "";
|
||||
};
|
||||
props.currentVersion?.version_number ?? props.versions?.[0]?.version_number ?? ''
|
||||
}
|
||||
|
||||
const onHide = () => {
|
||||
hardReset.value = false;
|
||||
selectedVersion.value = "";
|
||||
isLoading.value = false;
|
||||
};
|
||||
hardReset.value = false
|
||||
selectedVersion.value = ''
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const show = () => modal.value?.show();
|
||||
const hide = () => modal.value?.hide();
|
||||
const show = () => modal.value?.show()
|
||||
const hide = () => modal.value?.hide()
|
||||
|
||||
defineExpose({ show, hide });
|
||||
defineExpose({ show, hide })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -110,12 +110,12 @@
|
||||
<RightArrowIcon />
|
||||
{{
|
||||
isMrpackModalSecondPhase
|
||||
? "Erase and install"
|
||||
? 'Erase and install'
|
||||
: loadingServerCheck
|
||||
? "Loading..."
|
||||
? 'Loading...'
|
||||
: isDangerous
|
||||
? "Erase and install"
|
||||
: "Install"
|
||||
? 'Erase and install'
|
||||
: 'Install'
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
@ -125,15 +125,15 @@
|
||||
@click="
|
||||
() => {
|
||||
if (isMrpackModalSecondPhase) {
|
||||
isMrpackModalSecondPhase = false;
|
||||
isMrpackModalSecondPhase = false
|
||||
} else {
|
||||
hide();
|
||||
hide()
|
||||
}
|
||||
}
|
||||
"
|
||||
>
|
||||
<XIcon />
|
||||
{{ isMrpackModalSecondPhase ? "Go back" : "Cancel" }}
|
||||
{{ isMrpackModalSecondPhase ? 'Go back' : 'Cancel' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@ -150,200 +150,200 @@ import {
|
||||
ServerIcon,
|
||||
UploadIcon,
|
||||
XIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { BackupWarning, ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { formatBytes, ModrinthServersFetchError } from "@modrinth/utils";
|
||||
import { onMounted, onUnmounted } from "vue";
|
||||
} from '@modrinth/assets'
|
||||
import { BackupWarning, ButtonStyled, NewModal } from '@modrinth/ui'
|
||||
import { formatBytes, ModrinthServersFetchError } from '@modrinth/utils'
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
|
||||
import type { ModrinthServer } from "~/composables/servers/modrinth-servers";
|
||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers'
|
||||
import type { BackupInProgressReason } from '~/pages/servers/manage/[id].vue'
|
||||
|
||||
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
|
||||
if (isLoading.value) {
|
||||
event.preventDefault();
|
||||
return "Upload in progress. Are you sure you want to leave?";
|
||||
event.preventDefault()
|
||||
return 'Upload in progress. Are you sure you want to leave?'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
});
|
||||
window.addEventListener('beforeunload', handleBeforeUnload)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
});
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload)
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
server: ModrinthServer;
|
||||
backupInProgress?: BackupInProgressReason;
|
||||
}>();
|
||||
server: ModrinthServer
|
||||
backupInProgress?: BackupInProgressReason
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
reinstall: [any?];
|
||||
}>();
|
||||
reinstall: [any?]
|
||||
}>()
|
||||
|
||||
const mrpackModal = ref();
|
||||
const isMrpackModalSecondPhase = ref(false);
|
||||
const hardReset = ref(false);
|
||||
const isLoading = ref(false);
|
||||
const loadingServerCheck = ref(false);
|
||||
const mrpackFile = ref<File | null>(null);
|
||||
const uploadProgress = ref(0);
|
||||
const uploadedBytes = ref(0);
|
||||
const totalBytes = ref(0);
|
||||
const mrpackModal = ref()
|
||||
const isMrpackModalSecondPhase = ref(false)
|
||||
const hardReset = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const loadingServerCheck = ref(false)
|
||||
const mrpackFile = ref<File | null>(null)
|
||||
const uploadProgress = ref(0)
|
||||
const uploadedBytes = ref(0)
|
||||
const totalBytes = ref(0)
|
||||
|
||||
const uploadPhrases = [
|
||||
"Removing Herobrine...",
|
||||
"Feeding parrots...",
|
||||
"Teaching villagers new trades...",
|
||||
"Convincing creepers to be friendly...",
|
||||
"Polishing diamonds...",
|
||||
"Training wolves to fetch...",
|
||||
"Building pixel art...",
|
||||
"Explaining redstone to beginners...",
|
||||
"Collecting all the cats...",
|
||||
"Negotiating with endermen...",
|
||||
"Planting suspicious stew ingredients...",
|
||||
"Calibrating TNT blast radius...",
|
||||
"Teaching chickens to fly...",
|
||||
"Sorting inventory alphabetically...",
|
||||
"Convincing iron golems to smile...",
|
||||
];
|
||||
'Removing Herobrine...',
|
||||
'Feeding parrots...',
|
||||
'Teaching villagers new trades...',
|
||||
'Convincing creepers to be friendly...',
|
||||
'Polishing diamonds...',
|
||||
'Training wolves to fetch...',
|
||||
'Building pixel art...',
|
||||
'Explaining redstone to beginners...',
|
||||
'Collecting all the cats...',
|
||||
'Negotiating with endermen...',
|
||||
'Planting suspicious stew ingredients...',
|
||||
'Calibrating TNT blast radius...',
|
||||
'Teaching chickens to fly...',
|
||||
'Sorting inventory alphabetically...',
|
||||
'Convincing iron golems to smile...',
|
||||
]
|
||||
|
||||
const currentPhrase = ref("Uploading...");
|
||||
let phraseInterval: NodeJS.Timeout | null = null;
|
||||
const usedPhrases = ref(new Set<number>());
|
||||
const currentPhrase = ref('Uploading...')
|
||||
let phraseInterval: NodeJS.Timeout | null = null
|
||||
const usedPhrases = ref(new Set<number>())
|
||||
|
||||
const getNextPhrase = () => {
|
||||
if (usedPhrases.value.size >= uploadPhrases.length) {
|
||||
const currentPhraseIndex = uploadPhrases.indexOf(currentPhrase.value);
|
||||
usedPhrases.value.clear();
|
||||
const currentPhraseIndex = uploadPhrases.indexOf(currentPhrase.value)
|
||||
usedPhrases.value.clear()
|
||||
if (currentPhraseIndex !== -1) {
|
||||
usedPhrases.value.add(currentPhraseIndex);
|
||||
usedPhrases.value.add(currentPhraseIndex)
|
||||
}
|
||||
}
|
||||
const availableIndices = uploadPhrases
|
||||
.map((_, index) => index)
|
||||
.filter((index) => !usedPhrases.value.has(index));
|
||||
.filter((index) => !usedPhrases.value.has(index))
|
||||
|
||||
const randomIndex = availableIndices[Math.floor(Math.random() * availableIndices.length)];
|
||||
usedPhrases.value.add(randomIndex);
|
||||
const randomIndex = availableIndices[Math.floor(Math.random() * availableIndices.length)]
|
||||
usedPhrases.value.add(randomIndex)
|
||||
|
||||
return uploadPhrases[randomIndex];
|
||||
};
|
||||
return uploadPhrases[randomIndex]
|
||||
}
|
||||
|
||||
const isDangerous = computed(() => hardReset.value);
|
||||
const canInstall = computed(() => !mrpackFile.value || isLoading.value || loadingServerCheck.value);
|
||||
const isDangerous = computed(() => hardReset.value)
|
||||
const canInstall = computed(() => !mrpackFile.value || isLoading.value || loadingServerCheck.value)
|
||||
|
||||
const uploadMrpack = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const target = event.target as HTMLInputElement
|
||||
if (!target.files || target.files.length === 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
mrpackFile.value = target.files[0];
|
||||
};
|
||||
mrpackFile.value = target.files[0]
|
||||
}
|
||||
|
||||
const handleReinstall = async () => {
|
||||
if (hardReset.value && !isMrpackModalSecondPhase.value) {
|
||||
isMrpackModalSecondPhase.value = true;
|
||||
return;
|
||||
isMrpackModalSecondPhase.value = true
|
||||
return
|
||||
}
|
||||
|
||||
if (!mrpackFile.value) {
|
||||
addNotification({
|
||||
group: "server",
|
||||
title: "No file selected",
|
||||
text: "Choose a .mrpack file before installing.",
|
||||
type: "error",
|
||||
});
|
||||
return;
|
||||
group: 'server',
|
||||
title: 'No file selected',
|
||||
text: 'Choose a .mrpack file before installing.',
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
uploadProgress.value = 0;
|
||||
uploadProgress.value = 0;
|
||||
uploadedBytes.value = 0;
|
||||
totalBytes.value = mrpackFile.value.size;
|
||||
isLoading.value = true
|
||||
uploadProgress.value = 0
|
||||
uploadProgress.value = 0
|
||||
uploadedBytes.value = 0
|
||||
totalBytes.value = mrpackFile.value.size
|
||||
|
||||
currentPhrase.value = getNextPhrase();
|
||||
currentPhrase.value = getNextPhrase()
|
||||
phraseInterval = setInterval(() => {
|
||||
currentPhrase.value = getNextPhrase();
|
||||
}, 4500);
|
||||
currentPhrase.value = getNextPhrase()
|
||||
}, 4500)
|
||||
|
||||
const { onProgress, promise } = props.server.general.reinstallFromMrpack(
|
||||
mrpackFile.value,
|
||||
hardReset.value,
|
||||
);
|
||||
)
|
||||
|
||||
onProgress(({ loaded, total, progress }) => {
|
||||
uploadProgress.value = progress;
|
||||
uploadedBytes.value = loaded;
|
||||
totalBytes.value = total;
|
||||
uploadProgress.value = progress
|
||||
uploadedBytes.value = loaded
|
||||
totalBytes.value = total
|
||||
|
||||
if (phraseInterval && progress >= 100) {
|
||||
clearInterval(phraseInterval);
|
||||
phraseInterval = null;
|
||||
currentPhrase.value = "Installing modpack...";
|
||||
clearInterval(phraseInterval)
|
||||
phraseInterval = null
|
||||
currentPhrase.value = 'Installing modpack...'
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
try {
|
||||
await promise;
|
||||
await promise
|
||||
|
||||
emit("reinstall", {
|
||||
loader: "mrpack",
|
||||
lVersion: "",
|
||||
mVersion: "",
|
||||
});
|
||||
emit('reinstall', {
|
||||
loader: 'mrpack',
|
||||
lVersion: '',
|
||||
mVersion: '',
|
||||
})
|
||||
|
||||
await nextTick();
|
||||
window.scrollTo(0, 0);
|
||||
hide();
|
||||
await nextTick()
|
||||
window.scrollTo(0, 0)
|
||||
hide()
|
||||
} catch (error) {
|
||||
if (error instanceof ModrinthServersFetchError && error.statusCode === 429) {
|
||||
addNotification({
|
||||
group: "server",
|
||||
title: "Cannot upload and install modpack to server",
|
||||
text: "You are being rate limited. Please try again later.",
|
||||
type: "error",
|
||||
});
|
||||
group: 'server',
|
||||
title: 'Cannot upload and install modpack to server',
|
||||
text: 'You are being rate limited. Please try again later.',
|
||||
type: 'error',
|
||||
})
|
||||
} else {
|
||||
addNotification({
|
||||
group: "server",
|
||||
title: "Modpack upload and install failed",
|
||||
text: "An unexpected error occurred while uploading/installing. Please try again later.",
|
||||
type: "error",
|
||||
});
|
||||
group: 'server',
|
||||
title: 'Modpack upload and install failed',
|
||||
text: 'An unexpected error occurred while uploading/installing. Please try again later.',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
isLoading.value = false
|
||||
if (phraseInterval) {
|
||||
clearInterval(phraseInterval);
|
||||
phraseInterval = null;
|
||||
clearInterval(phraseInterval)
|
||||
phraseInterval = null
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
const onShow = () => {
|
||||
hardReset.value = false;
|
||||
isMrpackModalSecondPhase.value = false;
|
||||
loadingServerCheck.value = false;
|
||||
isLoading.value = false;
|
||||
mrpackFile.value = null;
|
||||
uploadProgress.value = 0;
|
||||
uploadedBytes.value = 0;
|
||||
totalBytes.value = 0;
|
||||
currentPhrase.value = "Uploading...";
|
||||
usedPhrases.value.clear();
|
||||
hardReset.value = false
|
||||
isMrpackModalSecondPhase.value = false
|
||||
loadingServerCheck.value = false
|
||||
isLoading.value = false
|
||||
mrpackFile.value = null
|
||||
uploadProgress.value = 0
|
||||
uploadedBytes.value = 0
|
||||
totalBytes.value = 0
|
||||
currentPhrase.value = 'Uploading...'
|
||||
usedPhrases.value.clear()
|
||||
if (phraseInterval) {
|
||||
clearInterval(phraseInterval);
|
||||
phraseInterval = null;
|
||||
clearInterval(phraseInterval)
|
||||
phraseInterval = null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const show = () => mrpackModal.value?.show();
|
||||
const hide = () => mrpackModal.value?.hide();
|
||||
const show = () => mrpackModal.value?.show()
|
||||
const hide = () => mrpackModal.value?.hide()
|
||||
|
||||
defineExpose({ show, hide });
|
||||
defineExpose({ show, hide })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
}"
|
||||
>
|
||||
{{
|
||||
"This will reinstall your server and erase all data. Are you sure you want to continue?"
|
||||
'This will reinstall your server and erase all data. Are you sure you want to continue?'
|
||||
}}
|
||||
</p>
|
||||
<div v-if="!isSecondPhase" class="flex flex-col gap-4">
|
||||
@ -165,12 +165,12 @@
|
||||
<RightArrowIcon />
|
||||
{{
|
||||
isLoading
|
||||
? "Installing..."
|
||||
? 'Installing...'
|
||||
: isSecondPhase
|
||||
? "Erase and install"
|
||||
? 'Erase and install'
|
||||
: hardReset
|
||||
? "Continue"
|
||||
: "Install"
|
||||
? 'Continue'
|
||||
: 'Install'
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
@ -180,15 +180,15 @@
|
||||
@click="
|
||||
() => {
|
||||
if (isSecondPhase) {
|
||||
isSecondPhase = false;
|
||||
isSecondPhase = false
|
||||
} else {
|
||||
hide();
|
||||
hide()
|
||||
}
|
||||
}
|
||||
"
|
||||
>
|
||||
<XIcon />
|
||||
{{ isSecondPhase ? "Go back" : "Cancel" }}
|
||||
{{ isSecondPhase ? 'Go back' : 'Cancel' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@ -197,170 +197,169 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DropdownIcon, RightArrowIcon, ServerIcon, XIcon } from "@modrinth/assets";
|
||||
import { BackupWarning, ButtonStyled, NewModal, Toggle } from "@modrinth/ui";
|
||||
import { type Loaders, ModrinthServersFetchError } from "@modrinth/utils";
|
||||
import { $fetch } from "ofetch";
|
||||
import { DropdownIcon, RightArrowIcon, ServerIcon, XIcon } from '@modrinth/assets'
|
||||
import { BackupWarning, ButtonStyled, NewModal, Toggle } from '@modrinth/ui'
|
||||
import { type Loaders, ModrinthServersFetchError } from '@modrinth/utils'
|
||||
import { $fetch } from 'ofetch'
|
||||
|
||||
import type { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||
import type { BackupInProgressReason } from '~/pages/servers/manage/[id].vue'
|
||||
|
||||
const { formatMessage } = useVIntl();
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
interface LoaderVersion {
|
||||
id: string;
|
||||
stable: boolean;
|
||||
id: string
|
||||
stable: boolean
|
||||
loaders: {
|
||||
id: string;
|
||||
url: string;
|
||||
stable: boolean;
|
||||
}[];
|
||||
id: string
|
||||
url: string
|
||||
stable: boolean
|
||||
}[]
|
||||
}
|
||||
|
||||
type VersionMap = Record<string, LoaderVersion[]>;
|
||||
type VersionCache = Record<string, any>;
|
||||
type VersionMap = Record<string, LoaderVersion[]>
|
||||
type VersionCache = Record<string, any>
|
||||
|
||||
const props = defineProps<{
|
||||
server: ModrinthServer;
|
||||
currentLoader: Loaders | undefined;
|
||||
backupInProgress?: BackupInProgressReason;
|
||||
initialSetup?: boolean;
|
||||
}>();
|
||||
server: ModrinthServer
|
||||
currentLoader: Loaders | undefined
|
||||
backupInProgress?: BackupInProgressReason
|
||||
initialSetup?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
reinstall: [any?];
|
||||
}>();
|
||||
reinstall: [any?]
|
||||
}>()
|
||||
|
||||
const versionSelectModal = ref();
|
||||
const isSecondPhase = ref(false);
|
||||
const hardReset = ref(false);
|
||||
const isLoading = ref(false);
|
||||
const loadingServerCheck = ref(false);
|
||||
const serverCheckError = ref("");
|
||||
const showSnapshots = ref(false);
|
||||
const versionSelectModal = ref()
|
||||
const isSecondPhase = ref(false)
|
||||
const hardReset = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const loadingServerCheck = ref(false)
|
||||
const serverCheckError = ref('')
|
||||
const showSnapshots = ref(false)
|
||||
|
||||
const selectedLoader = ref<Loaders>("Vanilla");
|
||||
const selectedMCVersion = ref("");
|
||||
const selectedLoaderVersion = ref("");
|
||||
const selectedLoader = ref<Loaders>('Vanilla')
|
||||
const selectedMCVersion = ref('')
|
||||
const selectedLoaderVersion = ref('')
|
||||
|
||||
const paperVersions = ref<Record<string, number[]>>({});
|
||||
const purpurVersions = ref<Record<string, string[]>>({});
|
||||
const loaderVersions = ref<VersionMap>({});
|
||||
const cachedVersions = ref<VersionCache>({});
|
||||
const paperVersions = ref<Record<string, number[]>>({})
|
||||
const purpurVersions = ref<Record<string, string[]>>({})
|
||||
const loaderVersions = ref<VersionMap>({})
|
||||
const cachedVersions = ref<VersionCache>({})
|
||||
|
||||
const versionStrings = ["forge", "fabric", "quilt", "neo"] as const;
|
||||
const versionStrings = ['forge', 'fabric', 'quilt', 'neo'] as const
|
||||
|
||||
const isSnapshotSelected = computed(() => {
|
||||
if (selectedMCVersion.value) {
|
||||
const selected = tags.value.gameVersions.find((x) => x.version === selectedMCVersion.value);
|
||||
if (selected?.version_type !== "release") {
|
||||
return true;
|
||||
const selected = tags.value.gameVersions.find((x) => x.version === selectedMCVersion.value)
|
||||
if (selected?.version_type !== 'release') {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return false
|
||||
})
|
||||
|
||||
const getLoaderVersions = async (loader: string) => {
|
||||
return await $fetch(
|
||||
`https://launcher-meta.modrinth.com/${loader?.toLowerCase()}/v0/manifest.json`,
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const fetchLoaderVersions = async () => {
|
||||
const versions = await Promise.all(
|
||||
versionStrings.map(async (loader) => {
|
||||
const runFetch = async (iterations: number) => {
|
||||
if (iterations > 5) {
|
||||
throw new Error("Failed to fetch loader versions");
|
||||
throw new Error('Failed to fetch loader versions')
|
||||
}
|
||||
try {
|
||||
const res = await getLoaderVersions(loader);
|
||||
return { [loader]: (res as any).gameVersions };
|
||||
const res = await getLoaderVersions(loader)
|
||||
return { [loader]: (res as any).gameVersions }
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (_) {
|
||||
return await runFetch(iterations + 1);
|
||||
return await runFetch(iterations + 1)
|
||||
}
|
||||
}
|
||||
};
|
||||
try {
|
||||
return await runFetch(0);
|
||||
return await runFetch(0)
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return { [loader]: [] };
|
||||
console.error(e)
|
||||
return { [loader]: [] }
|
||||
}
|
||||
}),
|
||||
);
|
||||
)
|
||||
|
||||
loaderVersions.value = versions.reduce((acc, val) => ({ ...acc, ...val }), {});
|
||||
};
|
||||
loaderVersions.value = versions.reduce((acc, val) => ({ ...acc, ...val }), {})
|
||||
}
|
||||
|
||||
const fetchPaperVersions = async (mcVersion: string) => {
|
||||
try {
|
||||
const res = await $fetch(`https://api.papermc.io/v2/projects/paper/versions/${mcVersion}`);
|
||||
paperVersions.value[mcVersion] = (res as any).builds.sort((a: number, b: number) => b - a);
|
||||
return res;
|
||||
const res = await $fetch(`https://api.papermc.io/v2/projects/paper/versions/${mcVersion}`)
|
||||
paperVersions.value[mcVersion] = (res as any).builds.sort((a: number, b: number) => b - a)
|
||||
return res
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return null;
|
||||
console.error(e)
|
||||
return null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const fetchPurpurVersions = async (mcVersion: string) => {
|
||||
try {
|
||||
const res = await $fetch(`https://api.purpurmc.org/v2/purpur/${mcVersion}`);
|
||||
const res = await $fetch(`https://api.purpurmc.org/v2/purpur/${mcVersion}`)
|
||||
purpurVersions.value[mcVersion] = (res as any).builds.all.sort(
|
||||
(a: string, b: string) => parseInt(b) - parseInt(a),
|
||||
);
|
||||
return res;
|
||||
)
|
||||
return res
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return null;
|
||||
console.error(e)
|
||||
return null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const selectedLoaderVersions = computed<string[]>(() => {
|
||||
const loader = selectedLoader.value.toLowerCase();
|
||||
const loader = selectedLoader.value.toLowerCase()
|
||||
|
||||
if (loader === "paper") {
|
||||
return paperVersions.value[selectedMCVersion.value]?.map((x) => `${x}`) || [];
|
||||
if (loader === 'paper') {
|
||||
return paperVersions.value[selectedMCVersion.value]?.map((x) => `${x}`) || []
|
||||
}
|
||||
|
||||
if (loader === "purpur") {
|
||||
return purpurVersions.value[selectedMCVersion.value] || [];
|
||||
if (loader === 'purpur') {
|
||||
return purpurVersions.value[selectedMCVersion.value] || []
|
||||
}
|
||||
|
||||
if (loader === "vanilla") {
|
||||
return [];
|
||||
if (loader === 'vanilla') {
|
||||
return []
|
||||
}
|
||||
|
||||
let apiLoader = loader;
|
||||
if (loader === "neoforge") {
|
||||
apiLoader = "neo";
|
||||
let apiLoader = loader
|
||||
if (loader === 'neoforge') {
|
||||
apiLoader = 'neo'
|
||||
}
|
||||
|
||||
const backwardsCompatibleVersion = loaderVersions.value[apiLoader]?.find(
|
||||
|
||||
(x) => x.id === "${modrinth.gameVersion}",
|
||||
);
|
||||
(x) => x.id === '${modrinth.gameVersion}',
|
||||
)
|
||||
|
||||
if (backwardsCompatibleVersion) {
|
||||
return backwardsCompatibleVersion.loaders.map((x) => x.id);
|
||||
return backwardsCompatibleVersion.loaders.map((x) => x.id)
|
||||
}
|
||||
|
||||
return (
|
||||
loaderVersions.value[apiLoader]
|
||||
?.find((x) => x.id === selectedMCVersion.value)
|
||||
?.loaders.map((x) => x.id) || []
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
watch(selectedLoader, async () => {
|
||||
if (selectedMCVersion.value) {
|
||||
selectedLoaderVersion.value = "";
|
||||
serverCheckError.value = "";
|
||||
selectedLoaderVersion.value = ''
|
||||
serverCheckError.value = ''
|
||||
|
||||
await checkVersionAvailability(selectedMCVersion.value);
|
||||
await checkVersionAvailability(selectedMCVersion.value)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
watch(
|
||||
selectedLoaderVersions,
|
||||
@ -369,161 +368,161 @@ watch(
|
||||
newVersions.length > 0 &&
|
||||
(!selectedLoaderVersion.value || !newVersions.includes(selectedLoaderVersion.value))
|
||||
) {
|
||||
selectedLoaderVersion.value = String(newVersions[0]);
|
||||
selectedLoaderVersion.value = String(newVersions[0])
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
)
|
||||
|
||||
const getLoaderVersion = async (loader: string, version: string) => {
|
||||
return await $fetch(
|
||||
`https://launcher-meta.modrinth.com/${loader?.toLowerCase()}/v0/versions/${version}.json`,
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const checkVersionAvailability = async (version: string) => {
|
||||
if (!version || version.trim().length < 3) return;
|
||||
if (!version || version.trim().length < 3) return
|
||||
|
||||
isLoading.value = true;
|
||||
loadingServerCheck.value = true;
|
||||
isLoading.value = true
|
||||
loadingServerCheck.value = true
|
||||
|
||||
try {
|
||||
const mcRes = cachedVersions.value[version] || (await getLoaderVersion("minecraft", version));
|
||||
const mcRes = cachedVersions.value[version] || (await getLoaderVersion('minecraft', version))
|
||||
|
||||
cachedVersions.value[version] = mcRes;
|
||||
cachedVersions.value[version] = mcRes
|
||||
|
||||
if (!mcRes.downloads?.server) {
|
||||
serverCheckError.value = "We couldn't find a server.jar for this version.";
|
||||
return;
|
||||
serverCheckError.value = "We couldn't find a server.jar for this version."
|
||||
return
|
||||
}
|
||||
|
||||
const loader = selectedLoader.value.toLowerCase();
|
||||
if (loader === "paper" || loader === "purpur") {
|
||||
const fetchFn = loader === "paper" ? fetchPaperVersions : fetchPurpurVersions;
|
||||
const result = await fetchFn(version);
|
||||
const loader = selectedLoader.value.toLowerCase()
|
||||
if (loader === 'paper' || loader === 'purpur') {
|
||||
const fetchFn = loader === 'paper' ? fetchPaperVersions : fetchPurpurVersions
|
||||
const result = await fetchFn(version)
|
||||
if (!result) {
|
||||
serverCheckError.value = `This Minecraft version is not supported by ${loader}.`;
|
||||
return;
|
||||
serverCheckError.value = `This Minecraft version is not supported by ${loader}.`
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
serverCheckError.value = "";
|
||||
serverCheckError.value = ''
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
serverCheckError.value = "Failed to fetch versions.";
|
||||
console.error(error)
|
||||
serverCheckError.value = 'Failed to fetch versions.'
|
||||
} finally {
|
||||
loadingServerCheck.value = false;
|
||||
isLoading.value = false;
|
||||
loadingServerCheck.value = false
|
||||
isLoading.value = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
watch(selectedMCVersion, checkVersionAvailability);
|
||||
watch(selectedMCVersion, checkVersionAvailability)
|
||||
|
||||
onMounted(() => {
|
||||
fetchLoaderVersions();
|
||||
});
|
||||
fetchLoaderVersions()
|
||||
})
|
||||
|
||||
const tags = useTags();
|
||||
const tags = useTags()
|
||||
const mcVersions = computed(() =>
|
||||
tags.value.gameVersions
|
||||
.filter((x) =>
|
||||
showSnapshots.value
|
||||
? x.version_type === "snapshot" || x.version_type === "release"
|
||||
: x.version_type === "release",
|
||||
? x.version_type === 'snapshot' || x.version_type === 'release'
|
||||
: x.version_type === 'release',
|
||||
)
|
||||
.map((x) => x.version),
|
||||
);
|
||||
)
|
||||
|
||||
const isDangerous = computed(() => hardReset.value);
|
||||
const isDangerous = computed(() => hardReset.value)
|
||||
const canInstall = computed(() => {
|
||||
const conds =
|
||||
!selectedMCVersion.value ||
|
||||
isLoading.value ||
|
||||
loadingServerCheck.value ||
|
||||
serverCheckError.value.trim().length > 0;
|
||||
serverCheckError.value.trim().length > 0
|
||||
|
||||
if (selectedLoader.value.toLowerCase() === "vanilla") {
|
||||
return conds;
|
||||
if (selectedLoader.value.toLowerCase() === 'vanilla') {
|
||||
return conds
|
||||
}
|
||||
|
||||
return conds || !selectedLoaderVersion.value;
|
||||
});
|
||||
return conds || !selectedLoaderVersion.value
|
||||
})
|
||||
|
||||
const handleReinstall = async () => {
|
||||
if (hardReset.value && !isSecondPhase.value) {
|
||||
isSecondPhase.value = true;
|
||||
return;
|
||||
isSecondPhase.value = true
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
await props.server.general?.reinstall(
|
||||
true,
|
||||
selectedLoader.value,
|
||||
selectedMCVersion.value,
|
||||
selectedLoader.value === "Vanilla" ? "" : selectedLoaderVersion.value,
|
||||
selectedLoader.value === 'Vanilla' ? '' : selectedLoaderVersion.value,
|
||||
props.initialSetup ? true : hardReset.value,
|
||||
);
|
||||
)
|
||||
|
||||
emit("reinstall", {
|
||||
emit('reinstall', {
|
||||
loader: selectedLoader.value,
|
||||
lVersion: selectedLoaderVersion.value,
|
||||
mVersion: selectedMCVersion.value,
|
||||
});
|
||||
})
|
||||
|
||||
hide();
|
||||
hide()
|
||||
} catch (error) {
|
||||
if (error instanceof ModrinthServersFetchError && (error as any)?.statusCode === 429) {
|
||||
addNotification({
|
||||
group: "server",
|
||||
title: "Cannot reinstall server",
|
||||
text: "You are being rate limited. Please try again later.",
|
||||
type: "error",
|
||||
});
|
||||
group: 'server',
|
||||
title: 'Cannot reinstall server',
|
||||
text: 'You are being rate limited. Please try again later.',
|
||||
type: 'error',
|
||||
})
|
||||
} else {
|
||||
addNotification({
|
||||
group: "server",
|
||||
title: "Reinstall Failed",
|
||||
text: "An unexpected error occurred while reinstalling. Please try again later.",
|
||||
type: "error",
|
||||
});
|
||||
group: 'server',
|
||||
title: 'Reinstall Failed',
|
||||
text: 'An unexpected error occurred while reinstalling. Please try again later.',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
isLoading.value = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const onShow = () => {
|
||||
selectedMCVersion.value = props.server.general?.mc_version || "";
|
||||
selectedMCVersion.value = props.server.general?.mc_version || ''
|
||||
if (isSnapshotSelected.value) {
|
||||
showSnapshots.value = true;
|
||||
showSnapshots.value = true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const onHide = () => {
|
||||
hardReset.value = false;
|
||||
isSecondPhase.value = false;
|
||||
serverCheckError.value = "";
|
||||
loadingServerCheck.value = false;
|
||||
isLoading.value = false;
|
||||
selectedMCVersion.value = "";
|
||||
serverCheckError.value = "";
|
||||
paperVersions.value = {};
|
||||
purpurVersions.value = {};
|
||||
};
|
||||
hardReset.value = false
|
||||
isSecondPhase.value = false
|
||||
serverCheckError.value = ''
|
||||
loadingServerCheck.value = false
|
||||
isLoading.value = false
|
||||
selectedMCVersion.value = ''
|
||||
serverCheckError.value = ''
|
||||
paperVersions.value = {}
|
||||
purpurVersions.value = {}
|
||||
}
|
||||
|
||||
const show = (loader: Loaders) => {
|
||||
if (selectedLoader.value !== loader) {
|
||||
selectedLoaderVersion.value = "";
|
||||
selectedLoaderVersion.value = ''
|
||||
}
|
||||
selectedLoader.value = loader;
|
||||
selectedMCVersion.value = props.server.general?.mc_version || "";
|
||||
versionSelectModal.value?.show();
|
||||
};
|
||||
const hide = () => versionSelectModal.value?.hide();
|
||||
selectedLoader.value = loader
|
||||
selectedMCVersion.value = props.server.general?.mc_version || ''
|
||||
versionSelectModal.value?.show()
|
||||
}
|
||||
const hide = () => versionSelectModal.value?.hide()
|
||||
|
||||
defineExpose({ show, hide });
|
||||
defineExpose({ show, hide })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -14,12 +14,12 @@
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="standard" :color="props.restart ? 'standard' : 'brand'">
|
||||
<button :disabled="props.isUpdating" @click="props.save">
|
||||
{{ props.isUpdating ? "Saving..." : "Save" }}
|
||||
{{ props.isUpdating ? 'Saving...' : 'Save' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="props.restart" type="standard" color="brand">
|
||||
<button :disabled="props.isUpdating" @click="saveAndRestart">
|
||||
{{ props.isUpdating ? "Saving..." : "Save & restart" }}
|
||||
{{ props.isUpdating ? 'Saving...' : 'Save & restart' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@ -30,23 +30,23 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import { ButtonStyled } from '@modrinth/ui'
|
||||
|
||||
import type { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||
|
||||
const props = defineProps<{
|
||||
isUpdating: boolean;
|
||||
restart?: boolean;
|
||||
save: () => void;
|
||||
reset: () => void;
|
||||
isVisible: boolean;
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
isUpdating: boolean
|
||||
restart?: boolean
|
||||
save: () => void
|
||||
reset: () => void
|
||||
isVisible: boolean
|
||||
server: ModrinthServer
|
||||
}>()
|
||||
|
||||
const saveAndRestart = async () => {
|
||||
props.save();
|
||||
await props.server.general?.power("Restart");
|
||||
};
|
||||
props.save()
|
||||
await props.server.general?.power('Restart')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -26,14 +26,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { GameIcon } from "@modrinth/assets";
|
||||
import { GameIcon } from '@modrinth/assets'
|
||||
|
||||
defineProps<{
|
||||
game: string;
|
||||
mcVersion: string;
|
||||
isLink?: boolean;
|
||||
}>();
|
||||
game: string
|
||||
mcVersion: string
|
||||
isLink?: boolean
|
||||
}>()
|
||||
|
||||
const route = useNativeRoute();
|
||||
const serverId = route.params.id as string;
|
||||
const route = useNativeRoute()
|
||||
const serverId = route.params.id as string
|
||||
</script>
|
||||
|
||||
@ -21,6 +21,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
image: string | undefined;
|
||||
}>();
|
||||
image: string | undefined
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@ -28,13 +28,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
interface ServerInfoLabelsProps {
|
||||
serverData: Record<string, any>;
|
||||
showGameLabel: boolean;
|
||||
showLoaderLabel: boolean;
|
||||
uptimeSeconds?: number;
|
||||
column?: boolean;
|
||||
linked?: boolean;
|
||||
serverData: Record<string, any>
|
||||
showGameLabel: boolean
|
||||
showLoaderLabel: boolean
|
||||
uptimeSeconds?: number
|
||||
column?: boolean
|
||||
linked?: boolean
|
||||
}
|
||||
|
||||
defineProps<ServerInfoLabelsProps>();
|
||||
defineProps<ServerInfoLabelsProps>()
|
||||
</script>
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
style="min-height: 20px; min-width: 20px; height: 20px; width: 20px"
|
||||
alt="Server Icon"
|
||||
/>
|
||||
Using {{ projectData?.title || "Unknown" }}
|
||||
Using {{ projectData?.title || 'Unknown' }}
|
||||
</div>
|
||||
<div v-else class="min-h-[20px]"></div>
|
||||
|
||||
@ -101,37 +101,37 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChevronRightIcon, LockIcon, SparklesIcon } from "@modrinth/assets";
|
||||
import { Avatar, CopyCode } from "@modrinth/ui";
|
||||
import type { Project, Server } from "@modrinth/utils";
|
||||
import { ChevronRightIcon, LockIcon, SparklesIcon } from '@modrinth/assets'
|
||||
import { Avatar, CopyCode } from '@modrinth/ui'
|
||||
import type { Project, Server } from '@modrinth/utils'
|
||||
|
||||
import { useModrinthServers } from "~/composables/servers/modrinth-servers.ts";
|
||||
import { useModrinthServers } from '~/composables/servers/modrinth-servers.ts'
|
||||
|
||||
const props = defineProps<Partial<Server>>();
|
||||
const props = defineProps<Partial<Server>>()
|
||||
|
||||
if (props.server_id && props.status === "available") {
|
||||
if (props.server_id && props.status === 'available') {
|
||||
// Necessary only to get server icon
|
||||
await useModrinthServers(props.server_id, ["general"]);
|
||||
await useModrinthServers(props.server_id, ['general'])
|
||||
}
|
||||
|
||||
const showGameLabel = computed(() => !!props.game);
|
||||
const showLoaderLabel = computed(() => !!props.loader);
|
||||
const showGameLabel = computed(() => !!props.game)
|
||||
const showLoaderLabel = computed(() => !!props.loader)
|
||||
|
||||
let projectData: Ref<Project | null>;
|
||||
let projectData: Ref<Project | null>
|
||||
if (props.upstream) {
|
||||
const { data } = await useAsyncData<Project>(
|
||||
`server-project-${props.server_id}`,
|
||||
async (): Promise<Project> => {
|
||||
const result = await useBaseFetch(`project/${props.upstream?.project_id}`);
|
||||
return result as Project;
|
||||
const result = await useBaseFetch(`project/${props.upstream?.project_id}`)
|
||||
return result as Project
|
||||
},
|
||||
);
|
||||
projectData = data;
|
||||
)
|
||||
projectData = data
|
||||
} else {
|
||||
projectData = ref(null);
|
||||
projectData = ref(null)
|
||||
}
|
||||
|
||||
const image = useState<string | undefined>(`server-icon-${props.server_id}`, () => undefined);
|
||||
const iconUrl = computed(() => projectData.value?.icon_url || undefined);
|
||||
const isConfiguring = computed(() => props.flows?.intro);
|
||||
const image = useState<string | undefined>(`server-icon-${props.server_id}`, () => undefined)
|
||||
const iconUrl = computed(() => projectData.value?.icon_url || undefined)
|
||||
const isConfiguring = computed(() => props.flows?.intro)
|
||||
</script>
|
||||
|
||||
@ -35,12 +35,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
noSeparator?: boolean;
|
||||
loader?: "Fabric" | "Quilt" | "Forge" | "NeoForge" | "Paper" | "Spigot" | "Bukkit" | "Vanilla";
|
||||
loaderVersion?: string;
|
||||
isLink?: boolean;
|
||||
}>();
|
||||
noSeparator?: boolean
|
||||
loader?: 'Fabric' | 'Quilt' | 'Forge' | 'NeoForge' | 'Paper' | 'Spigot' | 'Bukkit' | 'Vanilla'
|
||||
loaderVersion?: string
|
||||
isLink?: boolean
|
||||
}>()
|
||||
|
||||
const route = useNativeRoute();
|
||||
const serverId = route.params.id as string;
|
||||
const route = useNativeRoute()
|
||||
const serverId = route.params.id as string
|
||||
</script>
|
||||
|
||||
@ -15,5 +15,5 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import { ButtonStyled } from '@modrinth/ui'
|
||||
</script>
|
||||
|
||||
@ -35,22 +35,22 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RightArrowIcon } from "@modrinth/assets";
|
||||
import type { RouteLocationNormalized } from "vue-router";
|
||||
import { RightArrowIcon } from '@modrinth/assets'
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
|
||||
import type { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||
import type { BackupInProgressReason } from '~/pages/servers/manage/[id].vue'
|
||||
|
||||
const emit = defineEmits(["reinstall"]);
|
||||
const emit = defineEmits(['reinstall'])
|
||||
|
||||
defineProps<{
|
||||
navLinks: { label: string; href: string; icon: Component; external?: boolean; shown?: boolean }[];
|
||||
route: RouteLocationNormalized;
|
||||
server: ModrinthServer;
|
||||
backupInProgress?: BackupInProgressReason;
|
||||
}>();
|
||||
navLinks: { label: string; href: string; icon: Component; external?: boolean; shown?: boolean }[]
|
||||
route: RouteLocationNormalized
|
||||
server: ModrinthServer
|
||||
backupInProgress?: BackupInProgressReason
|
||||
}>()
|
||||
|
||||
const onReinstall = (...args: any[]) => {
|
||||
emit("reinstall", ...args);
|
||||
};
|
||||
emit('reinstall', ...args)
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -57,7 +57,7 @@
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<h2 class="m-0 -ml-0.5 mt-1 text-3xl font-extrabold text-contrast">
|
||||
{{ loading ? "0 B" : formatBytes(stats.storage_usage_bytes) }}
|
||||
{{ loading ? '0 B' : formatBytes(stats.storage_usage_bytes) }}
|
||||
</h2>
|
||||
</div>
|
||||
<h3 class="text-base font-normal text-secondary">Storage usage</h3>
|
||||
@ -67,25 +67,25 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CpuIcon, DatabaseIcon, FolderOpenIcon, IssuesIcon } from "@modrinth/assets";
|
||||
import type { Stats } from "@modrinth/utils";
|
||||
import { useStorage } from "@vueuse/core";
|
||||
import { computed, ref, shallowRef } from "vue";
|
||||
import { CpuIcon, DatabaseIcon, FolderOpenIcon, IssuesIcon } from '@modrinth/assets'
|
||||
import type { Stats } from '@modrinth/utils'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { computed, ref, shallowRef } from 'vue'
|
||||
|
||||
const flags = useFeatureFlags();
|
||||
const route = useNativeRoute();
|
||||
const serverId = route.params.id;
|
||||
const VueApexCharts = defineAsyncComponent(() => import("vue3-apexcharts"));
|
||||
const flags = useFeatureFlags()
|
||||
const route = useNativeRoute()
|
||||
const serverId = route.params.id
|
||||
const VueApexCharts = defineAsyncComponent(() => import('vue3-apexcharts'))
|
||||
|
||||
const chartsReady = ref(new Set<number>());
|
||||
const chartsReady = ref(new Set<number>())
|
||||
|
||||
const userPreferences = useStorage(`pyro-server-${serverId}-preferences`, {
|
||||
ramAsNumber: false,
|
||||
});
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<{ data?: Stats; loading?: boolean }>(), {
|
||||
loading: false,
|
||||
});
|
||||
})
|
||||
|
||||
const stats = shallowRef(
|
||||
props.data?.current || {
|
||||
@ -94,76 +94,76 @@ const stats = shallowRef(
|
||||
ram_total_bytes: 1, // Avoid division by zero
|
||||
storage_usage_bytes: 0,
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
const onChartReady = (index: number) => {
|
||||
chartsReady.value.add(index);
|
||||
};
|
||||
chartsReady.value.add(index)
|
||||
}
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
const units = ["B", "KB", "MB", "GB"];
|
||||
let value = bytes;
|
||||
let unit = 0;
|
||||
const units = ['B', 'KB', 'MB', 'GB']
|
||||
let value = bytes
|
||||
let unit = 0
|
||||
while (value >= 1024 && unit < units.length - 1) {
|
||||
value /= 1024;
|
||||
unit++;
|
||||
value /= 1024
|
||||
unit++
|
||||
}
|
||||
return `${Math.round(value * 10) / 10} ${units[unit]}`;
|
||||
};
|
||||
return `${Math.round(value * 10) / 10} ${units[unit]}`
|
||||
}
|
||||
|
||||
const cpuData = ref<number[]>(Array(20).fill(0));
|
||||
const ramData = ref<number[]>(Array(20).fill(0));
|
||||
const cpuData = ref<number[]>(Array(20).fill(0))
|
||||
const ramData = ref<number[]>(Array(20).fill(0))
|
||||
|
||||
const updateGraphData = (arr: number[], newValue: number) => {
|
||||
arr.push(newValue);
|
||||
arr.shift();
|
||||
};
|
||||
arr.push(newValue)
|
||||
arr.shift()
|
||||
}
|
||||
|
||||
const metrics = computed(() => {
|
||||
if (props.loading) {
|
||||
return [
|
||||
{
|
||||
title: "CPU usage",
|
||||
value: "0.00%",
|
||||
max: "100%",
|
||||
title: 'CPU usage',
|
||||
value: '0.00%',
|
||||
max: '100%',
|
||||
icon: CpuIcon,
|
||||
data: cpuData.value,
|
||||
showGraph: false,
|
||||
warning: null,
|
||||
},
|
||||
{
|
||||
title: "Memory usage",
|
||||
value: "0.00%",
|
||||
max: "100%",
|
||||
title: 'Memory usage',
|
||||
value: '0.00%',
|
||||
max: '100%',
|
||||
icon: DatabaseIcon,
|
||||
data: ramData.value,
|
||||
showGraph: false,
|
||||
warning: null,
|
||||
},
|
||||
];
|
||||
]
|
||||
}
|
||||
|
||||
const ramPercent = Math.min(
|
||||
(stats.value.ram_usage_bytes / stats.value.ram_total_bytes) * 100,
|
||||
100,
|
||||
);
|
||||
const cpuPercent = Math.min(stats.value.cpu_percent, 100);
|
||||
)
|
||||
const cpuPercent = Math.min(stats.value.cpu_percent, 100)
|
||||
|
||||
updateGraphData(cpuData.value, cpuPercent);
|
||||
updateGraphData(ramData.value, ramPercent);
|
||||
updateGraphData(cpuData.value, cpuPercent)
|
||||
updateGraphData(ramData.value, ramPercent)
|
||||
|
||||
return [
|
||||
{
|
||||
title: "CPU usage",
|
||||
title: 'CPU usage',
|
||||
value: `${cpuPercent.toFixed(2)}%`,
|
||||
max: "100%",
|
||||
max: '100%',
|
||||
icon: CpuIcon,
|
||||
data: cpuData.value,
|
||||
showGraph: true,
|
||||
warning: cpuPercent >= 90 ? "CPU usage is very high" : null,
|
||||
warning: cpuPercent >= 90 ? 'CPU usage is very high' : null,
|
||||
},
|
||||
{
|
||||
title: "Memory usage",
|
||||
title: 'Memory usage',
|
||||
value:
|
||||
userPreferences.value.ramAsNumber || flags.developerMode
|
||||
? formatBytes(stats.value.ram_usage_bytes)
|
||||
@ -171,18 +171,18 @@ const metrics = computed(() => {
|
||||
max:
|
||||
userPreferences.value.ramAsNumber || flags.developerMode
|
||||
? formatBytes(stats.value.ram_total_bytes)
|
||||
: "100%",
|
||||
: '100%',
|
||||
icon: DatabaseIcon,
|
||||
data: ramData.value,
|
||||
showGraph: true,
|
||||
warning: ramPercent >= 90 ? "Memory usage is very high" : null,
|
||||
warning: ramPercent >= 90 ? 'Memory usage is very high' : null,
|
||||
},
|
||||
];
|
||||
});
|
||||
]
|
||||
})
|
||||
|
||||
const getChartOptions = (hasWarning: string | null, index: number) => ({
|
||||
chart: {
|
||||
type: "area",
|
||||
type: 'area',
|
||||
animations: { enabled: false },
|
||||
sparkline: { enabled: true },
|
||||
toolbar: { show: false },
|
||||
@ -197,9 +197,9 @@ const getChartOptions = (hasWarning: string | null, index: number) => ({
|
||||
updated: () => onChartReady(index),
|
||||
},
|
||||
},
|
||||
stroke: { curve: "smooth", width: 3 },
|
||||
stroke: { curve: 'smooth', width: 3 },
|
||||
fill: {
|
||||
type: "gradient",
|
||||
type: 'gradient',
|
||||
gradient: {
|
||||
shadeIntensity: 1,
|
||||
opacityFrom: 0.25,
|
||||
@ -212,7 +212,7 @@ const getChartOptions = (hasWarning: string | null, index: number) => ({
|
||||
xaxis: {
|
||||
labels: { show: false },
|
||||
axisBorder: { show: false },
|
||||
type: "numeric",
|
||||
type: 'numeric',
|
||||
tickAmount: 20,
|
||||
range: 20,
|
||||
},
|
||||
@ -222,20 +222,20 @@ const getChartOptions = (hasWarning: string | null, index: number) => ({
|
||||
max: 100,
|
||||
forceNiceScale: false,
|
||||
},
|
||||
colors: [hasWarning ? "var(--color-orange)" : "var(--color-brand)"],
|
||||
colors: [hasWarning ? 'var(--color-orange)' : 'var(--color-brand)'],
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.data?.current,
|
||||
(newStats) => {
|
||||
if (newStats) {
|
||||
stats.value = newStats;
|
||||
stats.value = newStats
|
||||
}
|
||||
},
|
||||
);
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -19,30 +19,30 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LinkIcon } from "@modrinth/assets";
|
||||
import { useStorage } from "@vueuse/core";
|
||||
import { LinkIcon } from '@modrinth/assets'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
|
||||
const props = defineProps<{
|
||||
subdomain: string;
|
||||
noSeparator?: boolean;
|
||||
}>();
|
||||
subdomain: string
|
||||
noSeparator?: boolean
|
||||
}>()
|
||||
|
||||
const copySubdomain = () => {
|
||||
navigator.clipboard.writeText(props.subdomain + ".modrinth.gg");
|
||||
navigator.clipboard.writeText(props.subdomain + '.modrinth.gg')
|
||||
addNotification({
|
||||
group: "servers",
|
||||
title: "Custom URL copied",
|
||||
group: 'servers',
|
||||
title: 'Custom URL copied',
|
||||
text: "Your server's URL has been copied to your clipboard.",
|
||||
type: "success",
|
||||
});
|
||||
};
|
||||
type: 'success',
|
||||
})
|
||||
}
|
||||
|
||||
const route = useNativeRoute();
|
||||
const serverId = computed(() => route.params.id as string);
|
||||
const route = useNativeRoute()
|
||||
const serverId = computed(() => route.params.id as string)
|
||||
|
||||
const userPreferences = useStorage(`pyro-server-${serverId.value}-preferences`, {
|
||||
hideSubdomainLabel: false,
|
||||
});
|
||||
})
|
||||
|
||||
const isHidden = computed(() => userPreferences.value.hideSubdomainLabel);
|
||||
const isHidden = computed(() => userPreferences.value.hideSubdomainLabel)
|
||||
</script>
|
||||
|
||||
@ -17,49 +17,49 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
uptimeSeconds: number;
|
||||
noSeparator?: boolean;
|
||||
}>();
|
||||
uptimeSeconds: number
|
||||
noSeparator?: boolean
|
||||
}>()
|
||||
|
||||
const formattedUptime = computed(() => {
|
||||
const days = Math.floor(props.uptimeSeconds / (24 * 3600));
|
||||
const hours = Math.floor((props.uptimeSeconds % (24 * 3600)) / 3600);
|
||||
const minutes = Math.floor((props.uptimeSeconds % 3600) / 60);
|
||||
const seconds = props.uptimeSeconds % 60;
|
||||
const days = Math.floor(props.uptimeSeconds / (24 * 3600))
|
||||
const hours = Math.floor((props.uptimeSeconds % (24 * 3600)) / 3600)
|
||||
const minutes = Math.floor((props.uptimeSeconds % 3600) / 60)
|
||||
const seconds = props.uptimeSeconds % 60
|
||||
|
||||
let formatted = "";
|
||||
let formatted = ''
|
||||
if (days > 0) {
|
||||
formatted += `${days}d `;
|
||||
formatted += `${days}d `
|
||||
}
|
||||
if (hours > 0 || days > 0) {
|
||||
formatted += `${hours}h `;
|
||||
formatted += `${hours}h `
|
||||
}
|
||||
formatted += `${minutes}m ${seconds}s`;
|
||||
formatted += `${minutes}m ${seconds}s`
|
||||
|
||||
return formatted.trim();
|
||||
});
|
||||
return formatted.trim()
|
||||
})
|
||||
|
||||
const verboseUptime = computed(() => {
|
||||
const days = Math.floor(props.uptimeSeconds / (24 * 3600));
|
||||
const hours = Math.floor((props.uptimeSeconds % (24 * 3600)) / 3600);
|
||||
const minutes = Math.floor((props.uptimeSeconds % 3600) / 60);
|
||||
const seconds = props.uptimeSeconds % 60;
|
||||
const days = Math.floor(props.uptimeSeconds / (24 * 3600))
|
||||
const hours = Math.floor((props.uptimeSeconds % (24 * 3600)) / 3600)
|
||||
const minutes = Math.floor((props.uptimeSeconds % 3600) / 60)
|
||||
const seconds = props.uptimeSeconds % 60
|
||||
|
||||
let verbose = "";
|
||||
let verbose = ''
|
||||
if (days > 0) {
|
||||
verbose += `${days} day${days > 1 ? "s" : ""} `;
|
||||
verbose += `${days} day${days > 1 ? 's' : ''} `
|
||||
}
|
||||
if (hours > 0) {
|
||||
verbose += `${hours} hour${hours > 1 ? "s" : ""} `;
|
||||
verbose += `${hours} hour${hours > 1 ? 's' : ''} `
|
||||
}
|
||||
if (minutes > 0) {
|
||||
verbose += `${minutes} minute${minutes > 1 ? "s" : ""} `;
|
||||
verbose += `${minutes} minute${minutes > 1 ? 's' : ''} `
|
||||
}
|
||||
verbose += `${seconds} second${seconds > 1 ? "s" : ""}`;
|
||||
verbose += `${seconds} second${seconds > 1 ? 's' : ''}`
|
||||
|
||||
return verbose.trim();
|
||||
});
|
||||
return verbose.trim()
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -42,7 +42,7 @@
|
||||
v-if="typeof option.action === 'function'"
|
||||
:ref="
|
||||
(el) => {
|
||||
if (el) menuItemsRef[index] = el as HTMLElement;
|
||||
if (el) menuItemsRef[index] = el as HTMLElement
|
||||
}
|
||||
"
|
||||
class="w-full !justify-start !whitespace-nowrap focus-visible:!outline-none"
|
||||
@ -58,7 +58,7 @@
|
||||
v-else-if="typeof option.action === 'string' && option.action.startsWith('/')"
|
||||
:ref="
|
||||
(el) => {
|
||||
if (el) menuItemsRef[index] = el as HTMLElement;
|
||||
if (el) menuItemsRef[index] = el as HTMLElement
|
||||
}
|
||||
"
|
||||
:to="option.action"
|
||||
@ -75,7 +75,7 @@
|
||||
v-else-if="typeof option.action === 'string' && !option.action.startsWith('http')"
|
||||
:ref="
|
||||
(el) => {
|
||||
if (el) menuItemsRef[index] = el as HTMLElement;
|
||||
if (el) menuItemsRef[index] = el as HTMLElement
|
||||
}
|
||||
"
|
||||
:href="option.action"
|
||||
@ -101,347 +101,347 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import { onClickOutside, useElementHover } from "@vueuse/core";
|
||||
import { computed,nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
import { ButtonStyled } from '@modrinth/ui'
|
||||
import { onClickOutside, useElementHover } from '@vueuse/core'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
interface Option {
|
||||
id: string;
|
||||
action?: (() => void) | string;
|
||||
shown?: boolean;
|
||||
color?: "standard" | "brand" | "red" | "orange" | "green" | "blue" | "purple";
|
||||
id: string
|
||||
action?: (() => void) | string
|
||||
shown?: boolean
|
||||
color?: 'standard' | 'brand' | 'red' | 'orange' | 'green' | 'blue' | 'purple'
|
||||
}
|
||||
|
||||
type Divider = {
|
||||
divider: true;
|
||||
shown?: boolean;
|
||||
};
|
||||
divider: true
|
||||
shown?: boolean
|
||||
}
|
||||
|
||||
type Item = Option | Divider;
|
||||
type Item = Option | Divider
|
||||
|
||||
function isDivider(item: Item): item is Divider {
|
||||
return (item as Divider).divider;
|
||||
return (item as Divider).divider
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
options: Item[];
|
||||
hoverable?: boolean;
|
||||
options: Item[]
|
||||
hoverable?: boolean
|
||||
}>(),
|
||||
{
|
||||
hoverable: false,
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "select", option: Option): void;
|
||||
}>();
|
||||
(e: 'select', option: Option): void
|
||||
}>()
|
||||
|
||||
const isOpen = ref(false);
|
||||
const selectedIndex = ref(-1);
|
||||
const menuRef = ref<HTMLElement | null>(null);
|
||||
const triggerRef = ref<HTMLElement | null>(null);
|
||||
const isMouseDown = ref(false);
|
||||
const typeAheadBuffer = ref("");
|
||||
const typeAheadTimeout = ref<number | null>(null);
|
||||
const menuItemsRef = ref<HTMLElement[]>([]);
|
||||
const isOpen = ref(false)
|
||||
const selectedIndex = ref(-1)
|
||||
const menuRef = ref<HTMLElement | null>(null)
|
||||
const triggerRef = ref<HTMLElement | null>(null)
|
||||
const isMouseDown = ref(false)
|
||||
const typeAheadBuffer = ref('')
|
||||
const typeAheadTimeout = ref<number | null>(null)
|
||||
const menuItemsRef = ref<HTMLElement[]>([])
|
||||
|
||||
const hoveringTrigger = useElementHover(triggerRef);
|
||||
const hoveringMenu = useElementHover(menuRef);
|
||||
const hoveringTrigger = useElementHover(triggerRef)
|
||||
const hoveringMenu = useElementHover(menuRef)
|
||||
|
||||
const hovering = computed(() => hoveringTrigger.value || hoveringMenu.value);
|
||||
const hovering = computed(() => hoveringTrigger.value || hoveringMenu.value)
|
||||
|
||||
const menuStyle = ref({
|
||||
top: "0px",
|
||||
left: "0px",
|
||||
});
|
||||
top: '0px',
|
||||
left: '0px',
|
||||
})
|
||||
|
||||
const filteredOptions = computed(() => props.options.filter((option) => option.shown !== false));
|
||||
const filteredOptions = computed(() => props.options.filter((option) => option.shown !== false))
|
||||
|
||||
const calculateMenuPosition = () => {
|
||||
if (!triggerRef.value || !menuRef.value) return { top: "0px", left: "0px" };
|
||||
if (!triggerRef.value || !menuRef.value) return { top: '0px', left: '0px' }
|
||||
|
||||
const triggerRect = triggerRef.value.getBoundingClientRect();
|
||||
const menuRect = menuRef.value.getBoundingClientRect();
|
||||
const menuWidth = menuRect.width;
|
||||
const menuHeight = menuRect.height;
|
||||
const margin = 8;
|
||||
const triggerRect = triggerRef.value.getBoundingClientRect()
|
||||
const menuRect = menuRef.value.getBoundingClientRect()
|
||||
const menuWidth = menuRect.width
|
||||
const menuHeight = menuRect.height
|
||||
const margin = 8
|
||||
|
||||
let top: number;
|
||||
let left: number;
|
||||
let top: number
|
||||
let left: number
|
||||
|
||||
// okay gang lets calculate this shit
|
||||
// from the top now yall
|
||||
// y
|
||||
if (triggerRect.bottom + menuHeight + margin <= window.innerHeight) {
|
||||
top = triggerRect.bottom + margin;
|
||||
top = triggerRect.bottom + margin
|
||||
} else if (triggerRect.top - menuHeight - margin >= 0) {
|
||||
top = triggerRect.top - menuHeight - margin;
|
||||
top = triggerRect.top - menuHeight - margin
|
||||
} else {
|
||||
top = Math.max(margin, window.innerHeight - menuHeight - margin);
|
||||
top = Math.max(margin, window.innerHeight - menuHeight - margin)
|
||||
}
|
||||
|
||||
// x
|
||||
if (triggerRect.left + menuWidth + margin <= window.innerWidth) {
|
||||
left = triggerRect.left;
|
||||
left = triggerRect.left
|
||||
} else if (triggerRect.right - menuWidth - margin >= 0) {
|
||||
left = triggerRect.right - menuWidth;
|
||||
left = triggerRect.right - menuWidth
|
||||
} else {
|
||||
left = Math.max(margin, window.innerWidth - menuWidth - margin);
|
||||
left = Math.max(margin, window.innerWidth - menuWidth - margin)
|
||||
}
|
||||
|
||||
return {
|
||||
top: `${top}px`,
|
||||
left: `${left}px`,
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const toggleMenu = (event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
event.stopPropagation()
|
||||
if (!props.hoverable) {
|
||||
if (isOpen.value) {
|
||||
closeMenu();
|
||||
closeMenu()
|
||||
} else {
|
||||
openMenu();
|
||||
openMenu()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const openMenu = () => {
|
||||
isOpen.value = true;
|
||||
disableBodyScroll();
|
||||
isOpen.value = true
|
||||
disableBodyScroll()
|
||||
nextTick(() => {
|
||||
menuStyle.value = calculateMenuPosition();
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
focusFirstMenuItem();
|
||||
});
|
||||
};
|
||||
menuStyle.value = calculateMenuPosition()
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
focusFirstMenuItem()
|
||||
})
|
||||
}
|
||||
|
||||
const closeMenu = () => {
|
||||
isOpen.value = false;
|
||||
selectedIndex.value = -1;
|
||||
enableBodyScroll();
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
};
|
||||
isOpen.value = false
|
||||
selectedIndex.value = -1
|
||||
enableBodyScroll()
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
}
|
||||
|
||||
const selectOption = (option: Option) => {
|
||||
emit("select", option);
|
||||
if (typeof option.action === "function") {
|
||||
option.action();
|
||||
emit('select', option)
|
||||
if (typeof option.action === 'function') {
|
||||
option.action()
|
||||
}
|
||||
closeMenu();
|
||||
};
|
||||
closeMenu()
|
||||
}
|
||||
|
||||
const handleMouseDown = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
isMouseDown.value = true;
|
||||
};
|
||||
event.preventDefault()
|
||||
isMouseDown.value = true
|
||||
}
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (props.hoverable) {
|
||||
openMenu();
|
||||
openMenu()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (props.hoverable) {
|
||||
setTimeout(() => {
|
||||
if (!hovering.value) {
|
||||
closeMenu();
|
||||
closeMenu()
|
||||
}
|
||||
}, 250);
|
||||
}, 250)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
if (!isOpen.value || !isMouseDown.value) return;
|
||||
if (!isOpen.value || !isMouseDown.value) return
|
||||
|
||||
const menuRect = menuRef.value?.getBoundingClientRect();
|
||||
if (!menuRect) return;
|
||||
const menuRect = menuRef.value?.getBoundingClientRect()
|
||||
if (!menuRect) return
|
||||
|
||||
const menuItems = menuRef.value?.querySelectorAll('[role="menuitem"]');
|
||||
if (!menuItems) return;
|
||||
const menuItems = menuRef.value?.querySelectorAll('[role="menuitem"]')
|
||||
if (!menuItems) return
|
||||
|
||||
for (let i = 0; i < menuItems.length; i++) {
|
||||
const itemRect = (menuItems[i] as HTMLElement).getBoundingClientRect();
|
||||
const itemRect = (menuItems[i] as HTMLElement).getBoundingClientRect()
|
||||
if (
|
||||
event.clientX >= itemRect.left &&
|
||||
event.clientX <= itemRect.right &&
|
||||
event.clientY >= itemRect.top &&
|
||||
event.clientY <= itemRect.bottom
|
||||
) {
|
||||
selectedIndex.value = i;
|
||||
break;
|
||||
selectedIndex.value = i
|
||||
break
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleItemClick = (option: Option, index: number) => {
|
||||
selectedIndex.value = index;
|
||||
selectOption(option);
|
||||
};
|
||||
selectedIndex.value = index
|
||||
selectOption(option)
|
||||
}
|
||||
|
||||
const handleMouseOver = (index: number) => {
|
||||
selectedIndex.value = index;
|
||||
menuItemsRef.value[selectedIndex.value].focus?.();
|
||||
};
|
||||
selectedIndex.value = index
|
||||
menuItemsRef.value[selectedIndex.value].focus?.()
|
||||
}
|
||||
|
||||
// Scrolling is disabled for keyboard navigation
|
||||
const disableBodyScroll = () => {
|
||||
// Make opening not shift page when there's a vertical scrollbar
|
||||
const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth;
|
||||
const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth
|
||||
if (scrollBarWidth > 0) {
|
||||
document.body.style.paddingRight = `${scrollBarWidth}px`;
|
||||
document.body.style.paddingRight = `${scrollBarWidth}px`
|
||||
} else {
|
||||
document.body.style.paddingRight = "";
|
||||
document.body.style.paddingRight = ''
|
||||
}
|
||||
|
||||
document.body.style.overflow = "hidden";
|
||||
};
|
||||
document.body.style.overflow = 'hidden'
|
||||
}
|
||||
|
||||
const enableBodyScroll = () => {
|
||||
document.body.style.paddingRight = "";
|
||||
document.body.style.overflow = "";
|
||||
};
|
||||
document.body.style.paddingRight = ''
|
||||
document.body.style.overflow = ''
|
||||
}
|
||||
|
||||
const focusFirstMenuItem = () => {
|
||||
if (menuItemsRef.value.length > 0) {
|
||||
menuItemsRef.value[0].focus?.();
|
||||
menuItemsRef.value[0].focus?.()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (!isOpen.value) {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
openMenu();
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
openMenu()
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
switch (event.key) {
|
||||
case "ArrowDown":
|
||||
event.preventDefault();
|
||||
selectedIndex.value = (selectedIndex.value + 1) % filteredOptions.value.length;
|
||||
menuItemsRef.value[selectedIndex.value].focus?.();
|
||||
break;
|
||||
case "ArrowUp":
|
||||
event.preventDefault();
|
||||
case 'ArrowDown':
|
||||
event.preventDefault()
|
||||
selectedIndex.value = (selectedIndex.value + 1) % filteredOptions.value.length
|
||||
menuItemsRef.value[selectedIndex.value].focus?.()
|
||||
break
|
||||
case 'ArrowUp':
|
||||
event.preventDefault()
|
||||
selectedIndex.value =
|
||||
(selectedIndex.value - 1 + filteredOptions.value.length) % filteredOptions.value.length;
|
||||
menuItemsRef.value[selectedIndex.value].focus?.();
|
||||
break;
|
||||
case "Home":
|
||||
event.preventDefault();
|
||||
(selectedIndex.value - 1 + filteredOptions.value.length) % filteredOptions.value.length
|
||||
menuItemsRef.value[selectedIndex.value].focus?.()
|
||||
break
|
||||
case 'Home':
|
||||
event.preventDefault()
|
||||
if (menuItemsRef.value.length > 0) {
|
||||
selectedIndex.value = 0;
|
||||
menuItemsRef.value[selectedIndex.value].focus?.();
|
||||
selectedIndex.value = 0
|
||||
menuItemsRef.value[selectedIndex.value].focus?.()
|
||||
}
|
||||
break;
|
||||
case "End":
|
||||
event.preventDefault();
|
||||
break
|
||||
case 'End':
|
||||
event.preventDefault()
|
||||
if (menuItemsRef.value.length > 0) {
|
||||
selectedIndex.value = filteredOptions.value.length - 1;
|
||||
menuItemsRef.value[selectedIndex.value].focus?.();
|
||||
selectedIndex.value = filteredOptions.value.length - 1
|
||||
menuItemsRef.value[selectedIndex.value].focus?.()
|
||||
}
|
||||
break;
|
||||
case "Enter":
|
||||
case " ":
|
||||
event.preventDefault();
|
||||
break
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
event.preventDefault()
|
||||
if (selectedIndex.value >= 0) {
|
||||
const option = filteredOptions.value[selectedIndex.value];
|
||||
if (isDivider(option)) break;
|
||||
selectOption(option);
|
||||
const option = filteredOptions.value[selectedIndex.value]
|
||||
if (isDivider(option)) break
|
||||
selectOption(option)
|
||||
}
|
||||
break;
|
||||
case "Escape":
|
||||
event.preventDefault();
|
||||
closeMenu();
|
||||
triggerRef.value?.focus?.();
|
||||
break;
|
||||
case "Tab":
|
||||
event.preventDefault();
|
||||
break
|
||||
case 'Escape':
|
||||
event.preventDefault()
|
||||
closeMenu()
|
||||
triggerRef.value?.focus?.()
|
||||
break
|
||||
case 'Tab':
|
||||
event.preventDefault()
|
||||
if (menuItemsRef.value.length > 0) {
|
||||
if (event.shiftKey) {
|
||||
selectedIndex.value =
|
||||
(selectedIndex.value - 1 + filteredOptions.value.length) % filteredOptions.value.length;
|
||||
(selectedIndex.value - 1 + filteredOptions.value.length) % filteredOptions.value.length
|
||||
} else {
|
||||
selectedIndex.value = (selectedIndex.value + 1) % filteredOptions.value.length;
|
||||
selectedIndex.value = (selectedIndex.value + 1) % filteredOptions.value.length
|
||||
}
|
||||
menuItemsRef.value[selectedIndex.value].focus?.();
|
||||
menuItemsRef.value[selectedIndex.value].focus?.()
|
||||
}
|
||||
break;
|
||||
break
|
||||
default:
|
||||
if (event.key.length === 1) {
|
||||
typeAheadBuffer.value += event.key.toLowerCase();
|
||||
typeAheadBuffer.value += event.key.toLowerCase()
|
||||
const matchIndex = filteredOptions.value.findIndex(
|
||||
(option) =>
|
||||
!isDivider(option) && option.id.toLowerCase().startsWith(typeAheadBuffer.value),
|
||||
);
|
||||
)
|
||||
if (matchIndex !== -1) {
|
||||
selectedIndex.value = matchIndex;
|
||||
menuItemsRef.value[selectedIndex.value].focus?.();
|
||||
selectedIndex.value = matchIndex
|
||||
menuItemsRef.value[selectedIndex.value].focus?.()
|
||||
}
|
||||
if (typeAheadTimeout.value) {
|
||||
clearTimeout(typeAheadTimeout.value);
|
||||
clearTimeout(typeAheadTimeout.value)
|
||||
}
|
||||
typeAheadTimeout.value = setTimeout(() => {
|
||||
typeAheadBuffer.value = "";
|
||||
}, 1000) as unknown as number;
|
||||
typeAheadBuffer.value = ''
|
||||
}, 1000) as unknown as number
|
||||
}
|
||||
break;
|
||||
break
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleResizeOrScroll = () => {
|
||||
if (isOpen.value) {
|
||||
menuStyle.value = calculateMenuPosition();
|
||||
menuStyle.value = calculateMenuPosition()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const throttle = (func: (...args: any[]) => void, limit: number): ((...args: any[]) => void) => {
|
||||
let inThrottle: boolean;
|
||||
let inThrottle: boolean
|
||||
return function (...args: any[]) {
|
||||
if (!inThrottle) {
|
||||
func(...args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => (inThrottle = false), limit);
|
||||
func(...args)
|
||||
inThrottle = true
|
||||
setTimeout(() => (inThrottle = false), limit)
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const throttledHandleResizeOrScroll = throttle(handleResizeOrScroll, 100);
|
||||
const throttledHandleResizeOrScroll = throttle(handleResizeOrScroll, 100)
|
||||
|
||||
onMounted(() => {
|
||||
triggerRef.value?.addEventListener("keydown", handleKeydown);
|
||||
window.addEventListener("resize", throttledHandleResizeOrScroll);
|
||||
window.addEventListener("scroll", throttledHandleResizeOrScroll);
|
||||
});
|
||||
triggerRef.value?.addEventListener('keydown', handleKeydown)
|
||||
window.addEventListener('resize', throttledHandleResizeOrScroll)
|
||||
window.addEventListener('scroll', throttledHandleResizeOrScroll)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
triggerRef.value?.removeEventListener("keydown", handleKeydown);
|
||||
window.removeEventListener("resize", throttledHandleResizeOrScroll);
|
||||
window.removeEventListener("scroll", throttledHandleResizeOrScroll);
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
triggerRef.value?.removeEventListener('keydown', handleKeydown)
|
||||
window.removeEventListener('resize', throttledHandleResizeOrScroll)
|
||||
window.removeEventListener('scroll', throttledHandleResizeOrScroll)
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
if (typeAheadTimeout.value) {
|
||||
clearTimeout(typeAheadTimeout.value);
|
||||
clearTimeout(typeAheadTimeout.value)
|
||||
}
|
||||
enableBodyScroll();
|
||||
});
|
||||
enableBodyScroll()
|
||||
})
|
||||
|
||||
watch(isOpen, (newValue) => {
|
||||
if (newValue) {
|
||||
nextTick(() => {
|
||||
menuRef.value?.addEventListener("keydown", handleKeydown);
|
||||
});
|
||||
menuRef.value?.addEventListener('keydown', handleKeydown)
|
||||
})
|
||||
} else {
|
||||
menuRef.value?.removeEventListener("keydown", handleKeydown);
|
||||
menuRef.value?.removeEventListener('keydown', handleKeydown)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
onClickOutside(menuRef, (event) => {
|
||||
if (!triggerRef.value?.contains(event.target as Node)) {
|
||||
closeMenu();
|
||||
closeMenu()
|
||||
}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -223,10 +223,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LoaderIcon } from "@modrinth/assets";
|
||||
import type { Loaders } from "@modrinth/utils";
|
||||
import { LoaderIcon } from '@modrinth/assets'
|
||||
import type { Loaders } from '@modrinth/utils'
|
||||
|
||||
defineProps<{
|
||||
loader: Loaders;
|
||||
}>();
|
||||
loader: Loaders
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@ -1,95 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled, ServersSpecs } from "@modrinth/ui";
|
||||
import { formatPrice } from "@modrinth/utils";
|
||||
import type { MessageDescriptor } from "@vintl/vintl";
|
||||
import { ButtonStyled, ServersSpecs } from '@modrinth/ui'
|
||||
import { formatPrice } from '@modrinth/utils'
|
||||
import type { MessageDescriptor } from '@vintl/vintl'
|
||||
|
||||
const { formatMessage, locale } = useVIntl();
|
||||
const { formatMessage, locale } = useVIntl()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "select" | "scroll-to-faq"): void;
|
||||
}>();
|
||||
(e: 'select' | 'scroll-to-faq'): void
|
||||
}>()
|
||||
|
||||
type Plan = "small" | "medium" | "large";
|
||||
type Plan = 'small' | 'medium' | 'large'
|
||||
|
||||
const plans: Record<
|
||||
Plan,
|
||||
{
|
||||
buttonColor: "blue" | "green" | "purple";
|
||||
accentText: string;
|
||||
accentBg: string;
|
||||
name: MessageDescriptor;
|
||||
description: MessageDescriptor;
|
||||
mostPopular: boolean;
|
||||
buttonColor: 'blue' | 'green' | 'purple'
|
||||
accentText: string
|
||||
accentBg: string
|
||||
name: MessageDescriptor
|
||||
description: MessageDescriptor
|
||||
mostPopular: boolean
|
||||
}
|
||||
> = {
|
||||
small: {
|
||||
buttonColor: "blue",
|
||||
accentText: "text-blue",
|
||||
accentBg: "bg-bg-blue",
|
||||
buttonColor: 'blue',
|
||||
accentText: 'text-blue',
|
||||
accentBg: 'bg-bg-blue',
|
||||
name: defineMessage({
|
||||
id: "servers.plan.small.name",
|
||||
defaultMessage: "Small",
|
||||
id: 'servers.plan.small.name',
|
||||
defaultMessage: 'Small',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: "servers.plan.small.description",
|
||||
defaultMessage: "Perfect for 1–5 friends with a few light mods.",
|
||||
id: 'servers.plan.small.description',
|
||||
defaultMessage: 'Perfect for 1–5 friends with a few light mods.',
|
||||
}),
|
||||
mostPopular: false,
|
||||
},
|
||||
medium: {
|
||||
buttonColor: "green",
|
||||
accentText: "text-green",
|
||||
accentBg: "bg-bg-green",
|
||||
buttonColor: 'green',
|
||||
accentText: 'text-green',
|
||||
accentBg: 'bg-bg-green',
|
||||
name: defineMessage({
|
||||
id: "servers.plan.medium.name",
|
||||
defaultMessage: "Medium",
|
||||
id: 'servers.plan.medium.name',
|
||||
defaultMessage: 'Medium',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: "servers.plan.medium.description",
|
||||
defaultMessage: "Great for 6–15 players and multiple mods.",
|
||||
id: 'servers.plan.medium.description',
|
||||
defaultMessage: 'Great for 6–15 players and multiple mods.',
|
||||
}),
|
||||
mostPopular: true,
|
||||
},
|
||||
large: {
|
||||
buttonColor: "purple",
|
||||
accentText: "text-purple",
|
||||
accentBg: "bg-bg-purple",
|
||||
buttonColor: 'purple',
|
||||
accentText: 'text-purple',
|
||||
accentBg: 'bg-bg-purple',
|
||||
name: defineMessage({
|
||||
id: "servers.plan.large.name",
|
||||
defaultMessage: "Large",
|
||||
id: 'servers.plan.large.name',
|
||||
defaultMessage: 'Large',
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: "servers.plan.large.description",
|
||||
defaultMessage: "Ideal for 15–25 players, modpacks, or heavy modding.",
|
||||
id: 'servers.plan.large.description',
|
||||
defaultMessage: 'Ideal for 15–25 players, modpacks, or heavy modding.',
|
||||
}),
|
||||
mostPopular: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
capacity?: number;
|
||||
plan: Plan;
|
||||
ram: number;
|
||||
storage: number;
|
||||
cpus: number;
|
||||
price: number;
|
||||
interval: "monthly" | "quarterly" | "yearly";
|
||||
currency: string;
|
||||
isUsa: boolean;
|
||||
}>();
|
||||
capacity?: number
|
||||
plan: Plan
|
||||
ram: number
|
||||
storage: number
|
||||
cpus: number
|
||||
price: number
|
||||
interval: 'monthly' | 'quarterly' | 'yearly'
|
||||
currency: string
|
||||
isUsa: boolean
|
||||
}>()
|
||||
|
||||
const outOfStock = computed(() => {
|
||||
return !props.capacity || props.capacity === 0;
|
||||
});
|
||||
return !props.capacity || props.capacity === 0
|
||||
})
|
||||
|
||||
const billingMonths = computed(() => {
|
||||
if (props.interval === "yearly") {
|
||||
return 12;
|
||||
} else if (props.interval === "quarterly") {
|
||||
return 3;
|
||||
if (props.interval === 'yearly') {
|
||||
return 12
|
||||
} else if (props.interval === 'quarterly') {
|
||||
return 3
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
return 1
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -122,7 +122,7 @@ const billingMonths = computed(() => {
|
||||
</div>
|
||||
<span class="m-0 text-2xl font-bold text-contrast">
|
||||
{{ formatPrice(locale, price / billingMonths, currency, true) }}
|
||||
{{ isUsa ? "" : currency }}
|
||||
{{ isUsa ? '' : currency }}
|
||||
<span class="text-lg font-semibold text-secondary">
|
||||
/ month<template v-if="interval !== 'monthly'">, billed {{ interval }}</template>
|
||||
</span>
|
||||
|
||||
@ -1,112 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import { PlusIcon, XIcon } from "@modrinth/assets";
|
||||
import { Accordion, ButtonStyled, NewModal, ServerNotice, TagItem } from "@modrinth/ui";
|
||||
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
|
||||
import { ref } from "vue";
|
||||
import { PlusIcon, XIcon } from '@modrinth/assets'
|
||||
import { Accordion, ButtonStyled, NewModal, ServerNotice, TagItem } from '@modrinth/ui'
|
||||
import type { ServerNotice as ServerNoticeType } from '@modrinth/utils'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
|
||||
import { useServersFetch } from '~/composables/servers/servers-fetch.ts'
|
||||
|
||||
const app = useNuxtApp() as unknown as { $notify: any };
|
||||
const app = useNuxtApp() as unknown as { $notify: any }
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>();
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "close"): void;
|
||||
}>();
|
||||
(e: 'close'): void
|
||||
}>()
|
||||
|
||||
const notice = ref<ServerNoticeType>();
|
||||
const notice = ref<ServerNoticeType>()
|
||||
|
||||
const assigned = ref<ServerNoticeType["assigned"]>([]);
|
||||
const assigned = ref<ServerNoticeType['assigned']>([])
|
||||
|
||||
const assignedServers = computed(() => assigned.value.filter((n) => n.kind === "server") ?? []);
|
||||
const assignedNodes = computed(() => assigned.value.filter((n) => n.kind === "node") ?? []);
|
||||
const assignedServers = computed(() => assigned.value.filter((n) => n.kind === 'server') ?? [])
|
||||
const assignedNodes = computed(() => assigned.value.filter((n) => n.kind === 'node') ?? [])
|
||||
|
||||
const inputField = ref("");
|
||||
const inputField = ref('')
|
||||
|
||||
async function refresh() {
|
||||
await useServersFetch("notices").then((res) => {
|
||||
const notices = res as ServerNoticeType[];
|
||||
assigned.value = notices.find((n) => n.id === notice.value?.id)?.assigned ?? [];
|
||||
});
|
||||
await useServersFetch('notices').then((res) => {
|
||||
const notices = res as ServerNoticeType[]
|
||||
assigned.value = notices.find((n) => n.id === notice.value?.id)?.assigned ?? []
|
||||
})
|
||||
}
|
||||
|
||||
async function assign(server: boolean = true) {
|
||||
const input = inputField.value.trim();
|
||||
const input = inputField.value.trim()
|
||||
|
||||
if (input !== "" && notice.value) {
|
||||
if (input !== '' && notice.value) {
|
||||
await useServersFetch(
|
||||
`notices/${notice.value.id}/assign?${server ? "server" : "node"}=${input}`,
|
||||
`notices/${notice.value.id}/assign?${server ? 'server' : 'node'}=${input}`,
|
||||
{
|
||||
method: "PUT",
|
||||
method: 'PUT',
|
||||
},
|
||||
).catch((err) => {
|
||||
app.$notify({
|
||||
group: "main",
|
||||
title: "Error assigning notice",
|
||||
group: 'main',
|
||||
title: 'Error assigning notice',
|
||||
text: err,
|
||||
type: "error",
|
||||
});
|
||||
});
|
||||
type: 'error',
|
||||
})
|
||||
})
|
||||
} else {
|
||||
app.$notify({
|
||||
group: "main",
|
||||
title: "Error assigning notice",
|
||||
text: "No server or node specified",
|
||||
type: "error",
|
||||
});
|
||||
group: 'main',
|
||||
title: 'Error assigning notice',
|
||||
text: 'No server or node specified',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
await refresh();
|
||||
await refresh()
|
||||
}
|
||||
|
||||
async function unassignDetect() {
|
||||
const input = inputField.value.trim();
|
||||
const input = inputField.value.trim()
|
||||
|
||||
const server = assignedServers.value.some((assigned) => assigned.id === input);
|
||||
const node = assignedNodes.value.some((assigned) => assigned.id === input);
|
||||
const server = assignedServers.value.some((assigned) => assigned.id === input)
|
||||
const node = assignedNodes.value.some((assigned) => assigned.id === input)
|
||||
|
||||
if (!server && !node) {
|
||||
app.$notify({
|
||||
group: "main",
|
||||
title: "Error unassigning notice",
|
||||
text: "ID is not an assigned server or node",
|
||||
type: "error",
|
||||
});
|
||||
return;
|
||||
group: 'main',
|
||||
title: 'Error unassigning notice',
|
||||
text: 'ID is not an assigned server or node',
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await unassign(input, server);
|
||||
await unassign(input, server)
|
||||
}
|
||||
|
||||
async function unassign(id: string, server: boolean = true) {
|
||||
if (notice.value) {
|
||||
await useServersFetch(
|
||||
`notices/${notice.value.id}/unassign?${server ? "server" : "node"}=${id}`,
|
||||
`notices/${notice.value.id}/unassign?${server ? 'server' : 'node'}=${id}`,
|
||||
{
|
||||
method: "PUT",
|
||||
method: 'PUT',
|
||||
},
|
||||
).catch((err) => {
|
||||
app.$notify({
|
||||
group: "main",
|
||||
title: "Error unassigning notice",
|
||||
group: 'main',
|
||||
title: 'Error unassigning notice',
|
||||
text: err,
|
||||
type: "error",
|
||||
});
|
||||
});
|
||||
type: 'error',
|
||||
})
|
||||
})
|
||||
}
|
||||
await refresh();
|
||||
await refresh()
|
||||
}
|
||||
|
||||
function show(currentNotice: ServerNoticeType) {
|
||||
notice.value = currentNotice;
|
||||
assigned.value = currentNotice?.assigned ?? [];
|
||||
modal.value?.show();
|
||||
notice.value = currentNotice
|
||||
assigned.value = currentNotice?.assigned ?? []
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
modal.value?.hide();
|
||||
modal.value?.hide()
|
||||
}
|
||||
|
||||
defineExpose({ show, hide });
|
||||
defineExpose({ show, hide })
|
||||
</script>
|
||||
<template>
|
||||
<NewModal ref="modal" :on-hide="() => emit('close')">
|
||||
@ -165,7 +165,7 @@ defineExpose({ show, hide });
|
||||
:key="`node-${node.id}`"
|
||||
:action="
|
||||
() => {
|
||||
unassign(node.id, false);
|
||||
unassign(node.id, false)
|
||||
}
|
||||
"
|
||||
>
|
||||
|
||||
@ -102,11 +102,11 @@ import {
|
||||
MoreHorizontalIcon,
|
||||
ScaleIcon,
|
||||
TrashIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { AutoLink, Avatar, Badge, OverflowMenu, useRelativeTime } from "@modrinth/ui";
|
||||
import { renderString } from "@modrinth/utils";
|
||||
} from '@modrinth/assets'
|
||||
import { AutoLink, Avatar, Badge, OverflowMenu, useRelativeTime } from '@modrinth/ui'
|
||||
import { renderString } from '@modrinth/utils'
|
||||
|
||||
import { isStaff } from "~/helpers/users.js";
|
||||
import { isStaff } from '~/helpers/users.js'
|
||||
|
||||
const props = defineProps({
|
||||
message: {
|
||||
@ -137,34 +137,34 @@ const props = defineProps({
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const emit = defineEmits(["update-thread"]);
|
||||
const emit = defineEmits(['update-thread'])
|
||||
|
||||
const formattedMessage = computed(() => {
|
||||
const body = renderString(props.message.body.body);
|
||||
const body = renderString(props.message.body.body)
|
||||
if (props.forceCompact) {
|
||||
const hasImage = body.includes("<img");
|
||||
const noHtml = body.replace(/<\/?[^>]+(>|$)/g, "");
|
||||
const hasImage = body.includes('<img')
|
||||
const noHtml = body.replace(/<\/?[^>]+(>|$)/g, '')
|
||||
if (noHtml.trim()) {
|
||||
return noHtml;
|
||||
return noHtml
|
||||
} else if (hasImage) {
|
||||
return "sent an image.";
|
||||
return 'sent an image.'
|
||||
} else {
|
||||
return "sent a message.";
|
||||
return 'sent a message.'
|
||||
}
|
||||
}
|
||||
return body;
|
||||
});
|
||||
return body
|
||||
})
|
||||
|
||||
const formatRelativeTime = useRelativeTime();
|
||||
const timeSincePosted = ref(formatRelativeTime(props.message.created));
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
const timeSincePosted = ref(formatRelativeTime(props.message.created))
|
||||
|
||||
async function deleteMessage() {
|
||||
await useBaseFetch(`message/${props.message.id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
emit("update-thread");
|
||||
method: 'DELETE',
|
||||
})
|
||||
emit('update-thread')
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -189,9 +189,9 @@ async function deleteMessage() {
|
||||
--gap-size: var(--spacing-card-sm);
|
||||
display: grid;
|
||||
grid-template:
|
||||
"icon author actions"
|
||||
"icon body actions"
|
||||
"date date date";
|
||||
'icon author actions'
|
||||
'icon body actions'
|
||||
'date date date';
|
||||
grid-template-columns: min-content auto 1fr;
|
||||
column-gap: var(--gap-size);
|
||||
row-gap: var(--spacing-card-xs);
|
||||
@ -307,9 +307,9 @@ a:active + .message__author a,
|
||||
|
||||
&.has-body {
|
||||
grid-template:
|
||||
"icon author actions"
|
||||
"icon body actions"
|
||||
"date date date";
|
||||
'icon author actions'
|
||||
'icon body actions'
|
||||
'date date date';
|
||||
grid-template-columns: min-content auto 1fr;
|
||||
}
|
||||
}
|
||||
@ -322,8 +322,8 @@ a:active + .message__author a,
|
||||
|
||||
&.has-body {
|
||||
grid-template:
|
||||
"icon author date actions"
|
||||
"icon body body actions";
|
||||
'icon author date actions'
|
||||
'icon body body actions';
|
||||
grid-template-columns: min-content auto 1fr;
|
||||
grid-template-rows: min-content 1fr auto;
|
||||
}
|
||||
|
||||
@ -24,9 +24,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ChevronRightIcon } from "@modrinth/assets";
|
||||
import { ChevronRightIcon } from '@modrinth/assets'
|
||||
|
||||
import ThreadMessage from "~/components/ui/thread/ThreadMessage.vue";
|
||||
import ThreadMessage from '~/components/ui/thread/ThreadMessage.vue'
|
||||
|
||||
const props = defineProps({
|
||||
thread: {
|
||||
@ -50,36 +50,36 @@ const props = defineProps({
|
||||
type: Array,
|
||||
required: false,
|
||||
default() {
|
||||
return [];
|
||||
return []
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const app = useNuxtApp();
|
||||
const app = useNuxtApp()
|
||||
|
||||
const members = computed(() => {
|
||||
const members = {};
|
||||
const members = {}
|
||||
for (const member of props.thread.members) {
|
||||
members[member.id] = member;
|
||||
members[member.id] = member
|
||||
}
|
||||
members[props.auth.user.id] = props.auth.user;
|
||||
return members;
|
||||
});
|
||||
members[props.auth.user.id] = props.auth.user
|
||||
return members
|
||||
})
|
||||
|
||||
const displayMessages = computed(() => {
|
||||
const sortedMessages = props.thread.messages
|
||||
.slice()
|
||||
.sort((a, b) => app.$dayjs(a.created) - app.$dayjs(b.created));
|
||||
.sort((a, b) => app.$dayjs(a.created) - app.$dayjs(b.created))
|
||||
if (props.messages.length > 0) {
|
||||
return sortedMessages.filter((msg) => props.messages.includes(msg.id));
|
||||
return sortedMessages.filter((msg) => props.messages.includes(msg.id))
|
||||
} else {
|
||||
return sortedMessages.length > 0 ? [sortedMessages[sortedMessages.length - 1]] : [];
|
||||
return sortedMessages.length > 0 ? [sortedMessages[sortedMessages.length - 1]] : []
|
||||
}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -1,71 +1,71 @@
|
||||
export const useAuth = async (oldToken = null) => {
|
||||
const auth = useState("auth", () => ({
|
||||
const auth = useState('auth', () => ({
|
||||
user: null,
|
||||
token: "",
|
||||
token: '',
|
||||
headers: {},
|
||||
}));
|
||||
}))
|
||||
|
||||
if (!auth.value.user || oldToken) {
|
||||
auth.value = await initAuth(oldToken);
|
||||
auth.value = await initAuth(oldToken)
|
||||
}
|
||||
|
||||
return auth;
|
||||
};
|
||||
return auth
|
||||
}
|
||||
|
||||
export const initAuth = async (oldToken = null) => {
|
||||
const auth = {
|
||||
user: null,
|
||||
token: "",
|
||||
};
|
||||
|
||||
if (oldToken === "none") {
|
||||
return auth;
|
||||
token: '',
|
||||
}
|
||||
|
||||
const route = useRoute();
|
||||
const authCookie = useCookie("auth-token", {
|
||||
if (oldToken === 'none') {
|
||||
return auth
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const authCookie = useCookie('auth-token', {
|
||||
maxAge: 60 * 60 * 24 * 365 * 10,
|
||||
sameSite: "lax",
|
||||
sameSite: 'lax',
|
||||
secure: true,
|
||||
httpOnly: false,
|
||||
path: "/",
|
||||
});
|
||||
path: '/',
|
||||
})
|
||||
|
||||
if (oldToken) {
|
||||
authCookie.value = oldToken;
|
||||
authCookie.value = oldToken
|
||||
}
|
||||
|
||||
if (route.query.code && !route.fullPath.includes("new_account=true")) {
|
||||
authCookie.value = route.query.code;
|
||||
if (route.query.code && !route.fullPath.includes('new_account=true')) {
|
||||
authCookie.value = route.query.code
|
||||
}
|
||||
|
||||
if (route.fullPath.includes("new_account=true") && route.path !== "/auth/welcome") {
|
||||
const redirect = route.path.startsWith("/auth/") ? null : route.fullPath;
|
||||
if (route.fullPath.includes('new_account=true') && route.path !== '/auth/welcome') {
|
||||
const redirect = route.path.startsWith('/auth/') ? null : route.fullPath
|
||||
|
||||
await navigateTo(
|
||||
`/auth/welcome?authToken=${route.query.code}${
|
||||
redirect ? `&redirect=${encodeURIComponent(redirect)}` : ""
|
||||
redirect ? `&redirect=${encodeURIComponent(redirect)}` : ''
|
||||
}`,
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
if (authCookie.value) {
|
||||
auth.token = authCookie.value;
|
||||
auth.token = authCookie.value
|
||||
|
||||
if (!auth.token || !auth.token.startsWith("mra_")) {
|
||||
return auth;
|
||||
if (!auth.token || !auth.token.startsWith('mra_')) {
|
||||
return auth
|
||||
}
|
||||
|
||||
try {
|
||||
auth.user = await useBaseFetch(
|
||||
"user",
|
||||
'user',
|
||||
{
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
);
|
||||
)
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
@ -74,67 +74,67 @@ export const initAuth = async (oldToken = null) => {
|
||||
if (!auth.user && auth.token) {
|
||||
try {
|
||||
const session = await useBaseFetch(
|
||||
"session/refresh",
|
||||
'session/refresh',
|
||||
{
|
||||
method: "POST",
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
);
|
||||
)
|
||||
|
||||
auth.token = session.session;
|
||||
authCookie.value = auth.token;
|
||||
auth.token = session.session
|
||||
authCookie.value = auth.token
|
||||
|
||||
auth.user = await useBaseFetch(
|
||||
"user",
|
||||
'user',
|
||||
{
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
);
|
||||
)
|
||||
} catch {
|
||||
authCookie.value = null;
|
||||
authCookie.value = null
|
||||
}
|
||||
}
|
||||
|
||||
return auth;
|
||||
};
|
||||
return auth
|
||||
}
|
||||
|
||||
export const getAuthUrl = (provider, redirect = "/dashboard") => {
|
||||
const config = useRuntimeConfig();
|
||||
const route = useNativeRoute();
|
||||
export const getAuthUrl = (provider, redirect = '/dashboard') => {
|
||||
const config = useRuntimeConfig()
|
||||
const route = useNativeRoute()
|
||||
|
||||
const fullURL = route.query.launcher
|
||||
? "https://launcher-files.modrinth.com"
|
||||
: `${config.public.siteUrl}/auth/sign-in?redirect=${redirect}`;
|
||||
? 'https://launcher-files.modrinth.com'
|
||||
: `${config.public.siteUrl}/auth/sign-in?redirect=${redirect}`
|
||||
|
||||
return `${config.public.apiBaseUrl}auth/init?provider=${provider}&url=${encodeURIComponent(fullURL)}`;
|
||||
};
|
||||
return `${config.public.apiBaseUrl}auth/init?provider=${provider}&url=${encodeURIComponent(fullURL)}`
|
||||
}
|
||||
|
||||
export const removeAuthProvider = async (provider) => {
|
||||
startLoading();
|
||||
startLoading()
|
||||
try {
|
||||
const auth = await useAuth();
|
||||
const auth = await useAuth()
|
||||
|
||||
await useBaseFetch("auth/provider", {
|
||||
method: "DELETE",
|
||||
await useBaseFetch('auth/provider', {
|
||||
method: 'DELETE',
|
||||
body: {
|
||||
provider,
|
||||
},
|
||||
});
|
||||
await useAuth(auth.value.token);
|
||||
})
|
||||
await useAuth(auth.value.token)
|
||||
} catch (err) {
|
||||
const data = useNuxtApp();
|
||||
const data = useNuxtApp()
|
||||
data.$notify({
|
||||
group: "main",
|
||||
title: "An error occurred",
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data.description,
|
||||
type: "error",
|
||||
});
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading();
|
||||
};
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
@ -1,576 +1,576 @@
|
||||
export const scopeMessages = defineMessages({
|
||||
userReadEmailLabel: {
|
||||
id: "scopes.userReadEmail.label",
|
||||
defaultMessage: "Read user email",
|
||||
id: 'scopes.userReadEmail.label',
|
||||
defaultMessage: 'Read user email',
|
||||
},
|
||||
userReadEmailDescription: {
|
||||
id: "scopes.userReadEmail.description",
|
||||
defaultMessage: "Read your email",
|
||||
id: 'scopes.userReadEmail.description',
|
||||
defaultMessage: 'Read your email',
|
||||
},
|
||||
userReadLabel: {
|
||||
id: "scopes.userRead.label",
|
||||
defaultMessage: "Read user data",
|
||||
id: 'scopes.userRead.label',
|
||||
defaultMessage: 'Read user data',
|
||||
},
|
||||
userReadDescription: {
|
||||
id: "scopes.userRead.description",
|
||||
defaultMessage: "Access your public profile information",
|
||||
id: 'scopes.userRead.description',
|
||||
defaultMessage: 'Access your public profile information',
|
||||
},
|
||||
userWriteLabel: {
|
||||
id: "scopes.userWrite.label",
|
||||
defaultMessage: "Write user data",
|
||||
id: 'scopes.userWrite.label',
|
||||
defaultMessage: 'Write user data',
|
||||
},
|
||||
userWriteDescription: {
|
||||
id: "scopes.userWrite.description",
|
||||
defaultMessage: "Write to your profile",
|
||||
id: 'scopes.userWrite.description',
|
||||
defaultMessage: 'Write to your profile',
|
||||
},
|
||||
userDeleteLabel: {
|
||||
id: "scopes.userDelete.label",
|
||||
defaultMessage: "Delete your account",
|
||||
id: 'scopes.userDelete.label',
|
||||
defaultMessage: 'Delete your account',
|
||||
},
|
||||
userDeleteDescription: {
|
||||
id: "scopes.userDelete.description",
|
||||
defaultMessage: "Delete your account",
|
||||
id: 'scopes.userDelete.description',
|
||||
defaultMessage: 'Delete your account',
|
||||
},
|
||||
userAuthWriteLabel: {
|
||||
id: "scopes.userAuthWrite.label",
|
||||
defaultMessage: "Write auth data",
|
||||
id: 'scopes.userAuthWrite.label',
|
||||
defaultMessage: 'Write auth data',
|
||||
},
|
||||
userAuthWriteDescription: {
|
||||
id: "scopes.userAuthWrite.description",
|
||||
defaultMessage: "Modify your authentication data",
|
||||
id: 'scopes.userAuthWrite.description',
|
||||
defaultMessage: 'Modify your authentication data',
|
||||
},
|
||||
notificationReadLabel: {
|
||||
id: "scopes.notificationRead.label",
|
||||
defaultMessage: "Read notifications",
|
||||
id: 'scopes.notificationRead.label',
|
||||
defaultMessage: 'Read notifications',
|
||||
},
|
||||
notificationReadDescription: {
|
||||
id: "scopes.notificationRead.description",
|
||||
defaultMessage: "Read your notifications",
|
||||
id: 'scopes.notificationRead.description',
|
||||
defaultMessage: 'Read your notifications',
|
||||
},
|
||||
notificationWriteLabel: {
|
||||
id: "scopes.notificationWrite.label",
|
||||
defaultMessage: "Write notifications",
|
||||
id: 'scopes.notificationWrite.label',
|
||||
defaultMessage: 'Write notifications',
|
||||
},
|
||||
notificationWriteDescription: {
|
||||
id: "scopes.notificationWrite.description",
|
||||
defaultMessage: "Delete/View your notifications",
|
||||
id: 'scopes.notificationWrite.description',
|
||||
defaultMessage: 'Delete/View your notifications',
|
||||
},
|
||||
payoutsReadLabel: {
|
||||
id: "scopes.payoutsRead.label",
|
||||
defaultMessage: "Read payouts",
|
||||
id: 'scopes.payoutsRead.label',
|
||||
defaultMessage: 'Read payouts',
|
||||
},
|
||||
payoutsReadDescription: {
|
||||
id: "scopes.payoutsRead.description",
|
||||
defaultMessage: "Read your payouts data",
|
||||
id: 'scopes.payoutsRead.description',
|
||||
defaultMessage: 'Read your payouts data',
|
||||
},
|
||||
payoutsWriteLabel: {
|
||||
id: "scopes.payoutsWrite.label",
|
||||
defaultMessage: "Write payouts",
|
||||
id: 'scopes.payoutsWrite.label',
|
||||
defaultMessage: 'Write payouts',
|
||||
},
|
||||
payoutsWriteDescription: {
|
||||
id: "scopes.payoutsWrite.description",
|
||||
defaultMessage: "Withdraw money",
|
||||
id: 'scopes.payoutsWrite.description',
|
||||
defaultMessage: 'Withdraw money',
|
||||
},
|
||||
analyticsLabel: {
|
||||
id: "scopes.analytics.label",
|
||||
defaultMessage: "Read analytics",
|
||||
id: 'scopes.analytics.label',
|
||||
defaultMessage: 'Read analytics',
|
||||
},
|
||||
analyticsDescription: {
|
||||
id: "scopes.analytics.description",
|
||||
defaultMessage: "Access your analytics data",
|
||||
id: 'scopes.analytics.description',
|
||||
defaultMessage: 'Access your analytics data',
|
||||
},
|
||||
projectCreateLabel: {
|
||||
id: "scopes.projectCreate.label",
|
||||
defaultMessage: "Create projects",
|
||||
id: 'scopes.projectCreate.label',
|
||||
defaultMessage: 'Create projects',
|
||||
},
|
||||
projectCreateDescription: {
|
||||
id: "scopes.projectCreate.description",
|
||||
defaultMessage: "Create new projects",
|
||||
id: 'scopes.projectCreate.description',
|
||||
defaultMessage: 'Create new projects',
|
||||
},
|
||||
projectReadLabel: {
|
||||
id: "scopes.projectRead.label",
|
||||
defaultMessage: "Read projects",
|
||||
id: 'scopes.projectRead.label',
|
||||
defaultMessage: 'Read projects',
|
||||
},
|
||||
projectReadDescription: {
|
||||
id: "scopes.projectRead.description",
|
||||
defaultMessage: "Read all your projects",
|
||||
id: 'scopes.projectRead.description',
|
||||
defaultMessage: 'Read all your projects',
|
||||
},
|
||||
projectWriteLabel: {
|
||||
id: "scopes.projectWrite.label",
|
||||
defaultMessage: "Write projects",
|
||||
id: 'scopes.projectWrite.label',
|
||||
defaultMessage: 'Write projects',
|
||||
},
|
||||
projectWriteDescription: {
|
||||
id: "scopes.projectWrite.description",
|
||||
defaultMessage: "Write to project data",
|
||||
id: 'scopes.projectWrite.description',
|
||||
defaultMessage: 'Write to project data',
|
||||
},
|
||||
projectDeleteLabel: {
|
||||
id: "scopes.projectDelete.label",
|
||||
defaultMessage: "Delete projects",
|
||||
id: 'scopes.projectDelete.label',
|
||||
defaultMessage: 'Delete projects',
|
||||
},
|
||||
projectDeleteDescription: {
|
||||
id: "scopes.projectDelete.description",
|
||||
defaultMessage: "Delete your projects",
|
||||
id: 'scopes.projectDelete.description',
|
||||
defaultMessage: 'Delete your projects',
|
||||
},
|
||||
versionCreateLabel: {
|
||||
id: "scopes.versionCreate.label",
|
||||
defaultMessage: "Create versions",
|
||||
id: 'scopes.versionCreate.label',
|
||||
defaultMessage: 'Create versions',
|
||||
},
|
||||
versionCreateDescription: {
|
||||
id: "scopes.versionCreate.description",
|
||||
defaultMessage: "Create new versions",
|
||||
id: 'scopes.versionCreate.description',
|
||||
defaultMessage: 'Create new versions',
|
||||
},
|
||||
versionReadLabel: {
|
||||
id: "scopes.versionRead.label",
|
||||
defaultMessage: "Read versions",
|
||||
id: 'scopes.versionRead.label',
|
||||
defaultMessage: 'Read versions',
|
||||
},
|
||||
versionReadDescription: {
|
||||
id: "scopes.versionRead.description",
|
||||
defaultMessage: "Read all versions",
|
||||
id: 'scopes.versionRead.description',
|
||||
defaultMessage: 'Read all versions',
|
||||
},
|
||||
versionWriteLabel: {
|
||||
id: "scopes.versionWrite.label",
|
||||
defaultMessage: "Write versions",
|
||||
id: 'scopes.versionWrite.label',
|
||||
defaultMessage: 'Write versions',
|
||||
},
|
||||
versionWriteDescription: {
|
||||
id: "scopes.versionWrite.description",
|
||||
defaultMessage: "Write to version data",
|
||||
id: 'scopes.versionWrite.description',
|
||||
defaultMessage: 'Write to version data',
|
||||
},
|
||||
versionDeleteLabel: {
|
||||
id: "scopes.versionDelete.label",
|
||||
defaultMessage: "Delete versions",
|
||||
id: 'scopes.versionDelete.label',
|
||||
defaultMessage: 'Delete versions',
|
||||
},
|
||||
versionDeleteDescription: {
|
||||
id: "scopes.versionDelete.description",
|
||||
defaultMessage: "Delete a version",
|
||||
id: 'scopes.versionDelete.description',
|
||||
defaultMessage: 'Delete a version',
|
||||
},
|
||||
reportCreateLabel: {
|
||||
id: "scopes.reportCreate.label",
|
||||
defaultMessage: "Create reports",
|
||||
id: 'scopes.reportCreate.label',
|
||||
defaultMessage: 'Create reports',
|
||||
},
|
||||
reportCreateDescription: {
|
||||
id: "scopes.reportCreate.description",
|
||||
defaultMessage: "Create reports",
|
||||
id: 'scopes.reportCreate.description',
|
||||
defaultMessage: 'Create reports',
|
||||
},
|
||||
reportReadLabel: {
|
||||
id: "scopes.reportRead.label",
|
||||
defaultMessage: "Read reports",
|
||||
id: 'scopes.reportRead.label',
|
||||
defaultMessage: 'Read reports',
|
||||
},
|
||||
reportReadDescription: {
|
||||
id: "scopes.reportRead.description",
|
||||
defaultMessage: "Read reports",
|
||||
id: 'scopes.reportRead.description',
|
||||
defaultMessage: 'Read reports',
|
||||
},
|
||||
reportWriteLabel: {
|
||||
id: "scopes.reportWrite.label",
|
||||
defaultMessage: "Write reports",
|
||||
id: 'scopes.reportWrite.label',
|
||||
defaultMessage: 'Write reports',
|
||||
},
|
||||
reportWriteDescription: {
|
||||
id: "scopes.reportWrite.description",
|
||||
defaultMessage: "Edit reports",
|
||||
id: 'scopes.reportWrite.description',
|
||||
defaultMessage: 'Edit reports',
|
||||
},
|
||||
reportDeleteLabel: {
|
||||
id: "scopes.reportDelete.label",
|
||||
defaultMessage: "Delete reports",
|
||||
id: 'scopes.reportDelete.label',
|
||||
defaultMessage: 'Delete reports',
|
||||
},
|
||||
reportDeleteDescription: {
|
||||
id: "scopes.reportDelete.description",
|
||||
defaultMessage: "Delete reports",
|
||||
id: 'scopes.reportDelete.description',
|
||||
defaultMessage: 'Delete reports',
|
||||
},
|
||||
threadReadLabel: {
|
||||
id: "scopes.threadRead.label",
|
||||
defaultMessage: "Read threads",
|
||||
id: 'scopes.threadRead.label',
|
||||
defaultMessage: 'Read threads',
|
||||
},
|
||||
threadReadDescription: {
|
||||
id: "scopes.threadRead.description",
|
||||
defaultMessage: "Read threads",
|
||||
id: 'scopes.threadRead.description',
|
||||
defaultMessage: 'Read threads',
|
||||
},
|
||||
threadWriteLabel: {
|
||||
id: "scopes.threadWrite.label",
|
||||
defaultMessage: "Write threads",
|
||||
id: 'scopes.threadWrite.label',
|
||||
defaultMessage: 'Write threads',
|
||||
},
|
||||
threadWriteDescription: {
|
||||
id: "scopes.threadWrite.description",
|
||||
defaultMessage: "Write to threads",
|
||||
id: 'scopes.threadWrite.description',
|
||||
defaultMessage: 'Write to threads',
|
||||
},
|
||||
patCreateLabel: {
|
||||
id: "scopes.patCreate.label",
|
||||
defaultMessage: "Create PATs",
|
||||
id: 'scopes.patCreate.label',
|
||||
defaultMessage: 'Create PATs',
|
||||
},
|
||||
patCreateDescription: {
|
||||
id: "scopes.patCreate.description",
|
||||
defaultMessage: "Create personal API tokens",
|
||||
id: 'scopes.patCreate.description',
|
||||
defaultMessage: 'Create personal API tokens',
|
||||
},
|
||||
patReadLabel: {
|
||||
id: "scopes.patRead.label",
|
||||
defaultMessage: "Read PATs",
|
||||
id: 'scopes.patRead.label',
|
||||
defaultMessage: 'Read PATs',
|
||||
},
|
||||
patReadDescription: {
|
||||
id: "scopes.patRead.description",
|
||||
defaultMessage: "View created API tokens",
|
||||
id: 'scopes.patRead.description',
|
||||
defaultMessage: 'View created API tokens',
|
||||
},
|
||||
patWriteLabel: {
|
||||
id: "scopes.patWrite.label",
|
||||
defaultMessage: "Write PATs",
|
||||
id: 'scopes.patWrite.label',
|
||||
defaultMessage: 'Write PATs',
|
||||
},
|
||||
patWriteDescription: {
|
||||
id: "scopes.patWrite.description",
|
||||
defaultMessage: "Edit personal API tokens",
|
||||
id: 'scopes.patWrite.description',
|
||||
defaultMessage: 'Edit personal API tokens',
|
||||
},
|
||||
patDeleteLabel: {
|
||||
id: "scopes.patDelete.label",
|
||||
defaultMessage: "Delete PATs",
|
||||
id: 'scopes.patDelete.label',
|
||||
defaultMessage: 'Delete PATs',
|
||||
},
|
||||
patDeleteDescription: {
|
||||
id: "scopes.patDelete.description",
|
||||
defaultMessage: "Delete your personal API tokens",
|
||||
id: 'scopes.patDelete.description',
|
||||
defaultMessage: 'Delete your personal API tokens',
|
||||
},
|
||||
sessionReadLabel: {
|
||||
id: "scopes.sessionRead.label",
|
||||
defaultMessage: "Read sessions",
|
||||
id: 'scopes.sessionRead.label',
|
||||
defaultMessage: 'Read sessions',
|
||||
},
|
||||
sessionReadDescription: {
|
||||
id: "scopes.sessionRead.description",
|
||||
defaultMessage: "Read active sessions",
|
||||
id: 'scopes.sessionRead.description',
|
||||
defaultMessage: 'Read active sessions',
|
||||
},
|
||||
sessionDeleteLabel: {
|
||||
id: "scopes.sessionDelete.label",
|
||||
defaultMessage: "Delete sessions",
|
||||
id: 'scopes.sessionDelete.label',
|
||||
defaultMessage: 'Delete sessions',
|
||||
},
|
||||
sessionDeleteDescription: {
|
||||
id: "scopes.sessionDelete.description",
|
||||
defaultMessage: "Delete sessions",
|
||||
id: 'scopes.sessionDelete.description',
|
||||
defaultMessage: 'Delete sessions',
|
||||
},
|
||||
performAnalyticsLabel: {
|
||||
id: "scopes.performAnalytics.label",
|
||||
defaultMessage: "Perform analytics",
|
||||
id: 'scopes.performAnalytics.label',
|
||||
defaultMessage: 'Perform analytics',
|
||||
},
|
||||
performAnalyticsDescription: {
|
||||
id: "scopes.performAnalytics.description",
|
||||
defaultMessage: "Perform analytics actions",
|
||||
id: 'scopes.performAnalytics.description',
|
||||
defaultMessage: 'Perform analytics actions',
|
||||
},
|
||||
collectionCreateLabel: {
|
||||
id: "scopes.collectionCreate.label",
|
||||
defaultMessage: "Create collections",
|
||||
id: 'scopes.collectionCreate.label',
|
||||
defaultMessage: 'Create collections',
|
||||
},
|
||||
collectionCreateDescription: {
|
||||
id: "scopes.collectionCreate.description",
|
||||
defaultMessage: "Create collections",
|
||||
id: 'scopes.collectionCreate.description',
|
||||
defaultMessage: 'Create collections',
|
||||
},
|
||||
collectionReadLabel: {
|
||||
id: "scopes.collectionRead.label",
|
||||
defaultMessage: "Read collections",
|
||||
id: 'scopes.collectionRead.label',
|
||||
defaultMessage: 'Read collections',
|
||||
},
|
||||
collectionReadDescription: {
|
||||
id: "scopes.collectionRead.description",
|
||||
defaultMessage: "Read collections",
|
||||
id: 'scopes.collectionRead.description',
|
||||
defaultMessage: 'Read collections',
|
||||
},
|
||||
collectionWriteLabel: {
|
||||
id: "scopes.collectionWrite.label",
|
||||
defaultMessage: "Write collections",
|
||||
id: 'scopes.collectionWrite.label',
|
||||
defaultMessage: 'Write collections',
|
||||
},
|
||||
collectionWriteDescription: {
|
||||
id: "scopes.collectionWrite.description",
|
||||
defaultMessage: "Write to collections",
|
||||
id: 'scopes.collectionWrite.description',
|
||||
defaultMessage: 'Write to collections',
|
||||
},
|
||||
collectionDeleteLabel: {
|
||||
id: "scopes.collectionDelete.label",
|
||||
defaultMessage: "Delete collections",
|
||||
id: 'scopes.collectionDelete.label',
|
||||
defaultMessage: 'Delete collections',
|
||||
},
|
||||
collectionDeleteDescription: {
|
||||
id: "scopes.collectionDelete.description",
|
||||
defaultMessage: "Delete collections",
|
||||
id: 'scopes.collectionDelete.description',
|
||||
defaultMessage: 'Delete collections',
|
||||
},
|
||||
organizationCreateLabel: {
|
||||
id: "scopes.organizationCreate.label",
|
||||
defaultMessage: "Create organizations",
|
||||
id: 'scopes.organizationCreate.label',
|
||||
defaultMessage: 'Create organizations',
|
||||
},
|
||||
organizationCreateDescription: {
|
||||
id: "scopes.organizationCreate.description",
|
||||
defaultMessage: "Create organizations",
|
||||
id: 'scopes.organizationCreate.description',
|
||||
defaultMessage: 'Create organizations',
|
||||
},
|
||||
organizationReadLabel: {
|
||||
id: "scopes.organizationRead.label",
|
||||
defaultMessage: "Read organizations",
|
||||
id: 'scopes.organizationRead.label',
|
||||
defaultMessage: 'Read organizations',
|
||||
},
|
||||
organizationReadDescription: {
|
||||
id: "scopes.organizationRead.description",
|
||||
defaultMessage: "Read organizations",
|
||||
id: 'scopes.organizationRead.description',
|
||||
defaultMessage: 'Read organizations',
|
||||
},
|
||||
organizationWriteLabel: {
|
||||
id: "scopes.organizationWrite.label",
|
||||
defaultMessage: "Write organizations",
|
||||
id: 'scopes.organizationWrite.label',
|
||||
defaultMessage: 'Write organizations',
|
||||
},
|
||||
organizationWriteDescription: {
|
||||
id: "scopes.organizationWrite.description",
|
||||
defaultMessage: "Write to organizations",
|
||||
id: 'scopes.organizationWrite.description',
|
||||
defaultMessage: 'Write to organizations',
|
||||
},
|
||||
organizationDeleteLabel: {
|
||||
id: "scopes.organizationDelete.label",
|
||||
defaultMessage: "Delete organizations",
|
||||
id: 'scopes.organizationDelete.label',
|
||||
defaultMessage: 'Delete organizations',
|
||||
},
|
||||
organizationDeleteDescription: {
|
||||
id: "scopes.organizationDelete.description",
|
||||
defaultMessage: "Delete organizations",
|
||||
id: 'scopes.organizationDelete.description',
|
||||
defaultMessage: 'Delete organizations',
|
||||
},
|
||||
sessionAccessLabel: {
|
||||
id: "scopes.sessionAccess.label",
|
||||
defaultMessage: "Access sessions",
|
||||
id: 'scopes.sessionAccess.label',
|
||||
defaultMessage: 'Access sessions',
|
||||
},
|
||||
sessionAccessDescription: {
|
||||
id: "scopes.sessionAccess.description",
|
||||
defaultMessage: "Access modrinth-issued sessions",
|
||||
id: 'scopes.sessionAccess.description',
|
||||
defaultMessage: 'Access modrinth-issued sessions',
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const scopeDefinitions = [
|
||||
{
|
||||
id: "USER_READ_EMAIL",
|
||||
id: 'USER_READ_EMAIL',
|
||||
value: BigInt(1) << BigInt(0),
|
||||
label: scopeMessages.userReadEmailLabel,
|
||||
desc: scopeMessages.userReadEmailDescription,
|
||||
},
|
||||
{
|
||||
id: "USER_READ",
|
||||
id: 'USER_READ',
|
||||
value: BigInt(1) << BigInt(1),
|
||||
label: scopeMessages.userReadLabel,
|
||||
desc: scopeMessages.userReadDescription,
|
||||
},
|
||||
{
|
||||
id: "USER_WRITE",
|
||||
id: 'USER_WRITE',
|
||||
value: BigInt(1) << BigInt(2),
|
||||
label: scopeMessages.userWriteLabel,
|
||||
desc: scopeMessages.userWriteDescription,
|
||||
},
|
||||
{
|
||||
id: "USER_DELETE",
|
||||
id: 'USER_DELETE',
|
||||
value: BigInt(1) << BigInt(3),
|
||||
label: scopeMessages.userDeleteLabel,
|
||||
desc: scopeMessages.userDeleteDescription,
|
||||
},
|
||||
{
|
||||
id: "USER_AUTH_WRITE",
|
||||
id: 'USER_AUTH_WRITE',
|
||||
value: BigInt(1) << BigInt(4),
|
||||
label: scopeMessages.userAuthWriteLabel,
|
||||
desc: scopeMessages.userAuthWriteDescription,
|
||||
},
|
||||
{
|
||||
id: "NOTIFICATION_READ",
|
||||
id: 'NOTIFICATION_READ',
|
||||
value: BigInt(1) << BigInt(5),
|
||||
label: scopeMessages.notificationReadLabel,
|
||||
desc: scopeMessages.notificationReadDescription,
|
||||
},
|
||||
{
|
||||
id: "NOTIFICATION_WRITE",
|
||||
id: 'NOTIFICATION_WRITE',
|
||||
value: BigInt(1) << BigInt(6),
|
||||
label: scopeMessages.notificationWriteLabel,
|
||||
desc: scopeMessages.notificationWriteDescription,
|
||||
},
|
||||
{
|
||||
id: "PAYOUTS_READ",
|
||||
id: 'PAYOUTS_READ',
|
||||
value: BigInt(1) << BigInt(7),
|
||||
label: scopeMessages.payoutsReadLabel,
|
||||
desc: scopeMessages.payoutsReadDescription,
|
||||
},
|
||||
{
|
||||
id: "PAYOUTS_WRITE",
|
||||
id: 'PAYOUTS_WRITE',
|
||||
value: BigInt(1) << BigInt(8),
|
||||
label: scopeMessages.payoutsWriteLabel,
|
||||
desc: scopeMessages.payoutsWriteDescription,
|
||||
},
|
||||
{
|
||||
id: "ANALYTICS",
|
||||
id: 'ANALYTICS',
|
||||
value: BigInt(1) << BigInt(9),
|
||||
label: scopeMessages.analyticsLabel,
|
||||
desc: scopeMessages.analyticsDescription,
|
||||
},
|
||||
{
|
||||
id: "PROJECT_CREATE",
|
||||
id: 'PROJECT_CREATE',
|
||||
value: BigInt(1) << BigInt(10),
|
||||
label: scopeMessages.projectCreateLabel,
|
||||
desc: scopeMessages.projectCreateDescription,
|
||||
},
|
||||
{
|
||||
id: "PROJECT_READ",
|
||||
id: 'PROJECT_READ',
|
||||
value: BigInt(1) << BigInt(11),
|
||||
label: scopeMessages.projectReadLabel,
|
||||
desc: scopeMessages.projectReadDescription,
|
||||
},
|
||||
{
|
||||
id: "PROJECT_WRITE",
|
||||
id: 'PROJECT_WRITE',
|
||||
value: BigInt(1) << BigInt(12),
|
||||
label: scopeMessages.projectWriteLabel,
|
||||
desc: scopeMessages.projectWriteDescription,
|
||||
},
|
||||
{
|
||||
id: "PROJECT_DELETE",
|
||||
id: 'PROJECT_DELETE',
|
||||
value: BigInt(1) << BigInt(13),
|
||||
label: scopeMessages.projectDeleteLabel,
|
||||
desc: scopeMessages.projectDeleteDescription,
|
||||
},
|
||||
{
|
||||
id: "VERSION_CREATE",
|
||||
id: 'VERSION_CREATE',
|
||||
value: BigInt(1) << BigInt(14),
|
||||
label: scopeMessages.versionCreateLabel,
|
||||
desc: scopeMessages.versionCreateDescription,
|
||||
},
|
||||
{
|
||||
id: "VERSION_READ",
|
||||
id: 'VERSION_READ',
|
||||
value: BigInt(1) << BigInt(15),
|
||||
label: scopeMessages.versionReadLabel,
|
||||
desc: scopeMessages.versionReadDescription,
|
||||
},
|
||||
{
|
||||
id: "VERSION_WRITE",
|
||||
id: 'VERSION_WRITE',
|
||||
value: BigInt(1) << BigInt(16),
|
||||
label: scopeMessages.versionWriteLabel,
|
||||
desc: scopeMessages.versionWriteDescription,
|
||||
},
|
||||
{
|
||||
id: "VERSION_DELETE",
|
||||
id: 'VERSION_DELETE',
|
||||
value: BigInt(1) << BigInt(17),
|
||||
label: scopeMessages.versionDeleteLabel,
|
||||
desc: scopeMessages.versionDeleteDescription,
|
||||
},
|
||||
{
|
||||
id: "REPORT_CREATE",
|
||||
id: 'REPORT_CREATE',
|
||||
value: BigInt(1) << BigInt(18),
|
||||
label: scopeMessages.reportCreateLabel,
|
||||
desc: scopeMessages.reportCreateDescription,
|
||||
},
|
||||
{
|
||||
id: "REPORT_READ",
|
||||
id: 'REPORT_READ',
|
||||
value: BigInt(1) << BigInt(19),
|
||||
label: scopeMessages.reportReadLabel,
|
||||
desc: scopeMessages.reportReadDescription,
|
||||
},
|
||||
{
|
||||
id: "REPORT_WRITE",
|
||||
id: 'REPORT_WRITE',
|
||||
value: BigInt(1) << BigInt(20),
|
||||
label: scopeMessages.reportWriteLabel,
|
||||
desc: scopeMessages.reportWriteDescription,
|
||||
},
|
||||
{
|
||||
id: "REPORT_DELETE",
|
||||
id: 'REPORT_DELETE',
|
||||
value: BigInt(1) << BigInt(21),
|
||||
label: scopeMessages.reportDeleteLabel,
|
||||
desc: scopeMessages.reportDeleteDescription,
|
||||
},
|
||||
{
|
||||
id: "THREAD_READ",
|
||||
id: 'THREAD_READ',
|
||||
value: BigInt(1) << BigInt(22),
|
||||
label: scopeMessages.threadReadLabel,
|
||||
desc: scopeMessages.threadReadDescription,
|
||||
},
|
||||
{
|
||||
id: "THREAD_WRITE",
|
||||
id: 'THREAD_WRITE',
|
||||
value: BigInt(1) << BigInt(23),
|
||||
label: scopeMessages.threadWriteLabel,
|
||||
desc: scopeMessages.threadWriteDescription,
|
||||
},
|
||||
{
|
||||
id: "PAT_CREATE",
|
||||
id: 'PAT_CREATE',
|
||||
value: BigInt(1) << BigInt(24),
|
||||
label: scopeMessages.patCreateLabel,
|
||||
desc: scopeMessages.patCreateDescription,
|
||||
},
|
||||
{
|
||||
id: "PAT_READ",
|
||||
id: 'PAT_READ',
|
||||
value: BigInt(1) << BigInt(25),
|
||||
label: scopeMessages.patReadLabel,
|
||||
desc: scopeMessages.patReadDescription,
|
||||
},
|
||||
{
|
||||
id: "PAT_WRITE",
|
||||
id: 'PAT_WRITE',
|
||||
value: BigInt(1) << BigInt(26),
|
||||
label: scopeMessages.patWriteLabel,
|
||||
desc: scopeMessages.patWriteDescription,
|
||||
},
|
||||
{
|
||||
id: "PAT_DELETE",
|
||||
id: 'PAT_DELETE',
|
||||
value: BigInt(1) << BigInt(27),
|
||||
label: scopeMessages.patDeleteLabel,
|
||||
desc: scopeMessages.patDeleteDescription,
|
||||
},
|
||||
{
|
||||
id: "SESSION_READ",
|
||||
id: 'SESSION_READ',
|
||||
value: BigInt(1) << BigInt(28),
|
||||
label: scopeMessages.sessionReadLabel,
|
||||
desc: scopeMessages.sessionReadDescription,
|
||||
},
|
||||
{
|
||||
id: "SESSION_DELETE",
|
||||
id: 'SESSION_DELETE',
|
||||
value: BigInt(1) << BigInt(29),
|
||||
label: scopeMessages.sessionDeleteLabel,
|
||||
desc: scopeMessages.sessionDeleteDescription,
|
||||
},
|
||||
{
|
||||
id: "PERFORM_ANALYTICS",
|
||||
id: 'PERFORM_ANALYTICS',
|
||||
value: BigInt(1) << BigInt(30),
|
||||
label: scopeMessages.performAnalyticsLabel,
|
||||
desc: scopeMessages.performAnalyticsDescription,
|
||||
},
|
||||
{
|
||||
id: "COLLECTION_CREATE",
|
||||
id: 'COLLECTION_CREATE',
|
||||
value: BigInt(1) << BigInt(31),
|
||||
label: scopeMessages.collectionCreateLabel,
|
||||
desc: scopeMessages.collectionCreateDescription,
|
||||
},
|
||||
{
|
||||
id: "COLLECTION_READ",
|
||||
id: 'COLLECTION_READ',
|
||||
value: BigInt(1) << BigInt(32),
|
||||
label: scopeMessages.collectionReadLabel,
|
||||
desc: scopeMessages.collectionReadDescription,
|
||||
},
|
||||
{
|
||||
id: "COLLECTION_WRITE",
|
||||
id: 'COLLECTION_WRITE',
|
||||
value: BigInt(1) << BigInt(33),
|
||||
label: scopeMessages.collectionWriteLabel,
|
||||
desc: scopeMessages.collectionWriteDescription,
|
||||
},
|
||||
{
|
||||
id: "COLLECTION_DELETE",
|
||||
id: 'COLLECTION_DELETE',
|
||||
value: BigInt(1) << BigInt(34),
|
||||
label: scopeMessages.collectionDeleteLabel,
|
||||
desc: scopeMessages.collectionDeleteDescription,
|
||||
},
|
||||
{
|
||||
id: "ORGANIZATION_CREATE",
|
||||
id: 'ORGANIZATION_CREATE',
|
||||
value: BigInt(1) << BigInt(35),
|
||||
label: scopeMessages.organizationCreateLabel,
|
||||
desc: scopeMessages.organizationCreateDescription,
|
||||
},
|
||||
{
|
||||
id: "ORGANIZATION_READ",
|
||||
id: 'ORGANIZATION_READ',
|
||||
value: BigInt(1) << BigInt(36),
|
||||
label: scopeMessages.organizationReadLabel,
|
||||
desc: scopeMessages.organizationReadDescription,
|
||||
},
|
||||
{
|
||||
id: "ORGANIZATION_WRITE",
|
||||
id: 'ORGANIZATION_WRITE',
|
||||
value: BigInt(1) << BigInt(37),
|
||||
label: scopeMessages.organizationWriteLabel,
|
||||
desc: scopeMessages.organizationWriteDescription,
|
||||
},
|
||||
{
|
||||
id: "ORGANIZATION_DELETE",
|
||||
id: 'ORGANIZATION_DELETE',
|
||||
value: BigInt(1) << BigInt(38),
|
||||
label: scopeMessages.organizationDeleteLabel,
|
||||
desc: scopeMessages.organizationDeleteDescription,
|
||||
},
|
||||
{
|
||||
id: "SESSION_ACCESS",
|
||||
id: 'SESSION_ACCESS',
|
||||
value: BigInt(1) << BigInt(39),
|
||||
label: scopeMessages.sessionAccessLabel,
|
||||
desc: scopeMessages.sessionAccessDescription,
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const Scopes = scopeDefinitions.reduce(
|
||||
(acc, scope) => {
|
||||
acc[scope.id] = scope.value;
|
||||
return acc;
|
||||
acc[scope.id] = scope.value
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, bigint>,
|
||||
);
|
||||
)
|
||||
|
||||
export const restrictedScopes = [
|
||||
Scopes.PAT_READ,
|
||||
@ -583,18 +583,18 @@ export const restrictedScopes = [
|
||||
Scopes.USER_AUTH_WRITE,
|
||||
Scopes.USER_DELETE,
|
||||
Scopes.PERFORM_ANALYTICS,
|
||||
];
|
||||
]
|
||||
|
||||
export const scopeList = Object.entries(Scopes)
|
||||
.filter(([_, value]) => !restrictedScopes.includes(value))
|
||||
.map(([key, _]) => key);
|
||||
.map(([key, _]) => key)
|
||||
|
||||
export const getScopeValue = (scope: string) => {
|
||||
return Scopes[scope];
|
||||
};
|
||||
return Scopes[scope]
|
||||
}
|
||||
|
||||
export const encodeScopes = (scopes: string[]) => {
|
||||
let scopeFlag = BigInt(0);
|
||||
let scopeFlag = BigInt(0)
|
||||
|
||||
// We iterate over the provided scopes
|
||||
for (const scope of scopes) {
|
||||
@ -602,77 +602,77 @@ export const encodeScopes = (scopes: string[]) => {
|
||||
for (const [scopeName, scopeFlagValue] of Object.entries(Scopes)) {
|
||||
// If the scope name is the same as the provided scope, add the scope flag to the scopeFlag variable
|
||||
if (scopeName === scope) {
|
||||
scopeFlag = scopeFlag | scopeFlagValue;
|
||||
scopeFlag = scopeFlag | scopeFlagValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return scopeFlag;
|
||||
};
|
||||
return scopeFlag
|
||||
}
|
||||
|
||||
export const decodeScopes = (scopes: bigint | number) => {
|
||||
if (typeof scopes === "number") {
|
||||
scopes = BigInt(scopes);
|
||||
if (typeof scopes === 'number') {
|
||||
scopes = BigInt(scopes)
|
||||
}
|
||||
|
||||
const authorizedScopes = [];
|
||||
const authorizedScopes = []
|
||||
|
||||
// We iterate over the entries of the Scopes object
|
||||
for (const [scopeName, scopeFlag] of Object.entries(Scopes)) {
|
||||
// If the scope flag is present in the provided number, add the scope name to the list
|
||||
if ((scopes & scopeFlag) === scopeFlag) {
|
||||
authorizedScopes.push(scopeName);
|
||||
authorizedScopes.push(scopeName)
|
||||
}
|
||||
}
|
||||
|
||||
return authorizedScopes;
|
||||
};
|
||||
return authorizedScopes
|
||||
}
|
||||
|
||||
export const hasScope = (scopes: bigint, scope: string) => {
|
||||
const authorizedScopes = decodeScopes(scopes);
|
||||
return authorizedScopes.includes(scope);
|
||||
};
|
||||
const authorizedScopes = decodeScopes(scopes)
|
||||
return authorizedScopes.includes(scope)
|
||||
}
|
||||
|
||||
export const toggleScope = (scopes: bigint, scope: string) => {
|
||||
const authorizedScopes = decodeScopes(scopes);
|
||||
const authorizedScopes = decodeScopes(scopes)
|
||||
if (authorizedScopes.includes(scope)) {
|
||||
return encodeScopes(authorizedScopes.filter((authorizedScope) => authorizedScope !== scope));
|
||||
return encodeScopes(authorizedScopes.filter((authorizedScope) => authorizedScope !== scope))
|
||||
} else {
|
||||
return encodeScopes([...authorizedScopes, scope]);
|
||||
return encodeScopes([...authorizedScopes, scope])
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const useScopes = () => {
|
||||
const { formatMessage } = useVIntl();
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const scopesToDefinitions = (scopes: bigint) => {
|
||||
const authorizedScopes = decodeScopes(scopes);
|
||||
const authorizedScopes = decodeScopes(scopes)
|
||||
return authorizedScopes.map((scope) => {
|
||||
const scopeDefinition = scopeDefinitions.find(
|
||||
(scopeDefinition) => scopeDefinition.id === scope,
|
||||
);
|
||||
)
|
||||
if (!scopeDefinition) {
|
||||
throw new Error(`Scope ${scope} not found`);
|
||||
throw new Error(`Scope ${scope} not found`)
|
||||
}
|
||||
return formatMessage(scopeDefinition.desc)
|
||||
})
|
||||
}
|
||||
return formatMessage(scopeDefinition.desc);
|
||||
});
|
||||
};
|
||||
|
||||
const scopesToLabels = (scopes: bigint) => {
|
||||
const authorizedScopes = decodeScopes(scopes);
|
||||
const authorizedScopes = decodeScopes(scopes)
|
||||
return authorizedScopes.map((scope) => {
|
||||
const scopeDefinition = scopeDefinitions.find(
|
||||
(scopeDefinition) => scopeDefinition.id === scope,
|
||||
);
|
||||
)
|
||||
if (!scopeDefinition) {
|
||||
throw new Error(`Scope ${scope} not found`);
|
||||
throw new Error(`Scope ${scope} not found`)
|
||||
}
|
||||
return formatMessage(scopeDefinition.label)
|
||||
})
|
||||
}
|
||||
return formatMessage(scopeDefinition.label);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
scopesToDefinitions,
|
||||
scopesToLabels,
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,26 +1,26 @@
|
||||
const formatters = new WeakMap<object, Intl.NumberFormat>();
|
||||
const formatters = new WeakMap<object, Intl.NumberFormat>()
|
||||
|
||||
export function useCompactNumber(truncate = false, fractionDigits = 2, locale?: string) {
|
||||
const context = {};
|
||||
const context = {}
|
||||
|
||||
let formatter = formatters.get(context);
|
||||
let formatter = formatters.get(context)
|
||||
|
||||
if (!formatter) {
|
||||
formatter = new Intl.NumberFormat(locale, {
|
||||
notation: "compact",
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: fractionDigits,
|
||||
});
|
||||
formatters.set(context, formatter);
|
||||
})
|
||||
formatters.set(context, formatter)
|
||||
}
|
||||
|
||||
function format(value: number): string {
|
||||
let formattedValue = value;
|
||||
let formattedValue = value
|
||||
if (truncate) {
|
||||
const scale = Math.pow(10, fractionDigits);
|
||||
formattedValue = Math.floor(value * scale) / scale;
|
||||
const scale = Math.pow(10, fractionDigits)
|
||||
formattedValue = Math.floor(value * scale) / scale
|
||||
}
|
||||
return formatter!.format(formattedValue);
|
||||
return formatter!.format(formattedValue)
|
||||
}
|
||||
|
||||
return format;
|
||||
return format
|
||||
}
|
||||
|
||||
@ -1,37 +1,37 @@
|
||||
import { useRequestHeaders,useState } from "#imports";
|
||||
import { useRequestHeaders, useState } from '#imports'
|
||||
|
||||
export const useUserCountry = () => {
|
||||
const country = useState<string>("userCountry", () => "US");
|
||||
const fromServer = useState<boolean>("userCountryFromServer", () => false);
|
||||
const country = useState<string>('userCountry', () => 'US')
|
||||
const fromServer = useState<boolean>('userCountryFromServer', () => false)
|
||||
|
||||
if (import.meta.server) {
|
||||
const headers = useRequestHeaders(["cf-ipcountry", "accept-language"]);
|
||||
const cf = headers["cf-ipcountry"];
|
||||
const headers = useRequestHeaders(['cf-ipcountry', 'accept-language'])
|
||||
const cf = headers['cf-ipcountry']
|
||||
if (cf) {
|
||||
country.value = cf.toUpperCase();
|
||||
fromServer.value = true;
|
||||
country.value = cf.toUpperCase()
|
||||
fromServer.value = true
|
||||
} else {
|
||||
const al = headers["accept-language"] || "";
|
||||
const tag = al.split(",")[0];
|
||||
const val = tag.split("-")[1]?.toLowerCase();
|
||||
const al = headers['accept-language'] || ''
|
||||
const tag = al.split(',')[0]
|
||||
const val = tag.split('-')[1]?.toLowerCase()
|
||||
if (val) {
|
||||
country.value = val;
|
||||
fromServer.value = true;
|
||||
country.value = val
|
||||
fromServer.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.client) {
|
||||
onMounted(() => {
|
||||
if (fromServer.value) return;
|
||||
if (fromServer.value) return
|
||||
// @ts-expect-error - ignore TS not knowing about navigator.userLanguage
|
||||
const lang = navigator.language || navigator.userLanguage || "";
|
||||
const region = lang.split("-")[1];
|
||||
const lang = navigator.language || navigator.userLanguage || ''
|
||||
const region = lang.split('-')[1]
|
||||
if (region) {
|
||||
country.value = region.toUpperCase();
|
||||
country.value = region.toUpperCase()
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
return country;
|
||||
};
|
||||
return country
|
||||
}
|
||||
|
||||
@ -1,81 +1,79 @@
|
||||
const safeTags = new Map<string, string>();
|
||||
const safeTags = new Map<string, string>()
|
||||
|
||||
function safeTagFor(locale: string) {
|
||||
let safeTag = safeTags.get(locale);
|
||||
let safeTag = safeTags.get(locale)
|
||||
if (safeTag == null) {
|
||||
safeTag = new Intl.Locale(locale).baseName;
|
||||
safeTags.set(locale, safeTag);
|
||||
safeTag = new Intl.Locale(locale).baseName
|
||||
safeTags.set(locale, safeTag)
|
||||
}
|
||||
return safeTag;
|
||||
return safeTag
|
||||
}
|
||||
|
||||
type DisplayNamesWrapper = Intl.DisplayNames & {
|
||||
of(tag: string): string | undefined;
|
||||
};
|
||||
of(tag: string): string | undefined
|
||||
}
|
||||
|
||||
const displayNamesDicts = new Map<string, DisplayNamesWrapper>();
|
||||
const displayNamesDicts = new Map<string, DisplayNamesWrapper>()
|
||||
|
||||
function getWrapperKey(locale: string, options: Intl.DisplayNamesOptions) {
|
||||
return JSON.stringify({ ...options, locale });
|
||||
return JSON.stringify({ ...options, locale })
|
||||
}
|
||||
|
||||
export function createDisplayNames(
|
||||
locale: string,
|
||||
options: Intl.DisplayNamesOptions = { type: "language" },
|
||||
options: Intl.DisplayNamesOptions = { type: 'language' },
|
||||
) {
|
||||
const wrapperKey = getWrapperKey(locale, options);
|
||||
let wrapper = displayNamesDicts.get(wrapperKey);
|
||||
const wrapperKey = getWrapperKey(locale, options)
|
||||
let wrapper = displayNamesDicts.get(wrapperKey)
|
||||
|
||||
if (wrapper == null) {
|
||||
const dict = new Intl.DisplayNames(locale, options);
|
||||
const dict = new Intl.DisplayNames(locale, options)
|
||||
|
||||
const badTags: string[] = [];
|
||||
const badTags: string[] = []
|
||||
|
||||
wrapper = {
|
||||
resolvedOptions() {
|
||||
return dict.resolvedOptions();
|
||||
return dict.resolvedOptions()
|
||||
},
|
||||
of(tag: string) {
|
||||
let attempt = 0;
|
||||
|
||||
let attempt = 0
|
||||
|
||||
lookupLoop: do {
|
||||
let lookup: string;
|
||||
let lookup: string
|
||||
switch (attempt) {
|
||||
case 0:
|
||||
lookup = tag;
|
||||
break;
|
||||
lookup = tag
|
||||
break
|
||||
case 1:
|
||||
lookup = safeTagFor(tag);
|
||||
break;
|
||||
lookup = safeTagFor(tag)
|
||||
break
|
||||
default:
|
||||
|
||||
break lookupLoop;
|
||||
break lookupLoop
|
||||
}
|
||||
|
||||
if (badTags.includes(lookup)) continue;
|
||||
if (badTags.includes(lookup)) continue
|
||||
|
||||
try {
|
||||
return dict.of(lookup);
|
||||
return dict.of(lookup)
|
||||
} catch {
|
||||
console.warn(
|
||||
`Failed to get display name for ${lookup} using dictionary for ${
|
||||
this.resolvedOptions().locale
|
||||
}`,
|
||||
);
|
||||
badTags.push(lookup);
|
||||
continue;
|
||||
)
|
||||
badTags.push(lookup)
|
||||
continue
|
||||
}
|
||||
} while (++attempt < 5);
|
||||
} while (++attempt < 5)
|
||||
|
||||
return undefined;
|
||||
return undefined
|
||||
},
|
||||
};
|
||||
|
||||
displayNamesDicts.set(wrapperKey, wrapper);
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
displayNamesDicts.set(wrapperKey, wrapper)
|
||||
}
|
||||
|
||||
return wrapper
|
||||
}
|
||||
|
||||
export function useDisplayNames(
|
||||
@ -85,8 +83,8 @@ export function useDisplayNames(
|
||||
| (() => Intl.DisplayNamesOptions | undefined)
|
||||
| Ref<Intl.DisplayNamesOptions | undefined>,
|
||||
) {
|
||||
const $locale = toRef(locale);
|
||||
const $options = toRef(options);
|
||||
const $locale = toRef(locale)
|
||||
const $options = toRef(options)
|
||||
|
||||
return computed(() => createDisplayNames($locale.value, $options.value));
|
||||
return computed(() => createDisplayNames($locale.value, $options.value))
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user