chore: fix frontend lint issues
This commit is contained in:
parent
f7fc208b15
commit
34e65ace1e
@ -1,6 +1,6 @@
|
|||||||
project_id: 518556
|
project_id: 518556
|
||||||
preserve_hierarchy: true
|
preserve_hierarchy: true
|
||||||
commit_message: "[ci skip]"
|
commit_message: '[ci skip]'
|
||||||
|
|
||||||
files:
|
files:
|
||||||
- source: /locales/en-US/*
|
- source: /locales/en-US/*
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import config from '@modrinth/tooling-config/eslint/nuxt.mjs'
|
import config from '@modrinth/tooling-config/eslint/nuxt.mjs'
|
||||||
export default config.append([{
|
export default config.append([
|
||||||
|
{
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
"import/no-unresolved": "off",
|
'import/no-unresolved': 'off',
|
||||||
'no-undef': 'off'
|
'no-undef': 'off',
|
||||||
}
|
},
|
||||||
}])
|
},
|
||||||
|
])
|
||||||
|
|||||||
@ -6,6 +6,6 @@
|
|||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ModrinthLoadingIndicator from "~/components/ui/modrinth-loading-indicator.ts";
|
import ModrinthLoadingIndicator from '~/components/ui/modrinth-loading-indicator.ts'
|
||||||
import Notifications from "~/components/ui/Notifications.vue";
|
import Notifications from '~/components/ui/Notifications.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -415,7 +415,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:disabled,
|
&:disabled,
|
||||||
&[disabled="true"] {
|
&[disabled='true'] {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
filter: grayscale(50%);
|
filter: grayscale(50%);
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
@ -461,7 +461,7 @@ tr.button-transparent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:disabled > *,
|
&:disabled > *,
|
||||||
&[disabled="true"] > * {
|
&[disabled='true'] > * {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
filter: grayscale(50%);
|
filter: grayscale(50%);
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
@ -492,7 +492,7 @@ tr.button-transparent {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|
||||||
&disabled,
|
&disabled,
|
||||||
&[disabled="true"] {
|
&[disabled='true'] {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
@ -678,7 +678,7 @@ tr.button-transparent {
|
|||||||
background: var(--color-button-bg);
|
background: var(--color-button-bg);
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
content: "";
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 7px;
|
top: 7px;
|
||||||
left: 7px;
|
left: 7px;
|
||||||
|
|||||||
@ -125,8 +125,8 @@ html {
|
|||||||
|
|
||||||
--shadow-card: rgba(50, 50, 100, 0.1) 0px 2px 4px 0px;
|
--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-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-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-maze-outer-bg: linear-gradient(180deg, #f0f0f0 0%, #ffffff 100%);
|
||||||
|
|
||||||
--landing-color-heading: #000;
|
--landing-color-heading: #000;
|
||||||
@ -259,10 +259,10 @@ html {
|
|||||||
|
|
||||||
--shadow-card: rgba(0, 0, 0, 0.25) 0px 2px 4px 0px;
|
--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:
|
--landing-maze-gradient-bg:
|
||||||
linear-gradient(0deg, #31375f 0%, rgba(8, 14, 55, 0) 100%),
|
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-maze-outer-bg: linear-gradient(180deg, #06060d 0%, #000000 100%);
|
||||||
|
|
||||||
--landing-color-heading: #fff;
|
--landing-color-heading: #fff;
|
||||||
@ -492,7 +492,7 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:disabled,
|
&:disabled,
|
||||||
&[disabled="true"] {
|
&[disabled='true'] {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
@ -509,7 +509,7 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
input[type="button"] {
|
input[type='button'] {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: none;
|
border: none;
|
||||||
outline: 2px solid transparent;
|
outline: 2px solid transparent;
|
||||||
@ -525,13 +525,13 @@ kbd {
|
|||||||
font-size: 0.85em !important;
|
font-size: 0.85em !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@import "~/assets/styles/layout.scss";
|
@import '~/assets/styles/layout.scss';
|
||||||
@import "~/assets/styles/utils.scss";
|
@import '~/assets/styles/utils.scss';
|
||||||
@import "~/assets/styles/components.scss";
|
@import '~/assets/styles/components.scss';
|
||||||
|
|
||||||
button:focus-visible,
|
button:focus-visible,
|
||||||
a:focus-visible,
|
a:focus-visible,
|
||||||
[tabindex="0"]:focus-visible {
|
[tabindex='0']:focus-visible {
|
||||||
outline: 0.25rem solid #ea80ff;
|
outline: 0.25rem solid #ea80ff;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,9 +42,9 @@
|
|||||||
padding: 0 1.5rem;
|
padding: 0 1.5rem;
|
||||||
|
|
||||||
grid-template:
|
grid-template:
|
||||||
"sidebar"
|
'sidebar'
|
||||||
"content"
|
'content'
|
||||||
"info"
|
'info'
|
||||||
/ 100%;
|
/ 100%;
|
||||||
|
|
||||||
@media screen and (max-width: 1024px) {
|
@media screen and (max-width: 1024px) {
|
||||||
@ -81,25 +81,25 @@
|
|||||||
column-gap: 0.75rem;
|
column-gap: 0.75rem;
|
||||||
|
|
||||||
grid-template:
|
grid-template:
|
||||||
"sidebar content" auto
|
'sidebar content' auto
|
||||||
"info content" auto
|
'info content' auto
|
||||||
"dummy content" 1fr
|
'dummy content' 1fr
|
||||||
/ 18.75rem 1fr;
|
/ 18.75rem 1fr;
|
||||||
|
|
||||||
&.alt-layout {
|
&.alt-layout {
|
||||||
grid-template:
|
grid-template:
|
||||||
"content sidebar" auto
|
'content sidebar' auto
|
||||||
"content info" auto
|
'content info' auto
|
||||||
"content dummy" 1fr
|
'content dummy' 1fr
|
||||||
/ 1fr 18.75rem;
|
/ 1fr 18.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.no-sidebar {
|
&.no-sidebar {
|
||||||
grid-template:
|
grid-template:
|
||||||
"header header" auto
|
'header header' auto
|
||||||
"content content" auto
|
'content content' auto
|
||||||
"info info" auto
|
'info info' auto
|
||||||
"dummy dummy" 1fr
|
'dummy dummy' 1fr
|
||||||
/ 1fr 1fr;
|
/ 1fr 1fr;
|
||||||
|
|
||||||
.normal-page__content {
|
.normal-page__content {
|
||||||
@ -129,9 +129,9 @@
|
|||||||
padding-bottom: 1.5rem;
|
padding-bottom: 1.5rem;
|
||||||
|
|
||||||
grid-template:
|
grid-template:
|
||||||
"header"
|
'header'
|
||||||
"content"
|
'content'
|
||||||
"sidebar"
|
'sidebar'
|
||||||
/ 100%;
|
/ 100%;
|
||||||
|
|
||||||
.normal-page__ultimate-sidebar {
|
.normal-page__ultimate-sidebar {
|
||||||
@ -152,16 +152,16 @@
|
|||||||
@media screen and (min-width: 1024px) {
|
@media screen and (min-width: 1024px) {
|
||||||
&.sidebar {
|
&.sidebar {
|
||||||
grid-template:
|
grid-template:
|
||||||
"header header" auto
|
'header header' auto
|
||||||
"content sidebar" auto
|
'content sidebar' auto
|
||||||
"content dummy" 1fr
|
'content dummy' 1fr
|
||||||
/ 1fr 18.75rem;
|
/ 1fr 18.75rem;
|
||||||
|
|
||||||
&.alt-layout {
|
&.alt-layout {
|
||||||
grid-template:
|
grid-template:
|
||||||
"header header" auto
|
'header header' auto
|
||||||
"sidebar content" auto
|
'sidebar content' auto
|
||||||
"dummy content" 1fr
|
'dummy content' 1fr
|
||||||
/ 18.75rem 1fr;
|
/ 18.75rem 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -177,9 +177,9 @@
|
|||||||
max-width: calc(80rem + 0.75rem + 600px);
|
max-width: calc(80rem + 0.75rem + 600px);
|
||||||
|
|
||||||
grid-template:
|
grid-template:
|
||||||
"header header ultimate-sidebar" auto
|
'header header ultimate-sidebar' auto
|
||||||
"content sidebar ultimate-sidebar" auto
|
'content sidebar ultimate-sidebar' auto
|
||||||
"content dummy ultimate-sidebar" 1fr
|
'content dummy ultimate-sidebar' 1fr
|
||||||
/ 1fr 18.75rem auto;
|
/ 1fr 18.75rem auto;
|
||||||
|
|
||||||
.normal-page__header {
|
.normal-page__header {
|
||||||
@ -203,9 +203,9 @@
|
|||||||
|
|
||||||
&.alt-layout {
|
&.alt-layout {
|
||||||
grid-template:
|
grid-template:
|
||||||
"ultimate-sidebar header header" auto
|
'ultimate-sidebar header header' auto
|
||||||
"ultimate-sidebar sidebar content" auto
|
'ultimate-sidebar sidebar content' auto
|
||||||
"ultimate-sidebar dummy content" 1fr
|
'ultimate-sidebar dummy content' 1fr
|
||||||
/ auto 18.75rem 1fr;
|
/ auto 18.75rem 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,21 +54,21 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
const loading = useLoading();
|
const loading = useLoading()
|
||||||
|
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig()
|
||||||
|
|
||||||
const api = computed(() => {
|
const api = computed(() => {
|
||||||
const apiUrl = config.public.apiBaseUrl;
|
const apiUrl = config.public.apiBaseUrl
|
||||||
if (apiUrl.startsWith("https://api.modrinth.com")) {
|
if (apiUrl.startsWith('https://api.modrinth.com')) {
|
||||||
return "prod";
|
return 'prod'
|
||||||
} else if (apiUrl.startsWith("https://staging-api.modrinth.com")) {
|
} else if (apiUrl.startsWith('https://staging-api.modrinth.com')) {
|
||||||
return "staging";
|
return 'staging'
|
||||||
} else if (apiUrl.startsWith("localhost") || apiUrl.startsWith("127.0.0.1")) {
|
} else if (apiUrl.startsWith('localhost') || apiUrl.startsWith('127.0.0.1')) {
|
||||||
return "localhost";
|
return 'localhost'
|
||||||
}
|
}
|
||||||
return "foreign";
|
return 'foreign'
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@ -19,43 +19,43 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DropdownIcon } from "@modrinth/assets";
|
import { DropdownIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
import { ButtonStyled } from '@modrinth/ui'
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
openByDefault?: boolean;
|
openByDefault?: boolean
|
||||||
type?: "standard" | "outlined" | "transparent";
|
type?: 'standard' | 'outlined' | 'transparent'
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
type: "standard",
|
type: 'standard',
|
||||||
openByDefault: false,
|
openByDefault: false,
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
const isOpen = ref(props.openByDefault);
|
const isOpen = ref(props.openByDefault)
|
||||||
const emit = defineEmits(["onOpen", "onClose"]);
|
const emit = defineEmits(['onOpen', 'onClose'])
|
||||||
|
|
||||||
const slots = useSlots();
|
const slots = useSlots()
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
isOpen.value = true;
|
isOpen.value = true
|
||||||
emit("onOpen");
|
emit('onOpen')
|
||||||
}
|
}
|
||||||
function close() {
|
function close() {
|
||||||
isOpen.value = false;
|
isOpen.value = false
|
||||||
emit("onClose");
|
emit('onClose')
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
open,
|
open,
|
||||||
close,
|
close,
|
||||||
isOpen,
|
isOpen,
|
||||||
});
|
})
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.accordion-content {
|
.accordion-content {
|
||||||
|
|||||||
@ -31,53 +31,53 @@ useHead({
|
|||||||
// },
|
// },
|
||||||
{
|
{
|
||||||
// Aditude
|
// 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,
|
async: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Optima
|
// Optima
|
||||||
src: "https://bservr.com/o.js?uid=8118d1fdb2e0d6f32180bd27",
|
src: 'https://bservr.com/o.js?uid=8118d1fdb2e0d6f32180bd27',
|
||||||
async: true,
|
async: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: "/inmobi.js",
|
src: '/inmobi.js',
|
||||||
async: true,
|
async: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
link: [
|
link: [
|
||||||
{
|
{
|
||||||
rel: "preload",
|
rel: 'preload',
|
||||||
as: "script",
|
as: 'script',
|
||||||
href: "https://www.googletagservices.com/tag/js/gpt.js",
|
href: 'https://www.googletagservices.com/tag/js/gpt.js',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.tude = window.tude || { cmd: [] };
|
window.tude = window.tude || { cmd: [] }
|
||||||
window.Raven = window.Raven || { cmd: [] };
|
window.Raven = window.Raven || { cmd: [] }
|
||||||
|
|
||||||
window.Raven.cmd.push(({ config }) => {
|
window.Raven.cmd.push(({ config }) => {
|
||||||
config.setCustom({
|
config.setCustom({
|
||||||
param1: "web",
|
param1: 'web',
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
||||||
tude.cmd.push(function () {
|
tude.cmd.push(function () {
|
||||||
tude.refreshAdsViaDivMappings([
|
tude.refreshAdsViaDivMappings([
|
||||||
{
|
{
|
||||||
divId: "modrinth-rail-1",
|
divId: 'modrinth-rail-1',
|
||||||
baseDivId: "pb-slot-square-2",
|
baseDivId: 'pb-slot-square-2',
|
||||||
targeting: {
|
targeting: {
|
||||||
location: "web",
|
location: 'web',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
])
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
iframe[id^="google_ads_iframe"] {
|
iframe[id^='google_ads_iframe'] {
|
||||||
color-scheme: normal;
|
color-scheme: normal;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
@ -96,21 +96,21 @@ iframe[id^="google_ads_iframe"] {
|
|||||||
background: var(--color-raised-bg);
|
background: var(--color-raised-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
#qc-cmp2-ui button[mode="primary"] {
|
#qc-cmp2-ui button[mode='primary'] {
|
||||||
background: var(--color-brand);
|
background: var(--color-brand);
|
||||||
color: var(--color-accent-contrast);
|
color: var(--color-accent-contrast);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#qc-cmp2-ui button[mode="secondary"] {
|
#qc-cmp2-ui button[mode='secondary'] {
|
||||||
background: var(--color-button-bg);
|
background: var(--color-button-bg);
|
||||||
color: var(--color-base);
|
color: var(--color-base);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#qc-cmp2-ui button[mode="link"] {
|
#qc-cmp2-ui button[mode='link'] {
|
||||||
color: var(--color-link);
|
color: var(--color-link);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,7 +129,7 @@ iframe[id^="google_ads_iframe"] {
|
|||||||
font-family: var(--font-standard);
|
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);
|
background-color: var(--color-brand);
|
||||||
border: 1px solid var(--color-brand);
|
border: 1px solid var(--color-brand);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,35 +13,35 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineOptions({
|
defineOptions({
|
||||||
inheritAttrs: false,
|
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() {
|
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(() => {
|
onMounted(() => {
|
||||||
mutationObserver.value = new MutationObserver(updateContent);
|
mutationObserver.value = new MutationObserver(updateContent)
|
||||||
|
|
||||||
mutationObserver.value.observe(slotContainer.value, {
|
mutationObserver.value.observe(slotContainer.value, {
|
||||||
childList: true,
|
childList: true,
|
||||||
});
|
})
|
||||||
|
|
||||||
updateContent();
|
updateContent()
|
||||||
});
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (mutationObserver.value) {
|
if (mutationObserver.value) {
|
||||||
mutationObserver.value.disconnect();
|
mutationObserver.value.disconnect()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.accordion-content {
|
.accordion-content {
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ChevronRightIcon } from "@modrinth/assets";
|
import { ChevronRightIcon } from '@modrinth/assets'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
linkStack: {
|
linkStack: {
|
||||||
@ -26,7 +26,7 @@ defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@ -25,7 +25,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { CheckIcon, DropdownIcon } from "@modrinth/assets";
|
import { CheckIcon, DropdownIcon } from '@modrinth/assets'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -36,7 +36,7 @@ export default {
|
|||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: "",
|
default: '',
|
||||||
},
|
},
|
||||||
disabled: {
|
disabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -60,15 +60,15 @@ export default {
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
emits: ["update:modelValue"],
|
emits: ['update:modelValue'],
|
||||||
methods: {
|
methods: {
|
||||||
toggle() {
|
toggle() {
|
||||||
if (!this.disabled) {
|
if (!this.disabled) {
|
||||||
this.$emit("update:modelValue", !this.modelValue);
|
this.$emit('update:modelValue', !this.modelValue)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { CheckIcon } from "@modrinth/assets";
|
import { CheckIcon } from '@modrinth/assets'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -42,32 +42,32 @@ export default {
|
|||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
emits: ["update:modelValue"],
|
emits: ['update:modelValue'],
|
||||||
computed: {
|
computed: {
|
||||||
selected: {
|
selected: {
|
||||||
get() {
|
get() {
|
||||||
return this.modelValue;
|
return this.modelValue
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
this.$emit("update:modelValue", value);
|
this.$emit('update:modelValue', value)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
if (this.items.length > 0 && this.neverEmpty) {
|
if (this.items.length > 0 && this.neverEmpty) {
|
||||||
this.selected = this.items[0];
|
this.selected = this.items[0]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleItem(item) {
|
toggleItem(item) {
|
||||||
if (this.selected === item && !this.neverEmpty) {
|
if (this.selected === item && !this.neverEmpty) {
|
||||||
this.selected = null;
|
this.selected = null
|
||||||
} else {
|
} else {
|
||||||
this.selected = item;
|
this.selected = item
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@ -28,8 +28,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="m-0 max-w-[30rem]">
|
<p class="m-0 max-w-[30rem]">
|
||||||
Your new collection will be created as a public collection with
|
Your new collection will be created as a public collection with
|
||||||
{{ projectIds.length > 0 ? projectIds.length : "no" }}
|
{{ projectIds.length > 0 ? projectIds.length : 'no' }}
|
||||||
{{ projectIds.length !== 1 ? "projects" : "project" }}.
|
{{ projectIds.length !== 1 ? 'projects' : 'project' }}.
|
||||||
</p>
|
</p>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<ButtonStyled color="brand">
|
<ButtonStyled color="brand">
|
||||||
@ -49,61 +49,61 @@
|
|||||||
</NewModal>
|
</NewModal>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { PlusIcon, XIcon } from "@modrinth/assets";
|
import { PlusIcon, XIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
import { ButtonStyled, NewModal } from '@modrinth/ui'
|
||||||
|
|
||||||
const router = useNativeRouter();
|
const router = useNativeRouter()
|
||||||
|
|
||||||
const name = ref("");
|
const name = ref('')
|
||||||
const description = ref("");
|
const description = ref('')
|
||||||
|
|
||||||
const modal = ref();
|
const modal = ref()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
projectIds: {
|
projectIds: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default() {
|
default() {
|
||||||
return [];
|
return []
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
async function create() {
|
async function create() {
|
||||||
startLoading();
|
startLoading()
|
||||||
try {
|
try {
|
||||||
const result = await useBaseFetch("collection", {
|
const result = await useBaseFetch('collection', {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
name: name.value.trim(),
|
name: name.value.trim(),
|
||||||
description: description.value.trim() || undefined,
|
description: description.value.trim() || undefined,
|
||||||
projects: props.projectIds,
|
projects: props.projectIds,
|
||||||
},
|
},
|
||||||
apiVersion: 3,
|
apiVersion: 3,
|
||||||
});
|
})
|
||||||
|
|
||||||
await initUserCollections();
|
await initUserCollections()
|
||||||
|
|
||||||
modal.value.hide();
|
modal.value.hide()
|
||||||
await router.push(`/collection/${result.id}`);
|
await router.push(`/collection/${result.id}`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "main",
|
group: 'main',
|
||||||
title: "An error occurred",
|
title: 'An error occurred',
|
||||||
text: err?.data?.description || err?.message || err,
|
text: err?.data?.description || err?.message || err,
|
||||||
type: "error",
|
type: 'error',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
stopLoading();
|
stopLoading()
|
||||||
}
|
}
|
||||||
function show(event) {
|
function show(event) {
|
||||||
name.value = "";
|
name.value = ''
|
||||||
description.value = "";
|
description.value = ''
|
||||||
modal.value.show(event);
|
modal.value.show(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show,
|
show,
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@ -17,5 +17,5 @@ defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -4,10 +4,10 @@
|
|||||||
class="drop-area"
|
class="drop-area"
|
||||||
@drop.stop.prevent="
|
@drop.stop.prevent="
|
||||||
(event) => {
|
(event) => {
|
||||||
$refs.drop_area.style.visibility = 'hidden';
|
$refs.drop_area.style.visibility = 'hidden'
|
||||||
|
|
||||||
if (event.dataTransfer && event.dataTransfer.files && fileAllowed) {
|
if (event.dataTransfer && event.dataTransfer.files && fileAllowed) {
|
||||||
$emit('change', event.dataTransfer.files);
|
$emit('change', event.dataTransfer.files)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@ -22,45 +22,45 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
accept: {
|
accept: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
emits: ["change"],
|
emits: ['change'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
fileAllowed: false,
|
fileAllowed: false,
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
document.addEventListener("dragenter", this.allowDrag);
|
document.addEventListener('dragenter', this.allowDrag)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
allowDrag(event) {
|
allowDrag(event) {
|
||||||
const file = event.dataTransfer?.items[0];
|
const file = event.dataTransfer?.items[0]
|
||||||
|
|
||||||
if (
|
if (
|
||||||
file &&
|
file &&
|
||||||
this.accept
|
this.accept
|
||||||
.split(",")
|
.split(',')
|
||||||
.reduce((acc, t) => acc || file.type.startsWith(t) || file.type === t || t === "*", false)
|
.reduce((acc, t) => acc || file.type.startsWith(t) || file.type === t || t === '*', false)
|
||||||
) {
|
) {
|
||||||
this.fileAllowed = true;
|
this.fileAllowed = true
|
||||||
event.dataTransfer.dropEffect = "copy";
|
event.dataTransfer.dropEffect = 'copy'
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
|
|
||||||
if (this.$refs.drop_area) {
|
if (this.$refs.drop_area) {
|
||||||
this.$refs.drop_area.style.visibility = "visible";
|
this.$refs.drop_area.style.visibility = 'visible'
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.fileAllowed = false;
|
this.fileAllowed = false
|
||||||
|
|
||||||
if (this.$refs.drop_area) {
|
if (this.$refs.drop_area) {
|
||||||
this.$refs.drop_area.style.visibility = "hidden";
|
this.$refs.drop_area.style.visibility = 'hidden'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@ -81,7 +81,7 @@ export default {
|
|||||||
&::before {
|
&::before {
|
||||||
--indent: 4rem;
|
--indent: 4rem;
|
||||||
|
|
||||||
content: " ";
|
content: ' ';
|
||||||
position: relative;
|
position: relative;
|
||||||
top: var(--indent);
|
top: var(--indent);
|
||||||
left: var(--indent);
|
left: var(--indent);
|
||||||
|
|||||||
@ -49,22 +49,22 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ClientIcon, GlobeIcon, InfoIcon, ServerIcon } from "@modrinth/assets";
|
import { ClientIcon, GlobeIcon, InfoIcon, ServerIcon } from '@modrinth/assets'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "mod",
|
default: 'mod',
|
||||||
},
|
},
|
||||||
serverSide: {
|
serverSide: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: "",
|
default: '',
|
||||||
},
|
},
|
||||||
clientSide: {
|
clientSide: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: "",
|
default: '',
|
||||||
},
|
},
|
||||||
typeOnly: {
|
typeOnly: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -85,12 +85,12 @@ defineProps({
|
|||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
default() {
|
default() {
|
||||||
return [];
|
return []
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const tags = useTags();
|
const tags = useTags()
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.environment {
|
.environment {
|
||||||
|
|||||||
@ -18,14 +18,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { fileIsValid } from "~/helpers/fileUtils.js";
|
import { fileIsValid } from '~/helpers/fileUtils.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {},
|
components: {},
|
||||||
props: {
|
props: {
|
||||||
prompt: {
|
prompt: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "Select file",
|
default: 'Select file',
|
||||||
},
|
},
|
||||||
multiple: {
|
multiple: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -59,33 +59,33 @@ export default {
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
emits: ["change"],
|
emits: ['change'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
files: [],
|
files: [],
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
addFiles(files, shouldNotReset) {
|
addFiles(files, shouldNotReset) {
|
||||||
if (!shouldNotReset || this.shouldAlwaysReset) {
|
if (!shouldNotReset || this.shouldAlwaysReset) {
|
||||||
this.files = files;
|
this.files = files
|
||||||
}
|
}
|
||||||
|
|
||||||
const validationOptions = { maxSize: this.maxSize, alertOnInvalid: true };
|
const validationOptions = { maxSize: this.maxSize, alertOnInvalid: true }
|
||||||
this.files = [...this.files].filter((file) => fileIsValid(file, validationOptions));
|
this.files = [...this.files].filter((file) => fileIsValid(file, validationOptions))
|
||||||
|
|
||||||
if (this.files.length > 0) {
|
if (this.files.length > 0) {
|
||||||
this.$emit("change", this.files);
|
this.$emit('change', this.files)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleDrop(e) {
|
handleDrop(e) {
|
||||||
this.addFiles(e.dataTransfer.files);
|
this.addFiles(e.dataTransfer.files)
|
||||||
},
|
},
|
||||||
handleChange(e) {
|
handleChange(e) {
|
||||||
this.addFiles(e.target.files);
|
this.addFiles(e.target.files)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@ -1,40 +1,40 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const token = defineModel();
|
const token = defineModel()
|
||||||
const id = ref(null);
|
const id = ref(null)
|
||||||
|
|
||||||
function hCaptchaUpdateToken(newToken) {
|
function hCaptchaUpdateToken(newToken) {
|
||||||
token.value = newToken;
|
token.value = newToken
|
||||||
}
|
}
|
||||||
|
|
||||||
function hCaptchaReady() {
|
function hCaptchaReady() {
|
||||||
window.hCaptchaUpdateToken = hCaptchaUpdateToken;
|
window.hCaptchaUpdateToken = hCaptchaUpdateToken
|
||||||
id.value = window.hcaptcha.render("h-captcha");
|
id.value = window.hcaptcha.render('h-captcha')
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (window.hcaptcha) {
|
if (window.hcaptcha) {
|
||||||
hCaptchaReady();
|
hCaptchaReady()
|
||||||
} else {
|
} else {
|
||||||
window.hCaptchaReady = hCaptchaReady;
|
window.hCaptchaReady = hCaptchaReady
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
script: [
|
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,
|
async: true,
|
||||||
defer: true,
|
defer: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
reset: () => {
|
reset: () => {
|
||||||
token.value = null;
|
token.value = null
|
||||||
window.hcaptcha.reset(id.value);
|
window.hcaptcha.reset(id.value)
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@ -7,12 +7,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
type MessageType = "information" | "warning";
|
type MessageType = 'information' | 'warning'
|
||||||
const props = withDefaults(defineProps<{ messageType?: MessageType }>(), {
|
const props = withDefaults(defineProps<{ messageType?: MessageType }>(), {
|
||||||
messageType: "information",
|
messageType: 'information',
|
||||||
});
|
})
|
||||||
const cardClassByType = computed(() => `message-banner__content_${props.messageType}`);
|
const cardClassByType = computed(() => `message-banner__content_${props.messageType}`)
|
||||||
const ariaLabelByType = computed(() => `Banner with ${props.messageType} message`);
|
const ariaLabelByType = computed(() => `Banner with ${props.messageType} message`)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="css" scoped>
|
<style lang="css" scoped>
|
||||||
|
|||||||
@ -26,7 +26,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { XIcon } from "@modrinth/assets";
|
import { XIcon } from '@modrinth/assets'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -39,31 +39,31 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const cosmetics = useCosmetics();
|
const cosmetics = useCosmetics()
|
||||||
|
|
||||||
return { cosmetics };
|
return { cosmetics }
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
shown: false,
|
shown: false,
|
||||||
actuallyShown: false,
|
actuallyShown: false,
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
show() {
|
show() {
|
||||||
this.shown = true;
|
this.shown = true
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.actuallyShown = true;
|
this.actuallyShown = true
|
||||||
}, 50);
|
}, 50)
|
||||||
},
|
},
|
||||||
hide() {
|
hide() {
|
||||||
this.actuallyShown = false;
|
this.actuallyShown = false
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.shown = false;
|
this.shown = false
|
||||||
}, 300);
|
}, 300)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@ -84,11 +84,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { PlusIcon,XIcon } from "@modrinth/assets";
|
import { PlusIcon, XIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled, DropdownSelect,NewModal } from "@modrinth/ui";
|
import { ButtonStyled, DropdownSelect, NewModal } from '@modrinth/ui'
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
const app = useNuxtApp();
|
const app = useNuxtApp()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
organizationId: {
|
organizationId: {
|
||||||
@ -96,120 +96,120 @@ const props = defineProps({
|
|||||||
required: false,
|
required: false,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const modal = ref();
|
const modal = ref()
|
||||||
|
|
||||||
const name = ref("");
|
const name = ref('')
|
||||||
const slug = ref("");
|
const slug = ref('')
|
||||||
const description = ref("");
|
const description = ref('')
|
||||||
const manualSlug = ref(false);
|
const manualSlug = ref(false)
|
||||||
const visibilities = ref([
|
const visibilities = ref([
|
||||||
{
|
{
|
||||||
actual: "approved",
|
actual: 'approved',
|
||||||
display: "Public",
|
display: 'Public',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
actual: "unlisted",
|
actual: 'unlisted',
|
||||||
display: "Unlisted",
|
display: 'Unlisted',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
actual: "private",
|
actual: 'private',
|
||||||
display: "Private",
|
display: 'Private',
|
||||||
},
|
},
|
||||||
]);
|
])
|
||||||
const visibility = ref({
|
const visibility = ref({
|
||||||
actual: "approved",
|
actual: 'approved',
|
||||||
display: "Public",
|
display: 'Public',
|
||||||
});
|
})
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
modal.value.hide();
|
modal.value.hide()
|
||||||
};
|
}
|
||||||
|
|
||||||
async function createProject() {
|
async function createProject() {
|
||||||
startLoading();
|
startLoading()
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData()
|
||||||
|
|
||||||
const auth = await useAuth();
|
const auth = await useAuth()
|
||||||
|
|
||||||
const projectData = {
|
const projectData = {
|
||||||
title: name.value.trim(),
|
title: name.value.trim(),
|
||||||
project_type: "mod",
|
project_type: 'mod',
|
||||||
slug: slug.value,
|
slug: slug.value,
|
||||||
description: description.value.trim(),
|
description: description.value.trim(),
|
||||||
body: "",
|
body: '',
|
||||||
requested_status: visibility.value.actual,
|
requested_status: visibility.value.actual,
|
||||||
initial_versions: [],
|
initial_versions: [],
|
||||||
team_members: [
|
team_members: [
|
||||||
{
|
{
|
||||||
user_id: auth.value.user.id,
|
user_id: auth.value.user.id,
|
||||||
name: auth.value.user.username,
|
name: auth.value.user.username,
|
||||||
role: "Owner",
|
role: 'Owner',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
categories: [],
|
categories: [],
|
||||||
client_side: "required",
|
client_side: 'required',
|
||||||
server_side: "required",
|
server_side: 'required',
|
||||||
license_id: "LicenseRef-Unknown",
|
license_id: 'LicenseRef-Unknown',
|
||||||
is_draft: true,
|
is_draft: true,
|
||||||
};
|
}
|
||||||
|
|
||||||
if (props.organizationId) {
|
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 {
|
try {
|
||||||
await useBaseFetch("project", {
|
await useBaseFetch('project', {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Disposition": formData,
|
'Content-Disposition': formData,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
modal.value.hide();
|
modal.value.hide()
|
||||||
await router.push({
|
await router.push({
|
||||||
name: "type-id",
|
name: 'type-id',
|
||||||
params: {
|
params: {
|
||||||
type: "project",
|
type: 'project',
|
||||||
id: slug.value,
|
id: slug.value,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
app.$notify({
|
app.$notify({
|
||||||
group: "main",
|
group: 'main',
|
||||||
title: "An error occurred",
|
title: 'An error occurred',
|
||||||
text: err.data ? err.data.description : err,
|
text: err.data ? err.data.description : err,
|
||||||
type: "error",
|
type: 'error',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
stopLoading();
|
stopLoading()
|
||||||
}
|
}
|
||||||
|
|
||||||
function show(event) {
|
function show(event) {
|
||||||
name.value = "";
|
name.value = ''
|
||||||
slug.value = "";
|
slug.value = ''
|
||||||
description.value = "";
|
description.value = ''
|
||||||
manualSlug.value = false;
|
manualSlug.value = false
|
||||||
modal.value.show(event);
|
modal.value.show(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show,
|
show,
|
||||||
});
|
})
|
||||||
|
|
||||||
function updatedName() {
|
function updatedName() {
|
||||||
if (!manualSlug.value) {
|
if (!manualSlug.value) {
|
||||||
slug.value = name.value
|
slug.value = name.value
|
||||||
.trim()
|
.trim()
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replaceAll(" ", "-")
|
.replaceAll(' ', '-')
|
||||||
.replaceAll(/[^a-zA-Z0-9!@$()`.+,_"-]/g, "")
|
.replaceAll(/[^a-zA-Z0-9!@$()`.+,_"-]/g, '')
|
||||||
.replaceAll(/--+/gm, "-");
|
.replaceAll(/--+/gm, '-')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -24,7 +24,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
const route = useNativeRoute();
|
const route = useNativeRoute()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
links: {
|
links: {
|
||||||
@ -35,59 +35,59 @@ const props = defineProps({
|
|||||||
default: null,
|
default: null,
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const sliderPositionX = ref(0);
|
const sliderPositionX = ref(0)
|
||||||
const sliderPositionY = ref(18);
|
const sliderPositionY = ref(18)
|
||||||
const selectedElementWidth = ref(0);
|
const selectedElementWidth = ref(0)
|
||||||
const activeIndex = ref(-1);
|
const activeIndex = ref(-1)
|
||||||
const oldIndex = ref(-1);
|
const oldIndex = ref(-1)
|
||||||
|
|
||||||
const filteredLinks = computed(() =>
|
const filteredLinks = computed(() =>
|
||||||
props.links.filter((x) => (x.shown === undefined ? true : x.shown)),
|
props.links.filter((x) => (x.shown === undefined ? true : x.shown)),
|
||||||
);
|
)
|
||||||
const positionToMoveX = computed(() => `${sliderPositionX.value}px`);
|
const positionToMoveX = computed(() => `${sliderPositionX.value}px`)
|
||||||
const positionToMoveY = computed(() => `${sliderPositionY.value}px`);
|
const positionToMoveY = computed(() => `${sliderPositionY.value}px`)
|
||||||
const sliderWidth = computed(() => `${selectedElementWidth.value}px`);
|
const sliderWidth = computed(() => `${selectedElementWidth.value}px`)
|
||||||
|
|
||||||
function pickLink() {
|
function pickLink() {
|
||||||
activeIndex.value = props.query
|
activeIndex.value = props.query
|
||||||
? filteredLinks.value.findIndex(
|
? 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) {
|
if (activeIndex.value !== -1) {
|
||||||
startAnimation();
|
startAnimation()
|
||||||
} else {
|
} else {
|
||||||
oldIndex.value = -1;
|
oldIndex.value = -1
|
||||||
sliderPositionX.value = 0;
|
sliderPositionX.value = 0
|
||||||
selectedElementWidth.value = 0;
|
selectedElementWidth.value = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rowLinkElements = ref();
|
const rowLinkElements = ref()
|
||||||
|
|
||||||
function startAnimation() {
|
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;
|
sliderPositionX.value = el.offsetLeft
|
||||||
sliderPositionY.value = el.offsetTop + el.offsetHeight;
|
sliderPositionY.value = el.offsetTop + el.offsetHeight
|
||||||
selectedElementWidth.value = el.offsetWidth;
|
selectedElementWidth.value = el.offsetWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener("resize", pickLink);
|
window.addEventListener('resize', pickLink)
|
||||||
pickLink();
|
pickLink()
|
||||||
});
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener("resize", pickLink);
|
window.removeEventListener('resize', pickLink)
|
||||||
});
|
})
|
||||||
|
|
||||||
watch(route, () => pickLink());
|
watch(route, () => pickLink())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {};
|
export default {}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@ -23,7 +23,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ChevronRightIcon } from "@modrinth/assets";
|
import { ChevronRightIcon } from '@modrinth/assets'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -55,7 +55,7 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@ -35,129 +35,129 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 {
|
interface Tab {
|
||||||
label: string;
|
label: string
|
||||||
href: string;
|
href: string
|
||||||
shown?: boolean;
|
shown?: boolean
|
||||||
icon?: string;
|
icon?: string
|
||||||
subpages?: string[];
|
subpages?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
links: Tab[];
|
links: Tab[]
|
||||||
query?: string;
|
query?: string
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const scrollContainer = ref<HTMLElement | null>(null);
|
const scrollContainer = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
const sliderLeft = ref(4);
|
const sliderLeft = ref(4)
|
||||||
const sliderTop = ref(4);
|
const sliderTop = ref(4)
|
||||||
const sliderRight = ref(4);
|
const sliderRight = ref(4)
|
||||||
const sliderBottom = ref(4);
|
const sliderBottom = ref(4)
|
||||||
const activeIndex = ref(-1);
|
const activeIndex = ref(-1)
|
||||||
const subpageSelected = ref(false);
|
const subpageSelected = ref(false)
|
||||||
|
|
||||||
const filteredLinks = computed(() =>
|
const filteredLinks = computed(() =>
|
||||||
props.links.filter((x) => (x.shown === undefined ? true : x.shown)),
|
props.links.filter((x) => (x.shown === undefined ? true : x.shown)),
|
||||||
);
|
)
|
||||||
const sliderLeftPx = computed(() => `${sliderLeft.value}px`);
|
const sliderLeftPx = computed(() => `${sliderLeft.value}px`)
|
||||||
const sliderTopPx = computed(() => `${sliderTop.value}px`);
|
const sliderTopPx = computed(() => `${sliderTop.value}px`)
|
||||||
const sliderRightPx = computed(() => `${sliderRight.value}px`);
|
const sliderRightPx = computed(() => `${sliderRight.value}px`)
|
||||||
const sliderBottomPx = computed(() => `${sliderBottom.value}px`);
|
const sliderBottomPx = computed(() => `${sliderBottom.value}px`)
|
||||||
|
|
||||||
const tabLinkElements = ref();
|
const tabLinkElements = ref()
|
||||||
|
|
||||||
function pickLink() {
|
function pickLink() {
|
||||||
let index = -1;
|
let index = -1
|
||||||
subpageSelected.value = false;
|
subpageSelected.value = false
|
||||||
for (let i = filteredLinks.value.length - 1; i >= 0; i--) {
|
for (let i = filteredLinks.value.length - 1; i >= 0; i--) {
|
||||||
const link = filteredLinks.value[i];
|
const link = filteredLinks.value[i]
|
||||||
if (props.query) {
|
if (props.query) {
|
||||||
if (route.query[props.query] === link.href || (!route.query[props.query] && !link.href)) {
|
if (route.query[props.query] === link.href || (!route.query[props.query] && !link.href)) {
|
||||||
index = i;
|
index = i
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
} else if (decodeURIComponent(route.path) === link.href) {
|
} else if (decodeURIComponent(route.path) === link.href) {
|
||||||
index = i;
|
index = i
|
||||||
break;
|
break
|
||||||
} else if (
|
} else if (
|
||||||
decodeURIComponent(route.path).includes(link.href) ||
|
decodeURIComponent(route.path).includes(link.href) ||
|
||||||
(link.subpages &&
|
(link.subpages &&
|
||||||
link.subpages.some((subpage) => decodeURIComponent(route.path).includes(subpage)))
|
link.subpages.some((subpage) => decodeURIComponent(route.path).includes(subpage)))
|
||||||
) {
|
) {
|
||||||
index = i;
|
index = i
|
||||||
subpageSelected.value = true;
|
subpageSelected.value = true
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
activeIndex.value = index;
|
activeIndex.value = index
|
||||||
|
|
||||||
if (activeIndex.value !== -1) {
|
if (activeIndex.value !== -1) {
|
||||||
startAnimation();
|
startAnimation()
|
||||||
} else {
|
} else {
|
||||||
sliderLeft.value = 0;
|
sliderLeft.value = 0
|
||||||
sliderRight.value = 0;
|
sliderRight.value = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startAnimation() {
|
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 = {
|
const newValues = {
|
||||||
left: el.offsetLeft,
|
left: el.offsetLeft,
|
||||||
top: el.offsetTop,
|
top: el.offsetTop,
|
||||||
right: el.offsetParent.offsetWidth - el.offsetLeft - el.offsetWidth,
|
right: el.offsetParent.offsetWidth - el.offsetLeft - el.offsetWidth,
|
||||||
bottom: el.offsetParent.offsetHeight - el.offsetTop - el.offsetHeight,
|
bottom: el.offsetParent.offsetHeight - el.offsetTop - el.offsetHeight,
|
||||||
};
|
}
|
||||||
|
|
||||||
if (sliderLeft.value === 4 && sliderRight.value === 4) {
|
if (sliderLeft.value === 4 && sliderRight.value === 4) {
|
||||||
sliderLeft.value = newValues.left;
|
sliderLeft.value = newValues.left
|
||||||
sliderRight.value = newValues.right;
|
sliderRight.value = newValues.right
|
||||||
sliderTop.value = newValues.top;
|
sliderTop.value = newValues.top
|
||||||
sliderBottom.value = newValues.bottom;
|
sliderBottom.value = newValues.bottom
|
||||||
} else {
|
} else {
|
||||||
const delay = 200;
|
const delay = 200
|
||||||
|
|
||||||
if (newValues.left < sliderLeft.value) {
|
if (newValues.left < sliderLeft.value) {
|
||||||
sliderLeft.value = newValues.left;
|
sliderLeft.value = newValues.left
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sliderRight.value = newValues.right;
|
sliderRight.value = newValues.right
|
||||||
}, delay);
|
}, delay)
|
||||||
} else {
|
} else {
|
||||||
sliderRight.value = newValues.right;
|
sliderRight.value = newValues.right
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sliderLeft.value = newValues.left;
|
sliderLeft.value = newValues.left
|
||||||
}, delay);
|
}, delay)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newValues.top < sliderTop.value) {
|
if (newValues.top < sliderTop.value) {
|
||||||
sliderTop.value = newValues.top;
|
sliderTop.value = newValues.top
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sliderBottom.value = newValues.bottom;
|
sliderBottom.value = newValues.bottom
|
||||||
}, delay);
|
}, delay)
|
||||||
} else {
|
} else {
|
||||||
sliderBottom.value = newValues.bottom;
|
sliderBottom.value = newValues.bottom
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sliderTop.value = newValues.top;
|
sliderTop.value = newValues.top
|
||||||
}, delay);
|
}, delay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
pickLink();
|
pickLink()
|
||||||
});
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [route.path, route.query],
|
() => [route.path, route.query],
|
||||||
() => pickLink(),
|
() => pickLink(),
|
||||||
);
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -211,8 +211,8 @@
|
|||||||
class="iconified-button square-button brand-button button-transparent"
|
class="iconified-button square-button brand-button button-transparent"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
acceptTeamInvite(notification.body.team_id);
|
acceptTeamInvite(notification.body.team_id)
|
||||||
read();
|
read()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@ -223,8 +223,8 @@
|
|||||||
class="iconified-button square-button danger-button button-transparent"
|
class="iconified-button square-button danger-button button-transparent"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
removeSelfFromTeam(notification.body.team_id);
|
removeSelfFromTeam(notification.body.team_id)
|
||||||
read();
|
read()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@ -249,8 +249,8 @@
|
|||||||
class="iconified-button brand-button"
|
class="iconified-button brand-button"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
acceptTeamInvite(notification.body.team_id);
|
acceptTeamInvite(notification.body.team_id)
|
||||||
read();
|
read()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@ -261,8 +261,8 @@
|
|||||||
class="iconified-button danger-button"
|
class="iconified-button danger-button"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
removeSelfFromTeam(notification.body.team_id);
|
removeSelfFromTeam(notification.body.team_id)
|
||||||
read();
|
read()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@ -329,22 +329,22 @@ import {
|
|||||||
UserPlusIcon,
|
UserPlusIcon,
|
||||||
VersionIcon,
|
VersionIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from "@modrinth/assets";
|
} from '@modrinth/assets'
|
||||||
import { Avatar, CopyCode, ProjectStatusBadge, useRelativeTime } from "@modrinth/ui";
|
import { Avatar, CopyCode, ProjectStatusBadge, useRelativeTime } from '@modrinth/ui'
|
||||||
import { renderString } from "@modrinth/utils";
|
import { renderString } from '@modrinth/utils'
|
||||||
|
|
||||||
import DoubleIcon from "~/components/ui/DoubleIcon.vue";
|
import DoubleIcon from '~/components/ui/DoubleIcon.vue'
|
||||||
import Categories from "~/components/ui/search/Categories.vue";
|
import Categories from '~/components/ui/search/Categories.vue'
|
||||||
import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
|
import ThreadSummary from '~/components/ui/thread/ThreadSummary.vue'
|
||||||
import { markAsRead } from "~/helpers/notifications.ts";
|
import { markAsRead } from '~/helpers/notifications.ts'
|
||||||
import { getProjectLink, getVersionLink } from "~/helpers/projects.js";
|
import { getProjectLink, getVersionLink } from '~/helpers/projects.js'
|
||||||
import { acceptTeamInvite, removeSelfFromTeam } from "~/helpers/teams.js";
|
import { acceptTeamInvite, removeSelfFromTeam } from '~/helpers/teams.js'
|
||||||
import { getUserLink } from "~/helpers/users.js";
|
import { getUserLink } from '~/helpers/users.js'
|
||||||
|
|
||||||
const app = useNuxtApp();
|
const app = useNuxtApp()
|
||||||
const emit = defineEmits(["update:notifications"]);
|
const emit = defineEmits(['update:notifications'])
|
||||||
|
|
||||||
const formatRelativeTime = useRelativeTime();
|
const formatRelativeTime = useRelativeTime()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
notification: {
|
notification: {
|
||||||
@ -367,34 +367,34 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const flags = useFeatureFlags();
|
const flags = useFeatureFlags()
|
||||||
const tags = useTags();
|
const tags = useTags()
|
||||||
|
|
||||||
const type = computed(() =>
|
const type = computed(() =>
|
||||||
!props.notification.body || props.notification.body.type === "legacy_markdown"
|
!props.notification.body || props.notification.body.type === 'legacy_markdown'
|
||||||
? null
|
? null
|
||||||
: props.notification.body.type,
|
: props.notification.body.type,
|
||||||
);
|
)
|
||||||
const thread = computed(() => props.notification.extra_data.thread);
|
const thread = computed(() => props.notification.extra_data.thread)
|
||||||
const report = computed(() => props.notification.extra_data.report);
|
const report = computed(() => props.notification.extra_data.report)
|
||||||
const project = computed(() => props.notification.extra_data.project);
|
const project = computed(() => props.notification.extra_data.project)
|
||||||
const version = computed(() => props.notification.extra_data.version);
|
const version = computed(() => props.notification.extra_data.version)
|
||||||
const user = computed(() => props.notification.extra_data.user);
|
const user = computed(() => props.notification.extra_data.user)
|
||||||
const organization = computed(() => props.notification.extra_data.organization);
|
const organization = computed(() => props.notification.extra_data.organization)
|
||||||
const invitedBy = computed(() => props.notification.extra_data.invited_by);
|
const invitedBy = computed(() => props.notification.extra_data.invited_by)
|
||||||
|
|
||||||
const threadLink = computed(() => {
|
const threadLink = computed(() => {
|
||||||
if (report.value) {
|
if (report.value) {
|
||||||
return `/dashboard/report/${report.value.id}`;
|
return `/dashboard/report/${report.value.id}`
|
||||||
} else if (project.value) {
|
} 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() {
|
async function read() {
|
||||||
try {
|
try {
|
||||||
@ -403,54 +403,54 @@ async function read() {
|
|||||||
...(props.notification.grouped_notifs
|
...(props.notification.grouped_notifs
|
||||||
? props.notification.grouped_notifs.map((notif) => notif.id)
|
? props.notification.grouped_notifs.map((notif) => notif.id)
|
||||||
: []),
|
: []),
|
||||||
];
|
]
|
||||||
const updateNotifs = await markAsRead(ids);
|
const updateNotifs = await markAsRead(ids)
|
||||||
const newNotifs = updateNotifs(props.notifications);
|
const newNotifs = updateNotifs(props.notifications)
|
||||||
emit("update:notifications", newNotifs);
|
emit('update:notifications', newNotifs)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
app.$notify({
|
app.$notify({
|
||||||
group: "main",
|
group: 'main',
|
||||||
title: "Error marking notification as read",
|
title: 'Error marking notification as read',
|
||||||
text: err.data ? err.data.description : err,
|
text: err.data ? err.data.description : err,
|
||||||
type: "error",
|
type: 'error',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function performAction(notification, actionIndex) {
|
async function performAction(notification, actionIndex) {
|
||||||
startLoading();
|
startLoading()
|
||||||
try {
|
try {
|
||||||
await read();
|
await read()
|
||||||
|
|
||||||
if (actionIndex !== null) {
|
if (actionIndex !== null) {
|
||||||
await useBaseFetch(`${notification.actions[actionIndex].action_route[1]}`, {
|
await useBaseFetch(`${notification.actions[actionIndex].action_route[1]}`, {
|
||||||
method: notification.actions[actionIndex].action_route[0].toUpperCase(),
|
method: notification.actions[actionIndex].action_route[0].toUpperCase(),
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
app.$notify({
|
app.$notify({
|
||||||
group: "main",
|
group: 'main',
|
||||||
title: "An error occurred",
|
title: 'An error occurred',
|
||||||
text: err.data ? err.data.description : err,
|
text: err.data ? err.data.description : err,
|
||||||
type: "error",
|
type: 'error',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
stopLoading();
|
stopLoading()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMessages() {
|
function getMessages() {
|
||||||
const messages = [];
|
const messages = []
|
||||||
if (props.notification.body.message_id) {
|
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) {
|
if (props.notification.grouped_notifs) {
|
||||||
for (const notif of props.notification.grouped_notifs) {
|
for (const notif of props.notification.grouped_notifs) {
|
||||||
if (notif.body.message_id) {
|
if (notif.body.message_id) {
|
||||||
messages.push(notif.body.message_id);
|
messages.push(notif.body.message_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return messages;
|
return messages
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -458,35 +458,35 @@ function getMessages() {
|
|||||||
.notification {
|
.notification {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template:
|
grid-template:
|
||||||
"icon title"
|
'icon title'
|
||||||
"actions actions"
|
'actions actions'
|
||||||
"date date";
|
'date date';
|
||||||
grid-template-columns: min-content 1fr;
|
grid-template-columns: min-content 1fr;
|
||||||
grid-template-rows: min-content min-content min-content;
|
grid-template-rows: min-content min-content min-content;
|
||||||
gap: var(--spacing-card-sm);
|
gap: var(--spacing-card-sm);
|
||||||
|
|
||||||
&.compact {
|
&.compact {
|
||||||
grid-template:
|
grid-template:
|
||||||
"icon title actions"
|
'icon title actions'
|
||||||
"date date date";
|
'date date date';
|
||||||
grid-template-columns: min-content 1fr auto;
|
grid-template-columns: min-content 1fr auto;
|
||||||
grid-template-rows: auto min-content;
|
grid-template-rows: auto min-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.has-body {
|
&.has-body {
|
||||||
grid-template:
|
grid-template:
|
||||||
"icon title"
|
'icon title'
|
||||||
"body body"
|
'body body'
|
||||||
"actions actions"
|
'actions actions'
|
||||||
"date date";
|
'date date';
|
||||||
grid-template-columns: min-content 1fr;
|
grid-template-columns: min-content 1fr;
|
||||||
grid-template-rows: min-content auto auto min-content;
|
grid-template-rows: min-content auto auto min-content;
|
||||||
|
|
||||||
&.compact {
|
&.compact {
|
||||||
grid-template:
|
grid-template:
|
||||||
"icon title actions"
|
'icon title actions'
|
||||||
"body body body"
|
'body body body'
|
||||||
"date date date";
|
'date date date';
|
||||||
grid-template-columns: min-content 1fr auto;
|
grid-template-columns: min-content 1fr auto;
|
||||||
grid-template-rows: min-content auto min-content;
|
grid-template-rows: min-content auto min-content;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,88 +31,88 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts" generic="T">
|
<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<{
|
const props = defineProps<{
|
||||||
options: T[];
|
options: T[]
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const scrollContainer = ref<HTMLElement | null>(null);
|
const scrollContainer = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
const sliderLeft = ref(4);
|
const sliderLeft = ref(4)
|
||||||
const sliderTop = ref(4);
|
const sliderTop = ref(4)
|
||||||
const sliderRight = ref(4);
|
const sliderRight = ref(4)
|
||||||
const sliderBottom = ref(4);
|
const sliderBottom = ref(4)
|
||||||
|
|
||||||
const sliderLeftPx = computed(() => `${sliderLeft.value}px`);
|
const sliderLeftPx = computed(() => `${sliderLeft.value}px`)
|
||||||
const sliderTopPx = computed(() => `${sliderTop.value}px`);
|
const sliderTopPx = computed(() => `${sliderTop.value}px`)
|
||||||
const sliderRightPx = computed(() => `${sliderRight.value}px`);
|
const sliderRightPx = computed(() => `${sliderRight.value}px`)
|
||||||
const sliderBottomPx = computed(() => `${sliderBottom.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) {
|
function setOption(option: T) {
|
||||||
modelValue.value = option;
|
modelValue.value = option
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(modelValue, () => {
|
watch(modelValue, () => {
|
||||||
startAnimation(props.options.indexOf(modelValue.value));
|
startAnimation(props.options.indexOf(modelValue.value))
|
||||||
});
|
})
|
||||||
|
|
||||||
function startAnimation(index: number) {
|
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 = {
|
const newValues = {
|
||||||
left: el.offsetLeft,
|
left: el.offsetLeft,
|
||||||
top: el.offsetTop,
|
top: el.offsetTop,
|
||||||
right: el.offsetParent.offsetWidth - el.offsetLeft - el.offsetWidth,
|
right: el.offsetParent.offsetWidth - el.offsetLeft - el.offsetWidth,
|
||||||
bottom: el.offsetParent.offsetHeight - el.offsetTop - el.offsetHeight,
|
bottom: el.offsetParent.offsetHeight - el.offsetTop - el.offsetHeight,
|
||||||
};
|
}
|
||||||
|
|
||||||
if (sliderLeft.value === 4 && sliderRight.value === 4) {
|
if (sliderLeft.value === 4 && sliderRight.value === 4) {
|
||||||
sliderLeft.value = newValues.left;
|
sliderLeft.value = newValues.left
|
||||||
sliderRight.value = newValues.right;
|
sliderRight.value = newValues.right
|
||||||
sliderTop.value = newValues.top;
|
sliderTop.value = newValues.top
|
||||||
sliderBottom.value = newValues.bottom;
|
sliderBottom.value = newValues.bottom
|
||||||
} else {
|
} else {
|
||||||
const delay = 200;
|
const delay = 200
|
||||||
|
|
||||||
if (newValues.left < sliderLeft.value) {
|
if (newValues.left < sliderLeft.value) {
|
||||||
sliderLeft.value = newValues.left;
|
sliderLeft.value = newValues.left
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sliderRight.value = newValues.right;
|
sliderRight.value = newValues.right
|
||||||
}, delay);
|
}, delay)
|
||||||
} else {
|
} else {
|
||||||
sliderRight.value = newValues.right;
|
sliderRight.value = newValues.right
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sliderLeft.value = newValues.left;
|
sliderLeft.value = newValues.left
|
||||||
}, delay);
|
}, delay)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newValues.top < sliderTop.value) {
|
if (newValues.top < sliderTop.value) {
|
||||||
sliderTop.value = newValues.top;
|
sliderTop.value = newValues.top
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sliderBottom.value = newValues.bottom;
|
sliderBottom.value = newValues.bottom
|
||||||
}, delay);
|
}, delay)
|
||||||
} else {
|
} else {
|
||||||
sliderBottom.value = newValues.bottom;
|
sliderBottom.value = newValues.bottom
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sliderTop.value = newValues.top;
|
sliderTop.value = newValues.top
|
||||||
}, delay);
|
}, delay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
initialized.value = true;
|
initialized.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
startAnimation(props.options.indexOf(modelValue.value));
|
startAnimation(props.options.indexOf(modelValue.value))
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -71,51 +71,51 @@
|
|||||||
</NewModal>
|
</NewModal>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { PlusIcon,XIcon } from "@modrinth/assets";
|
import { PlusIcon, XIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
import { ButtonStyled, NewModal } from '@modrinth/ui'
|
||||||
|
|
||||||
const router = useNativeRouter();
|
const router = useNativeRouter()
|
||||||
|
|
||||||
const name = ref("");
|
const name = ref('')
|
||||||
const slug = ref("");
|
const slug = ref('')
|
||||||
const description = ref("");
|
const description = ref('')
|
||||||
const manualSlug = ref(false);
|
const manualSlug = ref(false)
|
||||||
|
|
||||||
const modal = ref();
|
const modal = ref()
|
||||||
|
|
||||||
async function createOrganization() {
|
async function createOrganization() {
|
||||||
startLoading();
|
startLoading()
|
||||||
try {
|
try {
|
||||||
const value = {
|
const value = {
|
||||||
name: name.value.trim(),
|
name: name.value.trim(),
|
||||||
description: description.value.trim(),
|
description: description.value.trim(),
|
||||||
slug: slug.value.trim().replace(/ +/g, ""),
|
slug: slug.value.trim().replace(/ +/g, ''),
|
||||||
};
|
}
|
||||||
|
|
||||||
const result = await useBaseFetch("organization", {
|
const result = await useBaseFetch('organization', {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
body: JSON.stringify(value),
|
body: JSON.stringify(value),
|
||||||
apiVersion: 3,
|
apiVersion: 3,
|
||||||
});
|
})
|
||||||
|
|
||||||
modal.value.hide();
|
modal.value.hide()
|
||||||
|
|
||||||
await router.push(`/organization/${result.slug}`);
|
await router.push(`/organization/${result.slug}`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err)
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "main",
|
group: 'main',
|
||||||
title: "An error occurred",
|
title: 'An error occurred',
|
||||||
text: err.data ? err.data.description : err,
|
text: err.data ? err.data.description : err,
|
||||||
type: "error",
|
type: 'error',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
stopLoading();
|
stopLoading()
|
||||||
}
|
}
|
||||||
function show(event) {
|
function show(event) {
|
||||||
name.value = "";
|
name.value = ''
|
||||||
description.value = "";
|
description.value = ''
|
||||||
modal.value.show(event);
|
modal.value.show(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSlug() {
|
function updateSlug() {
|
||||||
@ -123,15 +123,15 @@ function updateSlug() {
|
|||||||
slug.value = name.value
|
slug.value = name.value
|
||||||
.trim()
|
.trim()
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replaceAll(" ", "-")
|
.replaceAll(' ', '-')
|
||||||
.replaceAll(/[^a-zA-Z0-9!@$()`.+,_"-]/g, "")
|
.replaceAll(/[^a-zA-Z0-9!@$()`.+,_"-]/g, '')
|
||||||
.replaceAll(/--+/gm, "-");
|
.replaceAll(/--+/gm, '-')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show,
|
show,
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@ -59,7 +59,7 @@
|
|||||||
<span>{{
|
<span>{{
|
||||||
formatProjectType(
|
formatProjectType(
|
||||||
$getProjectTypeForDisplay(
|
$getProjectTypeForDisplay(
|
||||||
project.project_types?.[0] ?? "project",
|
project.project_types?.[0] ?? 'project',
|
||||||
project.loaders,
|
project.loaders,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -88,13 +88,13 @@
|
|||||||
<span>
|
<span>
|
||||||
{{
|
{{
|
||||||
selectedProjects.length === props.projects.length
|
selectedProjects.length === props.projects.length
|
||||||
? "All"
|
? 'All'
|
||||||
: selectedProjects.length
|
: selectedProjects.length
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{{ " " }}
|
{{ ' ' }}
|
||||||
{{ selectedProjects.length === 1 ? "project" : "projects" }}
|
{{ selectedProjects.length === 1 ? 'project' : 'projects' }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
@ -109,40 +109,40 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { BoxIcon, SettingsIcon, TransferIcon, XIcon } from "@modrinth/assets";
|
import { BoxIcon, SettingsIcon, TransferIcon, XIcon } from '@modrinth/assets'
|
||||||
import { Avatar,Button, Checkbox, CopyCode, Modal } from "@modrinth/ui";
|
import { Avatar, Button, Checkbox, CopyCode, Modal } from '@modrinth/ui'
|
||||||
import { formatProjectType } from "@modrinth/utils";
|
import { formatProjectType } from '@modrinth/utils'
|
||||||
|
|
||||||
const modalOpen = ref(null);
|
const modalOpen = ref(null)
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
projects: {
|
projects: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
// define emit for submission
|
// define emit for submission
|
||||||
const emit = defineEmits(["submit"]);
|
const emit = defineEmits(['submit'])
|
||||||
|
|
||||||
const selectedProjects = ref([]);
|
const selectedProjects = ref([])
|
||||||
|
|
||||||
const toggleSelectedProjects = () => {
|
const toggleSelectedProjects = () => {
|
||||||
if (selectedProjects.value.length === props.projects.length) {
|
if (selectedProjects.value.length === props.projects.length) {
|
||||||
selectedProjects.value = [];
|
selectedProjects.value = []
|
||||||
} else {
|
} else {
|
||||||
selectedProjects.value = props.projects;
|
selectedProjects.value = props.projects
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const onSubmitHandler = () => {
|
const onSubmitHandler = () => {
|
||||||
if (selectedProjects.value.length === 0) {
|
if (selectedProjects.value.length === 0) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
emit("submit", selectedProjects.value);
|
emit('submit', selectedProjects.value)
|
||||||
selectedProjects.value = [];
|
selectedProjects.value = []
|
||||||
modalOpen.value?.hide();
|
modalOpen.value?.hide()
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@ -176,7 +176,7 @@ const onSubmitHandler = () => {
|
|||||||
|
|
||||||
.table-row {
|
.table-row {
|
||||||
display: grid;
|
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:
|
grid-template-columns:
|
||||||
min-content min-content minmax(min-content, 2fr)
|
min-content min-content minmax(min-content, 2fr)
|
||||||
minmax(min-content, 1fr) min-content;
|
minmax(min-content, 1fr) min-content;
|
||||||
@ -208,7 +208,7 @@ const onSubmitHandler = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.table-head {
|
.table-head {
|
||||||
grid-template: "checkbox settings";
|
grid-template: 'checkbox settings';
|
||||||
grid-template-columns: min-content minmax(min-content, 1fr);
|
grid-template-columns: min-content minmax(min-content, 1fr);
|
||||||
|
|
||||||
:nth-child(2),
|
:nth-child(2),
|
||||||
@ -223,7 +223,7 @@ const onSubmitHandler = () => {
|
|||||||
@media screen and (max-width: 560px) {
|
@media screen and (max-width: 560px) {
|
||||||
.table-row {
|
.table-row {
|
||||||
display: grid;
|
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;
|
grid-template-columns: min-content min-content minmax(min-content, 1fr) min-content;
|
||||||
|
|
||||||
:nth-child(5) {
|
:nth-child(5) {
|
||||||
@ -232,7 +232,7 @@ const onSubmitHandler = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.table-head {
|
.table-head {
|
||||||
grid-template: "checkbox settings";
|
grid-template: 'checkbox settings';
|
||||||
grid-template-columns: min-content minmax(min-content, 1fr);
|
grid-template-columns: min-content minmax(min-content, 1fr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -90,11 +90,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { CalendarIcon, DownloadIcon, HeartIcon,UpdatedIcon } from "@modrinth/assets";
|
import { CalendarIcon, DownloadIcon, HeartIcon, UpdatedIcon } from '@modrinth/assets'
|
||||||
import { Avatar, ProjectStatusBadge, useRelativeTime } from "@modrinth/ui";
|
import { Avatar, ProjectStatusBadge, useRelativeTime } from '@modrinth/ui'
|
||||||
|
|
||||||
import EnvironmentIndicator from "~/components/ui/EnvironmentIndicator.vue";
|
import EnvironmentIndicator from '~/components/ui/EnvironmentIndicator.vue'
|
||||||
import Categories from "~/components/ui/search/Categories.vue";
|
import Categories from '~/components/ui/search/Categories.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -110,15 +110,15 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
id: {
|
id: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "modrinth-0",
|
default: 'modrinth-0',
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "mod",
|
default: 'mod',
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "Project Name",
|
default: 'Project Name',
|
||||||
},
|
},
|
||||||
author: {
|
author: {
|
||||||
type: String,
|
type: String,
|
||||||
@ -126,11 +126,11 @@ export default {
|
|||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "A _type description",
|
default: 'A _type description',
|
||||||
},
|
},
|
||||||
iconUrl: {
|
iconUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "#",
|
default: '#',
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
downloads: {
|
downloads: {
|
||||||
@ -145,7 +145,7 @@ export default {
|
|||||||
},
|
},
|
||||||
createdAt: {
|
createdAt: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "0000-00-00",
|
default: '0000-00-00',
|
||||||
},
|
},
|
||||||
updatedAt: {
|
updatedAt: {
|
||||||
type: String,
|
type: String,
|
||||||
@ -154,7 +154,7 @@ export default {
|
|||||||
categories: {
|
categories: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default() {
|
default() {
|
||||||
return [];
|
return []
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
@ -168,12 +168,12 @@ export default {
|
|||||||
serverSide: {
|
serverSide: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: "",
|
default: '',
|
||||||
},
|
},
|
||||||
clientSide: {
|
clientSide: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: "",
|
default: '',
|
||||||
},
|
},
|
||||||
moderation: {
|
moderation: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -212,26 +212,26 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const tags = useTags();
|
const tags = useTags()
|
||||||
const formatRelativeTime = useRelativeTime();
|
const formatRelativeTime = useRelativeTime()
|
||||||
|
|
||||||
return { tags, formatRelativeTime };
|
return { tags, formatRelativeTime }
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
projectTypeDisplay() {
|
projectTypeDisplay() {
|
||||||
return this.$getProjectTypeForDisplay(this.type, this.categories);
|
return this.$getProjectTypeForDisplay(this.type, this.categories)
|
||||||
},
|
},
|
||||||
toColor() {
|
toColor() {
|
||||||
let color = this.color;
|
let color = this.color
|
||||||
|
|
||||||
color >>>= 0;
|
color >>>= 0
|
||||||
const b = color & 0xff;
|
const b = color & 0xff
|
||||||
const g = (color & 0xff00) >>> 8;
|
const g = (color & 0xff00) >>> 8
|
||||||
const r = (color & 0xff0000) >>> 16;
|
const r = (color & 0xff0000) >>> 16
|
||||||
return "rgba(" + [r, g, b, 1].join(",") + ")";
|
return 'rgba(' + [r, g, b, 1].join(',') + ')'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@ -244,9 +244,9 @@ export default {
|
|||||||
|
|
||||||
.display-mode--list .project-card {
|
.display-mode--list .project-card {
|
||||||
grid-template:
|
grid-template:
|
||||||
"icon title stats"
|
'icon title stats'
|
||||||
"icon description stats"
|
'icon description stats'
|
||||||
"icon tags stats";
|
'icon tags stats';
|
||||||
grid-template-columns: min-content 1fr auto;
|
grid-template-columns: min-content 1fr auto;
|
||||||
grid-template-rows: min-content 1fr min-content;
|
grid-template-rows: min-content 1fr min-content;
|
||||||
column-gap: var(--spacing-card-md);
|
column-gap: var(--spacing-card-md);
|
||||||
@ -255,20 +255,20 @@ export default {
|
|||||||
|
|
||||||
@media screen and (max-width: 750px) {
|
@media screen and (max-width: 750px) {
|
||||||
grid-template:
|
grid-template:
|
||||||
"icon title"
|
'icon title'
|
||||||
"icon description"
|
'icon description'
|
||||||
"icon tags"
|
'icon tags'
|
||||||
"stats stats";
|
'stats stats';
|
||||||
grid-template-columns: min-content auto;
|
grid-template-columns: min-content auto;
|
||||||
grid-template-rows: min-content 1fr min-content min-content;
|
grid-template-rows: min-content 1fr min-content min-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 550px) {
|
@media screen and (max-width: 550px) {
|
||||||
grid-template:
|
grid-template:
|
||||||
"icon title"
|
'icon title'
|
||||||
"icon description"
|
'icon description'
|
||||||
"tags tags"
|
'tags tags'
|
||||||
"stats stats";
|
'stats stats';
|
||||||
grid-template-columns: min-content auto;
|
grid-template-columns: min-content auto;
|
||||||
grid-template-rows: min-content 1fr min-content min-content;
|
grid-template-rows: min-content 1fr min-content min-content;
|
||||||
}
|
}
|
||||||
@ -277,7 +277,7 @@ export default {
|
|||||||
.display-mode--gallery .project-card,
|
.display-mode--gallery .project-card,
|
||||||
.display-mode--grid .project-card {
|
.display-mode--grid .project-card {
|
||||||
padding: 0 0 var(--spacing-card-bg) 0;
|
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-columns: min-content 1fr;
|
||||||
grid-template-rows: min-content min-content 1fr min-content min-content;
|
grid-template-rows: min-content min-content 1fr min-content min-content;
|
||||||
row-gap: var(--spacing-card-sm);
|
row-gap: var(--spacing-card-sm);
|
||||||
@ -501,10 +501,10 @@ export default {
|
|||||||
.small-mode {
|
.small-mode {
|
||||||
@media screen and (min-width: 750px) {
|
@media screen and (min-width: 750px) {
|
||||||
grid-template:
|
grid-template:
|
||||||
"icon title"
|
'icon title'
|
||||||
"icon description"
|
'icon description'
|
||||||
"icon tags"
|
'icon tags'
|
||||||
"stats stats" !important;
|
'stats stats' !important;
|
||||||
grid-template-columns: min-content auto !important;
|
grid-template-columns: min-content auto !important;
|
||||||
grid-template-rows: min-content 1fr min-content min-content !important;
|
grid-template-rows: min-content 1fr min-content min-content !important;
|
||||||
|
|
||||||
|
|||||||
@ -117,10 +117,10 @@ import {
|
|||||||
ScaleIcon,
|
ScaleIcon,
|
||||||
SendIcon,
|
SendIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from "@modrinth/assets";
|
} from '@modrinth/assets'
|
||||||
import { formatProjectType } from "@modrinth/utils";
|
import { formatProjectType } from '@modrinth/utils'
|
||||||
|
|
||||||
import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js";
|
import { acceptTeamInvite, removeTeamMember } from '~/helpers/teams.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
project: {
|
project: {
|
||||||
@ -130,7 +130,7 @@ const props = defineProps({
|
|||||||
versions: {
|
versions: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default() {
|
default() {
|
||||||
return [];
|
return []
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
currentMember: {
|
currentMember: {
|
||||||
@ -151,7 +151,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
routeName: {
|
routeName: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: '',
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -166,12 +166,12 @@ const props = defineProps({
|
|||||||
default() {
|
default() {
|
||||||
return () => {
|
return () => {
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "main",
|
group: 'main',
|
||||||
title: "An error occurred",
|
title: 'An error occurred',
|
||||||
text: "setProcessing function not found",
|
text: 'setProcessing function not found',
|
||||||
type: "error",
|
type: 'error',
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
toggleCollapsed: {
|
toggleCollapsed: {
|
||||||
@ -179,12 +179,12 @@ const props = defineProps({
|
|||||||
default() {
|
default() {
|
||||||
return () => {
|
return () => {
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "main",
|
group: 'main',
|
||||||
title: "An error occurred",
|
title: 'An error occurred',
|
||||||
text: "toggleCollapsed function not found",
|
text: 'toggleCollapsed function not found',
|
||||||
type: "error",
|
type: 'error',
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
updateMembers: {
|
updateMembers: {
|
||||||
@ -192,81 +192,81 @@ const props = defineProps({
|
|||||||
default() {
|
default() {
|
||||||
return () => {
|
return () => {
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "main",
|
group: 'main',
|
||||||
title: "An error occurred",
|
title: 'An error occurred',
|
||||||
text: "updateMembers function not found",
|
text: 'updateMembers function not found',
|
||||||
type: "error",
|
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(() => [
|
const nags = computed(() => [
|
||||||
{
|
{
|
||||||
condition: props.versions.length < 1,
|
condition: props.versions.length < 1,
|
||||||
title: "Upload a version",
|
title: 'Upload a version',
|
||||||
id: "upload-version",
|
id: 'upload-version',
|
||||||
description: "At least one version is required for a project to be submitted for review.",
|
description: 'At least one version is required for a project to be submitted for review.',
|
||||||
status: "required",
|
status: 'required',
|
||||||
link: {
|
link: {
|
||||||
path: "versions",
|
path: 'versions',
|
||||||
title: "Visit versions page",
|
title: 'Visit versions page',
|
||||||
hide: props.routeName === "type-id-versions",
|
hide: props.routeName === 'type-id-versions',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
condition:
|
condition:
|
||||||
props.project.body === "" || props.project.body.startsWith("# Placeholder description"),
|
props.project.body === '' || props.project.body.startsWith('# Placeholder description'),
|
||||||
title: "Add a description",
|
title: 'Add a description',
|
||||||
id: "add-description",
|
id: 'add-description',
|
||||||
description:
|
description:
|
||||||
"A description that clearly describes the project's purpose and function is required.",
|
"A description that clearly describes the project's purpose and function is required.",
|
||||||
status: "required",
|
status: 'required',
|
||||||
link: {
|
link: {
|
||||||
path: "settings/description",
|
path: 'settings/description',
|
||||||
title: "Visit description settings",
|
title: 'Visit description settings',
|
||||||
hide: props.routeName === "type-id-settings-description",
|
hide: props.routeName === 'type-id-settings-description',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
condition: !props.project.icon_url,
|
condition: !props.project.icon_url,
|
||||||
title: "Add an icon",
|
title: 'Add an icon',
|
||||||
id: "add-icon",
|
id: 'add-icon',
|
||||||
description:
|
description:
|
||||||
"Your project should have a nice-looking icon to uniquely identify your project at a glance.",
|
'Your project should have a nice-looking icon to uniquely identify your project at a glance.',
|
||||||
status: "suggestion",
|
status: 'suggestion',
|
||||||
link: {
|
link: {
|
||||||
path: "settings",
|
path: 'settings',
|
||||||
title: "Visit general settings",
|
title: 'Visit general settings',
|
||||||
hide: props.routeName === "type-id-settings",
|
hide: props.routeName === 'type-id-settings',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
condition: props.project.gallery.length === 0 || !featuredGalleryImage.value,
|
condition: props.project.gallery.length === 0 || !featuredGalleryImage.value,
|
||||||
title: "Feature a gallery image",
|
title: 'Feature a gallery image',
|
||||||
id: "feature-gallery-image",
|
id: 'feature-gallery-image',
|
||||||
description: "Featured gallery images may be the first impression of many users.",
|
description: 'Featured gallery images may be the first impression of many users.',
|
||||||
status: "suggestion",
|
status: 'suggestion',
|
||||||
link: {
|
link: {
|
||||||
path: "gallery",
|
path: 'gallery',
|
||||||
title: "Visit gallery page",
|
title: 'Visit gallery page',
|
||||||
hide: props.routeName === "type-id-gallery",
|
hide: props.routeName === 'type-id-gallery',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
hide: props.project.versions.length === 0,
|
hide: props.project.versions.length === 0,
|
||||||
condition: props.project.categories.length < 1,
|
condition: props.project.categories.length < 1,
|
||||||
title: "Select tags",
|
title: 'Select tags',
|
||||||
id: "select-tags",
|
id: 'select-tags',
|
||||||
description: "Select all tags that apply to your project.",
|
description: 'Select all tags that apply to your project.',
|
||||||
status: "suggestion",
|
status: 'suggestion',
|
||||||
link: {
|
link: {
|
||||||
path: "settings/tags",
|
path: 'settings/tags',
|
||||||
title: "Visit tag settings",
|
title: 'Visit tag settings',
|
||||||
hide: props.routeName === "type-id-settings-tags",
|
hide: props.routeName === 'type-id-settings-tags',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -277,111 +277,111 @@ const nags = computed(() => [
|
|||||||
props.project.discord_url ||
|
props.project.discord_url ||
|
||||||
props.project.donation_urls.length > 0
|
props.project.donation_urls.length > 0
|
||||||
),
|
),
|
||||||
title: "Add external links",
|
title: 'Add external links',
|
||||||
id: "add-links",
|
id: 'add-links',
|
||||||
description:
|
description:
|
||||||
"Add any relevant links targeted outside of Modrinth, such as sources, issues, or a Discord invite.",
|
'Add any relevant links targeted outside of Modrinth, such as sources, issues, or a Discord invite.',
|
||||||
status: "suggestion",
|
status: 'suggestion',
|
||||||
link: {
|
link: {
|
||||||
path: "settings/links",
|
path: 'settings/links',
|
||||||
title: "Visit links settings",
|
title: 'Visit links settings',
|
||||||
hide: props.routeName === "type-id-settings-links",
|
hide: props.routeName === 'type-id-settings-links',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
hide:
|
hide:
|
||||||
props.project.versions.length === 0 ||
|
props.project.versions.length === 0 ||
|
||||||
props.project.project_type === "resourcepack" ||
|
props.project.project_type === 'resourcepack' ||
|
||||||
props.project.project_type === "plugin" ||
|
props.project.project_type === 'plugin' ||
|
||||||
props.project.project_type === "shader" ||
|
props.project.project_type === 'shader' ||
|
||||||
props.project.project_type === "datapack",
|
props.project.project_type === 'datapack',
|
||||||
condition:
|
condition:
|
||||||
props.project.client_side === "unknown" ||
|
props.project.client_side === 'unknown' ||
|
||||||
props.project.server_side === "unknown" ||
|
props.project.server_side === 'unknown' ||
|
||||||
(props.project.client_side === "unsupported" && props.project.server_side === "unsupported"),
|
(props.project.client_side === 'unsupported' && props.project.server_side === 'unsupported'),
|
||||||
title: "Select supported environments",
|
title: 'Select supported environments',
|
||||||
id: "select-environments",
|
id: 'select-environments',
|
||||||
description: `Select if the ${formatProjectType(
|
description: `Select if the ${formatProjectType(
|
||||||
props.project.project_type,
|
props.project.project_type,
|
||||||
).toLowerCase()} functions on the client-side and/or server-side.`,
|
).toLowerCase()} functions on the client-side and/or server-side.`,
|
||||||
status: "required",
|
status: 'required',
|
||||||
link: {
|
link: {
|
||||||
path: "settings",
|
path: 'settings',
|
||||||
title: "Visit general settings",
|
title: 'Visit general settings',
|
||||||
hide: props.routeName === "type-id-settings",
|
hide: props.routeName === 'type-id-settings',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
condition: props.project.license.id === "LicenseRef-Unknown",
|
condition: props.project.license.id === 'LicenseRef-Unknown',
|
||||||
title: "Select license",
|
title: 'Select license',
|
||||||
id: "select-license",
|
id: 'select-license',
|
||||||
description: `Select the license your ${formatProjectType(
|
description: `Select the license your ${formatProjectType(
|
||||||
props.project.project_type,
|
props.project.project_type,
|
||||||
).toLowerCase()} is distributed under.`,
|
).toLowerCase()} is distributed under.`,
|
||||||
status: "required",
|
status: 'required',
|
||||||
link: {
|
link: {
|
||||||
path: "settings/license",
|
path: 'settings/license',
|
||||||
title: "Visit license settings",
|
title: 'Visit license settings',
|
||||||
hide: props.routeName === "type-id-settings-license",
|
hide: props.routeName === 'type-id-settings-license',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
condition: props.project.status === "draft",
|
condition: props.project.status === 'draft',
|
||||||
title: "Submit for review",
|
title: 'Submit for review',
|
||||||
id: "submit-for-review",
|
id: 'submit-for-review',
|
||||||
description:
|
description:
|
||||||
"Your project is only viewable by members of the project. It must be reviewed by moderators in order to be published.",
|
'Your project is only viewable by members of the project. It must be reviewed by moderators in order to be published.',
|
||||||
status: "review",
|
status: 'review',
|
||||||
link: null,
|
link: null,
|
||||||
action: {
|
action: {
|
||||||
onClick: submitForReview,
|
onClick: submitForReview,
|
||||||
title: "Submit for review",
|
title: 'Submit for review',
|
||||||
disabled: () => nags.value.filter((x) => x.condition && x.status === "required").length > 0,
|
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),
|
condition: props.tags.rejectedStatuses.includes(props.project.status),
|
||||||
title: "Resubmit for review",
|
title: 'Resubmit for review',
|
||||||
id: "resubmit-for-review",
|
id: 'resubmit-for-review',
|
||||||
description: `Your project has been ${props.project.status} by
|
description: `Your project has been ${props.project.status} by
|
||||||
Modrinth's staff. In most cases, you can resubmit for review after
|
Modrinth's staff. In most cases, you can resubmit for review after
|
||||||
addressing the staff's message.`,
|
addressing the staff's message.`,
|
||||||
status: "review",
|
status: 'review',
|
||||||
link: {
|
link: {
|
||||||
path: "moderation",
|
path: 'moderation',
|
||||||
title: "Visit moderation page",
|
title: 'Visit moderation page',
|
||||||
hide: props.routeName === "type-id-moderation",
|
hide: props.routeName === 'type-id-moderation',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
])
|
||||||
|
|
||||||
const showInvitation = computed(() => {
|
const showInvitation = computed(() => {
|
||||||
if (props.allMembers && props.auth) {
|
if (props.allMembers && props.auth) {
|
||||||
const member = props.allMembers.find((x) => x.user.id === props.auth.user.id);
|
const member = props.allMembers.find((x) => x.user.id === props.auth.user.id)
|
||||||
return member && !member.accepted;
|
return member && !member.accepted
|
||||||
}
|
}
|
||||||
return false;
|
return false
|
||||||
});
|
})
|
||||||
|
|
||||||
const acceptInvite = () => {
|
const acceptInvite = () => {
|
||||||
acceptTeamInvite(props.project.team);
|
acceptTeamInvite(props.project.team)
|
||||||
props.updateMembers();
|
props.updateMembers()
|
||||||
};
|
}
|
||||||
|
|
||||||
const declineInvite = () => {
|
const declineInvite = () => {
|
||||||
removeTeamMember(props.project.team, props.auth.user.id);
|
removeTeamMember(props.project.team, props.auth.user.id)
|
||||||
props.updateMembers();
|
props.updateMembers()
|
||||||
};
|
}
|
||||||
|
|
||||||
const submitForReview = async () => {
|
const submitForReview = async () => {
|
||||||
if (
|
if (
|
||||||
!props.acknowledgedMessage ||
|
!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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@ -64,23 +64,23 @@ import {
|
|||||||
MailIcon,
|
MailIcon,
|
||||||
MastodonIcon,
|
MastodonIcon,
|
||||||
TwitterIcon,
|
TwitterIcon,
|
||||||
} from "@modrinth/assets";
|
} from '@modrinth/assets'
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
import { ButtonStyled } from '@modrinth/ui'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
title?: string;
|
title?: string
|
||||||
url: string;
|
url: string
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const copied = ref(false);
|
const copied = ref(false)
|
||||||
const encodedUrl = computed(() => encodeURIComponent(props.url));
|
const encodedUrl = computed(() => encodeURIComponent(props.url))
|
||||||
const encodedTitle = computed(() => (props.title ? encodeURIComponent(props.title) : undefined));
|
const encodedTitle = computed(() => (props.title ? encodeURIComponent(props.title) : undefined))
|
||||||
|
|
||||||
async function copyToClipboard(text: string) {
|
async function copyToClipboard(text: string) {
|
||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text)
|
||||||
copied.value = true;
|
copied.value = true
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
copied.value = false;
|
copied.value = false
|
||||||
}, 3000);
|
}, 3000)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { formatMoney,formatNumber } from "@modrinth/utils";
|
import { formatMoney, formatNumber } from '@modrinth/utils'
|
||||||
import dayjs from "dayjs";
|
import dayjs from 'dayjs'
|
||||||
import VueApexCharts from "vue3-apexcharts";
|
import VueApexCharts from 'vue3-apexcharts'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
name: {
|
name: {
|
||||||
@ -18,7 +18,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
formatLabels: {
|
formatLabels: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: (label) => dayjs(label).format("MMM D"),
|
default: (label) => dayjs(label).format('MMM D'),
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@ -26,11 +26,11 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
prefix: {
|
prefix: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: '',
|
||||||
},
|
},
|
||||||
suffix: {
|
suffix: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: '',
|
||||||
},
|
},
|
||||||
hideToolbar: {
|
hideToolbar: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -46,7 +46,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "bar",
|
default: 'bar',
|
||||||
},
|
},
|
||||||
hideTotal: {
|
hideTotal: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -58,11 +58,11 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
legendPosition: {
|
legendPosition: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "right",
|
default: 'right',
|
||||||
},
|
},
|
||||||
xAxisType: {
|
xAxisType: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "datetime",
|
default: 'datetime',
|
||||||
},
|
},
|
||||||
percentStacked: {
|
percentStacked: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -76,14 +76,14 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
function formatTooltipValue(value, props) {
|
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) {
|
function generateListEntry(value, index, _, w, props) {
|
||||||
const color = w.globals.colors?.[index];
|
const color = w.globals.colors?.[index]
|
||||||
|
|
||||||
return `<div class="list-entry">
|
return `<div class="list-entry">
|
||||||
<span class="circle" style="background-color: ${color}"></span>
|
<span class="circle" style="background-color: ${color}"></span>
|
||||||
@ -93,35 +93,35 @@ function generateListEntry(value, index, _, w, props) {
|
|||||||
<div class="value">
|
<div class="value">
|
||||||
${props.prefix}${formatTooltipValue(value, props)}${props.suffix}
|
${props.prefix}${formatTooltipValue(value, props)}${props.suffix}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateTooltip({ series, seriesIndex, dataPointIndex, w }, props) {
|
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">
|
let tooltip = `<div class="bar-tooltip">
|
||||||
<div class="seperated-entry title">
|
<div class="seperated-entry title">
|
||||||
<div class="label">${formattedLabel}</div>`;
|
<div class="label">${formattedLabel}</div>`
|
||||||
|
|
||||||
// Logic for total and percent stacked
|
// Logic for total and percent stacked
|
||||||
if (!props.hideTotal) {
|
if (!props.hideTotal) {
|
||||||
if (props.percentStacked) {
|
if (props.percentStacked) {
|
||||||
const total = series.reduce((a, b) => a + (b?.[dataPointIndex] || 0), 0);
|
const total = series.reduce((a, b) => a + (b?.[dataPointIndex] || 0), 0)
|
||||||
const percentValue = (100 * series[seriesIndex][dataPointIndex]) / total;
|
const percentValue = (100 * series[seriesIndex][dataPointIndex]) / total
|
||||||
tooltip += `<div class="value">${props.prefix}${formatNumber(percentValue)}%${
|
tooltip += `<div class="value">${props.prefix}${formatNumber(percentValue)}%${
|
||||||
props.suffix
|
props.suffix
|
||||||
}</div>`;
|
}</div>`
|
||||||
} else {
|
} 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)}${
|
tooltip += `<div class="value">${props.prefix}${formatTooltipValue(totalValue, props)}${
|
||||||
props.suffix
|
props.suffix
|
||||||
}</div>`;
|
}</div>`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltip += '</div><hr class="card-divider" />';
|
tooltip += '</div><hr class="card-divider" />'
|
||||||
|
|
||||||
// Logic for generating list entries
|
// Logic for generating list entries
|
||||||
if (props.percentStacked) {
|
if (props.percentStacked) {
|
||||||
@ -131,9 +131,9 @@ function generateTooltip({ series, seriesIndex, dataPointIndex, w }, props) {
|
|||||||
seriesIndex,
|
seriesIndex,
|
||||||
w,
|
w,
|
||||||
props,
|
props,
|
||||||
);
|
)
|
||||||
} else {
|
} else {
|
||||||
const returnTopN = 15;
|
const returnTopN = 15
|
||||||
|
|
||||||
const listEntries = series
|
const listEntries = series
|
||||||
.map((value, index) => [
|
.map((value, index) => [
|
||||||
@ -144,13 +144,13 @@ function generateTooltip({ series, seriesIndex, dataPointIndex, w }, props) {
|
|||||||
.sort((a, b) => b[0] - a[0])
|
.sort((a, b) => b[0] - a[0])
|
||||||
.slice(0, returnTopN) // Return only the top X entries
|
.slice(0, returnTopN) // Return only the top X entries
|
||||||
.map((value) => value[1])
|
.map((value) => value[1])
|
||||||
.join("");
|
.join('')
|
||||||
|
|
||||||
tooltip += listEntries;
|
tooltip += listEntries
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltip += "</div>";
|
tooltip += '</div>'
|
||||||
return tooltip;
|
return tooltip
|
||||||
}
|
}
|
||||||
|
|
||||||
const chartOptions = computed(() => {
|
const chartOptions = computed(() => {
|
||||||
@ -158,19 +158,19 @@ const chartOptions = computed(() => {
|
|||||||
chart: {
|
chart: {
|
||||||
id: props.name,
|
id: props.name,
|
||||||
fontFamily:
|
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',
|
||||||
foreColor: "var(--color-base)",
|
foreColor: 'var(--color-base)',
|
||||||
selection: {
|
selection: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
fill: {
|
fill: {
|
||||||
color: "var(--color-brand)",
|
color: 'var(--color-brand)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
toolbar: {
|
toolbar: {
|
||||||
show: false,
|
show: false,
|
||||||
},
|
},
|
||||||
stacked: props.stacked,
|
stacked: props.stacked,
|
||||||
stackType: props.percentStacked ? "100%" : "normal",
|
stackType: props.percentStacked ? '100%' : 'normal',
|
||||||
zoom: {
|
zoom: {
|
||||||
autoScaleYaxis: true,
|
autoScaleYaxis: true,
|
||||||
},
|
},
|
||||||
@ -183,7 +183,7 @@ const chartOptions = computed(() => {
|
|||||||
categories: props.labels,
|
categories: props.labels,
|
||||||
labels: {
|
labels: {
|
||||||
style: {
|
style: {
|
||||||
borderRadius: "var(--radius-sm)",
|
borderRadius: 'var(--radius-sm)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
axisTicks: {
|
axisTicks: {
|
||||||
@ -207,8 +207,8 @@ const chartOptions = computed(() => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
borderColor: "var(--color-button-bg)",
|
borderColor: 'var(--color-button-bg)',
|
||||||
tickColor: "var(--color-button-bg)",
|
tickColor: 'var(--color-button-bg)',
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
show: !props.hideLegend,
|
show: !props.hideLegend,
|
||||||
@ -216,16 +216,16 @@ const chartOptions = computed(() => {
|
|||||||
showForZeroSeries: false,
|
showForZeroSeries: false,
|
||||||
showForSingleSeries: false,
|
showForSingleSeries: false,
|
||||||
showForNullSeries: false,
|
showForNullSeries: false,
|
||||||
fontSize: "var(--font-size-nm)",
|
fontSize: 'var(--font-size-nm)',
|
||||||
fontFamily:
|
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: {
|
onItemClick: {
|
||||||
toggleDataSeries: true,
|
toggleDataSeries: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
markers: {
|
markers: {
|
||||||
size: 0,
|
size: 0,
|
||||||
strokeColor: "var(--color-contrast)",
|
strokeColor: 'var(--color-contrast)',
|
||||||
strokeWidth: 3,
|
strokeWidth: 3,
|
||||||
strokeOpacity: 1,
|
strokeOpacity: 1,
|
||||||
fillOpacity: 1,
|
fillOpacity: 1,
|
||||||
@ -236,29 +236,29 @@ const chartOptions = computed(() => {
|
|||||||
plotOptions: {
|
plotOptions: {
|
||||||
bar: {
|
bar: {
|
||||||
horizontal: props.horizontalBar,
|
horizontal: props.horizontalBar,
|
||||||
columnWidth: "80%",
|
columnWidth: '80%',
|
||||||
endingShape: "rounded",
|
endingShape: 'rounded',
|
||||||
borderRadius: 5,
|
borderRadius: 5,
|
||||||
borderRadiusApplication: "end",
|
borderRadiusApplication: 'end',
|
||||||
borderRadiusWhenStacked: "last",
|
borderRadiusWhenStacked: 'last',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
stroke: {
|
stroke: {
|
||||||
curve: "smooth",
|
curve: 'smooth',
|
||||||
width: 2,
|
width: 2,
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
custom: (d) => generateTooltip(d, props),
|
custom: (d) => generateTooltip(d, props),
|
||||||
},
|
},
|
||||||
fill:
|
fill:
|
||||||
props.type === "area"
|
props.type === 'area'
|
||||||
? {
|
? {
|
||||||
colors: props.colors,
|
colors: props.colors,
|
||||||
type: "gradient",
|
type: 'gradient',
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
gradient: {
|
gradient: {
|
||||||
shade: "light",
|
shade: 'light',
|
||||||
type: "vertical",
|
type: 'vertical',
|
||||||
shadeIntensity: 0,
|
shadeIntensity: 0,
|
||||||
gradientToColors: props.colors,
|
gradientToColors: props.colors,
|
||||||
inverseColors: true,
|
inverseColors: true,
|
||||||
@ -269,40 +269,40 @@ const chartOptions = computed(() => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {},
|
: {},
|
||||||
};
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
const chart = ref(null);
|
const chart = ref(null)
|
||||||
|
|
||||||
const legendValues = ref(
|
const legendValues = ref(
|
||||||
[...props.data].map((project, index) => {
|
[...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) => {
|
const flipLegend = (legend, newVal) => {
|
||||||
legend.visible = newVal;
|
legend.visible = newVal
|
||||||
chart.value.toggleSeries(legend.name);
|
chart.value.toggleSeries(legend.name)
|
||||||
};
|
}
|
||||||
|
|
||||||
const resetChart = () => {
|
const resetChart = () => {
|
||||||
if (!chart.value) return;
|
if (!chart.value) return
|
||||||
chart.value.updateSeries([...props.data]);
|
chart.value.updateSeries([...props.data])
|
||||||
chart.value.updateOptions({
|
chart.value.updateOptions({
|
||||||
xaxis: {
|
xaxis: {
|
||||||
categories: props.labels,
|
categories: props.labels,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
chart.value.resetSeries();
|
chart.value.resetSeries()
|
||||||
legendValues.value.forEach((legend) => {
|
legendValues.value.forEach((legend) => {
|
||||||
legend.visible = true;
|
legend.visible = true
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
resetChart,
|
resetChart,
|
||||||
flipLegend,
|
flipLegend,
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@ -304,28 +304,28 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DownloadIcon,UpdatedIcon } from "@modrinth/assets";
|
import { DownloadIcon, UpdatedIcon } from '@modrinth/assets'
|
||||||
import { Button, Card, DropdownSelect } from "@modrinth/ui";
|
import { Button, Card, DropdownSelect } from '@modrinth/ui'
|
||||||
import { formatCategoryHeader,formatMoney, formatNumber } from "@modrinth/utils";
|
import { formatCategoryHeader, formatMoney, formatNumber } from '@modrinth/utils'
|
||||||
import dayjs from "dayjs";
|
import dayjs from 'dayjs'
|
||||||
import { computed } from "vue";
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import { UiChartsChart as Chart,UiChartsCompactChart as CompactChart } from "#components";
|
import { UiChartsChart as Chart, UiChartsCompactChart as CompactChart } from '#components'
|
||||||
import PaletteIcon from "~/assets/icons/palette.svg?component";
|
import PaletteIcon from '~/assets/icons/palette.svg?component'
|
||||||
import { analyticsSetToCSVString, intToRgba } from "~/utils/analytics.js";
|
import { analyticsSetToCSVString, intToRgba } from '~/utils/analytics.js'
|
||||||
|
|
||||||
const router = useNativeRouter();
|
const router = useNativeRouter()
|
||||||
const theme = useTheme();
|
const theme = useTheme()
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
projects?: any[];
|
projects?: any[]
|
||||||
/**
|
/**
|
||||||
* @deprecated Use `ranges` instead
|
* @deprecated Use `ranges` instead
|
||||||
*/
|
*/
|
||||||
resoloutions?: Record<string, number>;
|
resoloutions?: Record<string, number>
|
||||||
ranges?: RangeObject[];
|
ranges?: RangeObject[]
|
||||||
personal?: boolean;
|
personal?: boolean
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
projects: undefined,
|
projects: undefined,
|
||||||
@ -333,19 +333,19 @@ const props = withDefaults(
|
|||||||
ranges: () => defaultRanges,
|
ranges: () => defaultRanges,
|
||||||
personal: false,
|
personal: false,
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
const projects = ref(props.projects || []);
|
const projects = ref(props.projects || [])
|
||||||
|
|
||||||
// const selectedChart = ref('downloads')
|
// const selectedChart = ref('downloads')
|
||||||
const selectedChart = computed({
|
const selectedChart = computed({
|
||||||
get: () => {
|
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 the id is anything but the 3 charts we have or undefined, throw an error
|
||||||
if (!["downloads", "views", "revenue"].includes(id)) {
|
if (!['downloads', 'views', 'revenue'].includes(id)) {
|
||||||
throw new Error(`Unknown chart ${id}`);
|
throw new Error(`Unknown chart ${id}`)
|
||||||
}
|
}
|
||||||
return id;
|
return id
|
||||||
},
|
},
|
||||||
set: (chart) => {
|
set: (chart) => {
|
||||||
router.push({
|
router.push({
|
||||||
@ -353,119 +353,119 @@ const selectedChart = computed({
|
|||||||
...router.currentRoute.value.query,
|
...router.currentRoute.value.query,
|
||||||
chart,
|
chart,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
// Chart refs
|
// Chart refs
|
||||||
const downloadsChart = ref();
|
const downloadsChart = ref()
|
||||||
const viewsChart = ref();
|
const viewsChart = ref()
|
||||||
const revenueChart = ref();
|
const revenueChart = ref()
|
||||||
const tinyDownloadChart = ref();
|
const tinyDownloadChart = ref()
|
||||||
const tinyViewChart = ref();
|
const tinyViewChart = ref()
|
||||||
const tinyRevenueChart = ref();
|
const tinyRevenueChart = ref()
|
||||||
|
|
||||||
const selectedDisplayProjects = ref(props.projects || []);
|
const selectedDisplayProjects = ref(props.projects || [])
|
||||||
|
|
||||||
const removeProjectFromDisplay = (id: string) => {
|
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) => {
|
const addProjectToDisplay = (id: string) => {
|
||||||
selectedDisplayProjects.value = [
|
selectedDisplayProjects.value = [
|
||||||
...selectedDisplayProjects.value,
|
...selectedDisplayProjects.value,
|
||||||
props.projects?.find((p) => p.id === id),
|
props.projects?.find((p) => p.id === id),
|
||||||
].filter(Boolean);
|
].filter(Boolean)
|
||||||
};
|
}
|
||||||
|
|
||||||
const projectIsOnDisplay = (id: string) => {
|
const projectIsOnDisplay = (id: string) => {
|
||||||
return selectedDisplayProjects.value?.some((p) => p.id === id) ?? false;
|
return selectedDisplayProjects.value?.some((p) => p.id === id) ?? false
|
||||||
};
|
}
|
||||||
|
|
||||||
const resetCharts = () => {
|
const resetCharts = () => {
|
||||||
downloadsChart.value?.resetChart();
|
downloadsChart.value?.resetChart()
|
||||||
viewsChart.value?.resetChart();
|
viewsChart.value?.resetChart()
|
||||||
revenueChart.value?.resetChart();
|
revenueChart.value?.resetChart()
|
||||||
|
|
||||||
tinyDownloadChart.value?.resetChart();
|
tinyDownloadChart.value?.resetChart()
|
||||||
tinyViewChart.value?.resetChart();
|
tinyViewChart.value?.resetChart()
|
||||||
tinyRevenueChart.value?.resetChart();
|
tinyRevenueChart.value?.resetChart()
|
||||||
};
|
}
|
||||||
|
|
||||||
const isUsingProjectColors = computed({
|
const isUsingProjectColors = computed({
|
||||||
get: () => {
|
get: () => {
|
||||||
return (
|
return (
|
||||||
router.currentRoute.value.query?.colors === "true" ||
|
router.currentRoute.value.query?.colors === 'true' ||
|
||||||
router.currentRoute.value.query?.colors === undefined
|
router.currentRoute.value.query?.colors === undefined
|
||||||
);
|
)
|
||||||
},
|
},
|
||||||
set: (newValue) => {
|
set: (newValue) => {
|
||||||
router.push({
|
router.push({
|
||||||
query: {
|
query: {
|
||||||
...router.currentRoute.value.query,
|
...router.currentRoute.value.query,
|
||||||
colors: newValue ? "true" : "false",
|
colors: newValue ? 'true' : 'false',
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const startDate = ref(dayjs().startOf("day"));
|
const startDate = ref(dayjs().startOf('day'))
|
||||||
const endDate = ref(dayjs().endOf("day"));
|
const endDate = ref(dayjs().endOf('day'))
|
||||||
const timeResolution = ref(30);
|
const timeResolution = ref(30)
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
// Load cached data and range from localStorage - cache.
|
// Load cached data and range from localStorage - cache.
|
||||||
if (import.meta.client) {
|
if (import.meta.client) {
|
||||||
const rangeLabel = localStorage.getItem("analyticsSelectedRange");
|
const rangeLabel = localStorage.getItem('analyticsSelectedRange')
|
||||||
if (rangeLabel) {
|
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) {
|
if (range !== undefined) {
|
||||||
internalRange.value = range;
|
internalRange.value = range
|
||||||
const ranges = range.getDates(dayjs());
|
const ranges = range.getDates(dayjs())
|
||||||
timeResolution.value = range.timeResolution;
|
timeResolution.value = range.timeResolution
|
||||||
startDate.value = ranges.startDate;
|
startDate.value = ranges.startDate
|
||||||
endDate.value = ranges.endDate;
|
endDate.value = ranges.endDate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (internalRange.value === null) {
|
if (internalRange.value === null) {
|
||||||
internalRange.value = props.ranges.find(
|
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());
|
const ranges = selectedRange.value.getDates(dayjs())
|
||||||
startDate.value = ranges.startDate;
|
startDate.value = ranges.startDate
|
||||||
endDate.value = ranges.endDate;
|
endDate.value = ranges.endDate
|
||||||
timeResolution.value = selectedRange.value.timeResolution;
|
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({
|
const selectedRange = computed({
|
||||||
get: () => {
|
get: () => {
|
||||||
return internalRange.value;
|
return internalRange.value
|
||||||
},
|
},
|
||||||
set: (newRange) => {
|
set: (newRange) => {
|
||||||
const ranges = newRange.getDates(dayjs());
|
const ranges = newRange.getDates(dayjs())
|
||||||
startDate.value = ranges.startDate;
|
startDate.value = ranges.startDate
|
||||||
endDate.value = ranges.endDate;
|
endDate.value = ranges.endDate
|
||||||
timeResolution.value = newRange.timeResolution;
|
timeResolution.value = newRange.timeResolution
|
||||||
|
|
||||||
internalRange.value = newRange;
|
internalRange.value = newRange
|
||||||
|
|
||||||
if (import.meta.client) {
|
if (import.meta.client) {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"analyticsSelectedRange",
|
'analyticsSelectedRange',
|
||||||
internalRange.value?.getLabel([dayjs(), dayjs()]) ?? "Previous 30 days",
|
internalRange.value?.getLabel([dayjs(), dayjs()]) ?? 'Previous 30 days',
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const analytics = useFetchAllAnalytics(
|
const analytics = useFetchAllAnalytics(
|
||||||
resetCharts,
|
resetCharts,
|
||||||
@ -475,53 +475,53 @@ const analytics = useFetchAllAnalytics(
|
|||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
timeResolution,
|
timeResolution,
|
||||||
);
|
)
|
||||||
|
|
||||||
const formattedCategorySubtitle = computed(() => {
|
const formattedCategorySubtitle = computed(() => {
|
||||||
return (
|
return (
|
||||||
selectedRange.value?.getLabel([dayjs(startDate.value), dayjs(endDate.value)]) ?? "Loading..."
|
selectedRange.value?.getLabel([dayjs(startDate.value), dayjs(endDate.value)]) ?? 'Loading...'
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
|
|
||||||
const selectedDataSet = computed(() => {
|
const selectedDataSet = computed(() => {
|
||||||
switch (selectedChart.value) {
|
switch (selectedChart.value) {
|
||||||
case "downloads":
|
case 'downloads':
|
||||||
return analytics.totalData.value.downloads;
|
return analytics.totalData.value.downloads
|
||||||
case "views":
|
case 'views':
|
||||||
return analytics.totalData.value.views;
|
return analytics.totalData.value.views
|
||||||
case "revenue":
|
case 'revenue':
|
||||||
return analytics.totalData.value.revenue;
|
return analytics.totalData.value.revenue
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown chart ${selectedChart.value}`);
|
throw new Error(`Unknown chart ${selectedChart.value}`)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
const selectedDataSetProjects = computed(() => {
|
const selectedDataSetProjects = computed(() => {
|
||||||
return selectedDataSet.value.projectIds
|
return selectedDataSet.value.projectIds
|
||||||
.map((id) => props.projects?.find((p) => p?.id === id))
|
.map((id) => props.projects?.find((p) => p?.id === id))
|
||||||
.filter(Boolean);
|
.filter(Boolean)
|
||||||
});
|
})
|
||||||
|
|
||||||
const downloadSelectedSetAsCSV = () => {
|
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 link = document.createElement('a')
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob)
|
||||||
link.setAttribute("href", url);
|
link.setAttribute('href', url)
|
||||||
link.setAttribute("download", `${selectedChartName}-data.csv`);
|
link.setAttribute('download', `${selectedChartName}-data.csv`)
|
||||||
link.style.visibility = "hidden";
|
link.style.visibility = 'hidden'
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link)
|
||||||
|
|
||||||
link.click();
|
link.click()
|
||||||
};
|
}
|
||||||
|
|
||||||
const onDownloadSetAsCSV = useClientTry(async () => await downloadSelectedSetAsCSV());
|
const onDownloadSetAsCSV = useClientTry(async () => await downloadSelectedSetAsCSV())
|
||||||
const onToggleColors = () => {
|
const onToggleColors = () => {
|
||||||
isUsingProjectColors.value = !isUsingProjectColors.value;
|
isUsingProjectColors.value = !isUsingProjectColors.value
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@ -529,177 +529,177 @@ const onToggleColors = () => {
|
|||||||
* @deprecated Use `ranges` instead
|
* @deprecated Use `ranges` instead
|
||||||
*/
|
*/
|
||||||
const defaultResoloutions: Record<string, number> = {
|
const defaultResoloutions: Record<string, number> = {
|
||||||
"5 minutes": 5,
|
'5 minutes': 5,
|
||||||
"30 minutes": 30,
|
'30 minutes': 30,
|
||||||
"An hour": 60,
|
'An hour': 60,
|
||||||
"12 hours": 720,
|
'12 hours': 720,
|
||||||
"A day": 1440,
|
'A day': 1440,
|
||||||
"A week": 10080,
|
'A week': 10080,
|
||||||
};
|
}
|
||||||
|
|
||||||
type DateRange = { startDate: dayjs.Dayjs; endDate: dayjs.Dayjs };
|
type DateRange = { startDate: dayjs.Dayjs; endDate: dayjs.Dayjs }
|
||||||
|
|
||||||
type RangeObject = {
|
type RangeObject = {
|
||||||
getLabel: (dateRange: [dayjs.Dayjs, dayjs.Dayjs]) => string;
|
getLabel: (dateRange: [dayjs.Dayjs, dayjs.Dayjs]) => string
|
||||||
getDates: (currentDate: dayjs.Dayjs) => DateRange;
|
getDates: (currentDate: dayjs.Dayjs) => DateRange
|
||||||
// A time resolution in minutes.
|
// A time resolution in minutes.
|
||||||
timeResolution: number;
|
timeResolution: number
|
||||||
};
|
}
|
||||||
|
|
||||||
const defaultRanges: RangeObject[] = [
|
const defaultRanges: RangeObject[] = [
|
||||||
{
|
{
|
||||||
getLabel: () => "Previous 30 minutes",
|
getLabel: () => 'Previous 30 minutes',
|
||||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||||
startDate: dayjs(currentDate).subtract(30, "minute"),
|
startDate: dayjs(currentDate).subtract(30, 'minute'),
|
||||||
endDate: currentDate,
|
endDate: currentDate,
|
||||||
}),
|
}),
|
||||||
timeResolution: 1,
|
timeResolution: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
getLabel: () => "Previous hour",
|
getLabel: () => 'Previous hour',
|
||||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||||
startDate: dayjs(currentDate).subtract(1, "hour"),
|
startDate: dayjs(currentDate).subtract(1, 'hour'),
|
||||||
endDate: currentDate,
|
endDate: currentDate,
|
||||||
}),
|
}),
|
||||||
timeResolution: 5,
|
timeResolution: 5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
getLabel: () => "Previous 12 hours",
|
getLabel: () => 'Previous 12 hours',
|
||||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||||
startDate: dayjs(currentDate).subtract(12, "hour"),
|
startDate: dayjs(currentDate).subtract(12, 'hour'),
|
||||||
endDate: currentDate,
|
endDate: currentDate,
|
||||||
}),
|
}),
|
||||||
timeResolution: 12,
|
timeResolution: 12,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
getLabel: () => "Previous 24 hours",
|
getLabel: () => 'Previous 24 hours',
|
||||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||||
startDate: dayjs(currentDate).subtract(1, "day"),
|
startDate: dayjs(currentDate).subtract(1, 'day'),
|
||||||
endDate: currentDate,
|
endDate: currentDate,
|
||||||
}),
|
}),
|
||||||
timeResolution: 30,
|
timeResolution: 30,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
getLabel: () => "Today",
|
getLabel: () => 'Today',
|
||||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||||
startDate: dayjs(currentDate).startOf("day"),
|
startDate: dayjs(currentDate).startOf('day'),
|
||||||
endDate: currentDate,
|
endDate: currentDate,
|
||||||
}),
|
}),
|
||||||
timeResolution: 30,
|
timeResolution: 30,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
getLabel: () => "Yesterday",
|
getLabel: () => 'Yesterday',
|
||||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||||
startDate: dayjs(currentDate).subtract(1, "day").startOf("day"),
|
startDate: dayjs(currentDate).subtract(1, 'day').startOf('day'),
|
||||||
endDate: dayjs(currentDate).startOf("day").subtract(1, "second"),
|
endDate: dayjs(currentDate).startOf('day').subtract(1, 'second'),
|
||||||
}),
|
}),
|
||||||
timeResolution: 30,
|
timeResolution: 30,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
getLabel: () => "This week",
|
getLabel: () => 'This week',
|
||||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||||
startDate: dayjs(currentDate).startOf("week").add(1, "hour"),
|
startDate: dayjs(currentDate).startOf('week').add(1, 'hour'),
|
||||||
endDate: currentDate,
|
endDate: currentDate,
|
||||||
}),
|
}),
|
||||||
timeResolution: 360,
|
timeResolution: 360,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
getLabel: () => "Last week",
|
getLabel: () => 'Last week',
|
||||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||||
startDate: dayjs(currentDate).subtract(1, "week").startOf("week").add(1, "hour"),
|
startDate: dayjs(currentDate).subtract(1, 'week').startOf('week').add(1, 'hour'),
|
||||||
endDate: dayjs(currentDate).startOf("week").subtract(1, "second"),
|
endDate: dayjs(currentDate).startOf('week').subtract(1, 'second'),
|
||||||
}),
|
}),
|
||||||
timeResolution: 1440,
|
timeResolution: 1440,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
getLabel: () => "Previous 7 days",
|
getLabel: () => 'Previous 7 days',
|
||||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||||
startDate: dayjs(currentDate).startOf("day").subtract(7, "day").add(1, "hour"),
|
startDate: dayjs(currentDate).startOf('day').subtract(7, 'day').add(1, 'hour'),
|
||||||
endDate: currentDate.startOf("day"),
|
endDate: currentDate.startOf('day'),
|
||||||
}),
|
}),
|
||||||
timeResolution: 720,
|
timeResolution: 720,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
getLabel: () => "This month",
|
getLabel: () => 'This month',
|
||||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||||
startDate: dayjs(currentDate).startOf("month").add(1, "hour"),
|
startDate: dayjs(currentDate).startOf('month').add(1, 'hour'),
|
||||||
endDate: currentDate,
|
endDate: currentDate,
|
||||||
}),
|
}),
|
||||||
timeResolution: 1440,
|
timeResolution: 1440,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
getLabel: () => "Last month",
|
getLabel: () => 'Last month',
|
||||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||||
startDate: dayjs(currentDate).subtract(1, "month").startOf("month").add(1, "hour"),
|
startDate: dayjs(currentDate).subtract(1, 'month').startOf('month').add(1, 'hour'),
|
||||||
endDate: dayjs(currentDate).startOf("month").subtract(1, "second"),
|
endDate: dayjs(currentDate).startOf('month').subtract(1, 'second'),
|
||||||
}),
|
}),
|
||||||
timeResolution: 1440,
|
timeResolution: 1440,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
getLabel: () => "Previous 30 days",
|
getLabel: () => 'Previous 30 days',
|
||||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||||
startDate: dayjs(currentDate).startOf("day").subtract(30, "day").add(1, "hour"),
|
startDate: dayjs(currentDate).startOf('day').subtract(30, 'day').add(1, 'hour'),
|
||||||
endDate: currentDate.startOf("day"),
|
endDate: currentDate.startOf('day'),
|
||||||
}),
|
}),
|
||||||
timeResolution: 1440,
|
timeResolution: 1440,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
getLabel: () => "This quarter",
|
getLabel: () => 'This quarter',
|
||||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||||
startDate: dayjs(currentDate).startOf("quarter").add(1, "hour"),
|
startDate: dayjs(currentDate).startOf('quarter').add(1, 'hour'),
|
||||||
endDate: currentDate,
|
endDate: currentDate,
|
||||||
}),
|
}),
|
||||||
timeResolution: 1440,
|
timeResolution: 1440,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
getLabel: () => "Last quarter",
|
getLabel: () => 'Last quarter',
|
||||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||||
startDate: dayjs(currentDate).subtract(1, "quarter").startOf("quarter").add(1, "hour"),
|
startDate: dayjs(currentDate).subtract(1, 'quarter').startOf('quarter').add(1, 'hour'),
|
||||||
endDate: dayjs(currentDate).startOf("quarter").subtract(1, "second"),
|
endDate: dayjs(currentDate).startOf('quarter').subtract(1, 'second'),
|
||||||
}),
|
}),
|
||||||
timeResolution: 1440,
|
timeResolution: 1440,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
getLabel: () => "This year",
|
getLabel: () => 'This year',
|
||||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||||
startDate: dayjs(currentDate).startOf("year"),
|
startDate: dayjs(currentDate).startOf('year'),
|
||||||
endDate: currentDate,
|
endDate: currentDate,
|
||||||
}),
|
}),
|
||||||
timeResolution: 20160,
|
timeResolution: 20160,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
getLabel: () => "Last year",
|
getLabel: () => 'Last year',
|
||||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||||
startDate: dayjs(currentDate).subtract(1, "year").startOf("year"),
|
startDate: dayjs(currentDate).subtract(1, 'year').startOf('year'),
|
||||||
endDate: dayjs(currentDate).startOf("year").subtract(1, "second"),
|
endDate: dayjs(currentDate).startOf('year').subtract(1, 'second'),
|
||||||
}),
|
}),
|
||||||
timeResolution: 20160,
|
timeResolution: 20160,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
getLabel: () => "Previous year",
|
getLabel: () => 'Previous year',
|
||||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||||
startDate: dayjs(currentDate).subtract(1, "year"),
|
startDate: dayjs(currentDate).subtract(1, 'year'),
|
||||||
endDate: dayjs(currentDate),
|
endDate: dayjs(currentDate),
|
||||||
}),
|
}),
|
||||||
timeResolution: 40320,
|
timeResolution: 40320,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
getLabel: () => "Previous two years",
|
getLabel: () => 'Previous two years',
|
||||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||||
startDate: dayjs(currentDate).subtract(2, "year"),
|
startDate: dayjs(currentDate).subtract(2, 'year'),
|
||||||
endDate: currentDate,
|
endDate: currentDate,
|
||||||
}),
|
}),
|
||||||
timeResolution: 40320,
|
timeResolution: 40320,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
getLabel: () => "All Time",
|
getLabel: () => 'All Time',
|
||||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||||
startDate: dayjs(0),
|
startDate: dayjs(0),
|
||||||
endDate: currentDate,
|
endDate: currentDate,
|
||||||
}),
|
}),
|
||||||
timeResolution: 40320,
|
timeResolution: 40320,
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@ -885,7 +885,7 @@ const defaultRanges: RangeObject[] = [
|
|||||||
|
|
||||||
.country-value {
|
.country-value {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas: "flag text bar";
|
grid-template-areas: 'flag text bar';
|
||||||
grid-template-columns: auto 1fr 10rem;
|
grid-template-columns: auto 1fr 10rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { Card } from "@modrinth/ui";
|
import { Card } from '@modrinth/ui'
|
||||||
import VueApexCharts from "vue3-apexcharts";
|
import VueApexCharts from 'vue3-apexcharts'
|
||||||
|
|
||||||
// let VueApexCharts
|
// let VueApexCharts
|
||||||
// if (import.meta.client) {
|
// if (import.meta.client) {
|
||||||
@ -10,11 +10,11 @@ import VueApexCharts from "vue3-apexcharts";
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
value: {
|
value: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: '',
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: '',
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@ -26,11 +26,11 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
prefix: {
|
prefix: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: '',
|
||||||
},
|
},
|
||||||
suffix: {
|
suffix: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: '',
|
||||||
},
|
},
|
||||||
isMoney: {
|
isMoney: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -38,17 +38,17 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
color: {
|
color: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "var(--color-brand)",
|
default: 'var(--color-brand)',
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
// no grid lines, no toolbar, no legend, no data labels
|
// no grid lines, no toolbar, no legend, no data labels
|
||||||
const chartOptions = {
|
const chartOptions = {
|
||||||
chart: {
|
chart: {
|
||||||
id: props.title,
|
id: props.title,
|
||||||
fontFamily:
|
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',
|
||||||
foreColor: "var(--color-base)",
|
foreColor: 'var(--color-base)',
|
||||||
toolbar: {
|
toolbar: {
|
||||||
show: false,
|
show: false,
|
||||||
},
|
},
|
||||||
@ -61,16 +61,16 @@ const chartOptions = {
|
|||||||
parentHeightOffset: 0,
|
parentHeightOffset: 0,
|
||||||
},
|
},
|
||||||
stroke: {
|
stroke: {
|
||||||
curve: "smooth",
|
curve: 'smooth',
|
||||||
width: 2,
|
width: 2,
|
||||||
},
|
},
|
||||||
fill: {
|
fill: {
|
||||||
colors: [props.color],
|
colors: [props.color],
|
||||||
type: "gradient",
|
type: 'gradient',
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
gradient: {
|
gradient: {
|
||||||
shade: "light",
|
shade: 'light',
|
||||||
type: "vertical",
|
type: 'vertical',
|
||||||
shadeIntensity: 0,
|
shadeIntensity: 0,
|
||||||
gradientToColors: [props.color],
|
gradientToColors: [props.color],
|
||||||
inverseColors: true,
|
inverseColors: true,
|
||||||
@ -91,7 +91,7 @@ const chartOptions = {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
xaxis: {
|
xaxis: {
|
||||||
type: "datetime",
|
type: 'datetime',
|
||||||
categories: props.labels,
|
categories: props.labels,
|
||||||
labels: {
|
labels: {
|
||||||
show: false,
|
show: false,
|
||||||
@ -120,23 +120,23 @@ const chartOptions = {
|
|||||||
tooltip: {
|
tooltip: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
const chart = ref(null);
|
const chart = ref(null)
|
||||||
|
|
||||||
const resetChart = () => {
|
const resetChart = () => {
|
||||||
chart.value?.updateSeries([...props.data]);
|
chart.value?.updateSeries([...props.data])
|
||||||
chart.value?.updateOptions({
|
chart.value?.updateOptions({
|
||||||
xaxis: {
|
xaxis: {
|
||||||
categories: props.labels,
|
categories: props.labels,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
chart.value?.resetSeries();
|
chart.value?.resetSeries()
|
||||||
};
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
resetChart,
|
resetChart,
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@ -47,7 +47,7 @@
|
|||||||
<span class="max-w-[200px] truncate font-mono text-xs sm:max-w-none">
|
<span class="max-w-[200px] truncate font-mono text-xs sm:max-w-none">
|
||||||
{{
|
{{
|
||||||
report.version.files.find((file) => file.primary)?.filename ||
|
report.version.files.find((file) => file.primary)?.filename ||
|
||||||
"Unknown primary file"
|
'Unknown primary file'
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -124,56 +124,56 @@ import {
|
|||||||
EyeIcon,
|
EyeIcon,
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
OrganizationIcon,
|
OrganizationIcon,
|
||||||
} from "@modrinth/assets";
|
} from '@modrinth/assets'
|
||||||
import type { ExtendedDelphiReport } from "@modrinth/moderation";
|
import type { ExtendedDelphiReport } from '@modrinth/moderation'
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
OverflowMenu,
|
OverflowMenu,
|
||||||
type OverflowMenuOption,
|
type OverflowMenuOption,
|
||||||
useRelativeTime,
|
useRelativeTime,
|
||||||
} from "@modrinth/ui";
|
} from '@modrinth/ui'
|
||||||
import dayjs from "dayjs";
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
report: ExtendedDelphiReport;
|
report: ExtendedDelphiReport
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const formatRelativeTime = useRelativeTime();
|
const formatRelativeTime = useRelativeTime()
|
||||||
const isPending = computed(() => props.report.status === "pending");
|
const isPending = computed(() => props.report.status === 'pending')
|
||||||
|
|
||||||
const quickActions: OverflowMenuOption[] = [
|
const quickActions: OverflowMenuOption[] = [
|
||||||
{
|
{
|
||||||
id: "copy-link",
|
id: 'copy-link',
|
||||||
action: () => {
|
action: () => {
|
||||||
const base = window.location.origin;
|
const base = window.location.origin
|
||||||
const reviewUrl = `${base}/moderation/tech-reviews?q=${props.report.version.id}`;
|
const reviewUrl = `${base}/moderation/tech-reviews?q=${props.report.version.id}`
|
||||||
navigator.clipboard.writeText(reviewUrl).then(() => {
|
navigator.clipboard.writeText(reviewUrl).then(() => {
|
||||||
addNotification({
|
addNotification({
|
||||||
type: "success",
|
type: 'success',
|
||||||
title: "Tech review link copied",
|
title: 'Tech review link copied',
|
||||||
text: "The link to this tech review has been copied to your clipboard.",
|
text: 'The link to this tech review has been copied to your clipboard.',
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "copy-id",
|
id: 'copy-id',
|
||||||
action: () => {
|
action: () => {
|
||||||
navigator.clipboard.writeText(props.report.version.id).then(() => {
|
navigator.clipboard.writeText(props.report.version.id).then(() => {
|
||||||
addNotification({
|
addNotification({
|
||||||
type: "success",
|
type: 'success',
|
||||||
title: "Version ID copied",
|
title: 'Version ID copied',
|
||||||
text: "The ID of this version has been copied to your clipboard.",
|
text: 'The ID of this version has been copied to your clipboard.',
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
|
|
||||||
const versionUrl = computed(() => {
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
|
|||||||
@ -29,56 +29,56 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type KeybindListener, keybinds, normalizeKeybind } from "@modrinth/moderation";
|
import { type KeybindListener, keybinds, normalizeKeybind } from '@modrinth/moderation'
|
||||||
import NewModal from "@modrinth/ui/src/components/modal/NewModal.vue";
|
import NewModal from '@modrinth/ui/src/components/modal/NewModal.vue'
|
||||||
import { ref } from "vue";
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const modal = ref<InstanceType<typeof NewModal>>();
|
const modal = ref<InstanceType<typeof NewModal>>()
|
||||||
|
|
||||||
function parseKeybindDisplay(keybind: KeybindListener["keybind"]): string[] {
|
function parseKeybindDisplay(keybind: KeybindListener['keybind']): string[] {
|
||||||
const keybinds = Array.isArray(keybind) ? keybind : [keybind];
|
const keybinds = Array.isArray(keybind) ? keybind : [keybind]
|
||||||
const normalized = keybinds[0];
|
const normalized = keybinds[0]
|
||||||
const def = normalizeKeybind(normalized);
|
const def = normalizeKeybind(normalized)
|
||||||
|
|
||||||
const keys = [];
|
const keys = []
|
||||||
|
|
||||||
if (def.ctrl || def.meta) {
|
if (def.ctrl || def.meta) {
|
||||||
keys.push(isMac() ? "CMD" : "CTRL");
|
keys.push(isMac() ? 'CMD' : 'CTRL')
|
||||||
}
|
}
|
||||||
if (def.shift) keys.push("SHIFT");
|
if (def.shift) keys.push('SHIFT')
|
||||||
if (def.alt) keys.push("ALT");
|
if (def.alt) keys.push('ALT')
|
||||||
|
|
||||||
const mainKey = def.key
|
const mainKey = def.key
|
||||||
.replace("ArrowLeft", "←")
|
.replace('ArrowLeft', '←')
|
||||||
.replace("ArrowRight", "→")
|
.replace('ArrowRight', '→')
|
||||||
.replace("ArrowUp", "↑")
|
.replace('ArrowUp', '↑')
|
||||||
.replace("ArrowDown", "↓")
|
.replace('ArrowDown', '↓')
|
||||||
.replace("Enter", "↵")
|
.replace('Enter', '↵')
|
||||||
.replace("Space", "SPACE")
|
.replace('Space', 'SPACE')
|
||||||
.replace("Escape", "ESC")
|
.replace('Escape', 'ESC')
|
||||||
.toUpperCase();
|
.toUpperCase()
|
||||||
|
|
||||||
keys.push(mainKey);
|
keys.push(mainKey)
|
||||||
|
|
||||||
return keys;
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMac() {
|
function isMac() {
|
||||||
return navigator.platform.toUpperCase().includes("MAC");
|
return navigator.platform.toUpperCase().includes('MAC')
|
||||||
}
|
}
|
||||||
|
|
||||||
function show(event?: MouseEvent) {
|
function show(event?: MouseEvent) {
|
||||||
modal.value?.show(event);
|
modal.value?.show(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
modal.value?.hide();
|
modal.value?.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show,
|
show,
|
||||||
hide,
|
hide,
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@ -326,9 +326,14 @@ import {
|
|||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import {
|
import {
|
||||||
|
type Action,
|
||||||
|
type ButtonAction,
|
||||||
checklist,
|
checklist,
|
||||||
|
type ConditionalButtonAction,
|
||||||
deserializeActionStates,
|
deserializeActionStates,
|
||||||
|
type DropdownAction,
|
||||||
expandVariables,
|
expandVariables,
|
||||||
|
finalPermissionMessages,
|
||||||
findMatchingVariant,
|
findMatchingVariant,
|
||||||
flattenProjectVariables,
|
flattenProjectVariables,
|
||||||
getActionIdForStage,
|
getActionIdForStage,
|
||||||
@ -338,16 +343,9 @@ import {
|
|||||||
initializeActionState,
|
initializeActionState,
|
||||||
kebabToTitleCase,
|
kebabToTitleCase,
|
||||||
keybinds,
|
keybinds,
|
||||||
|
type MultiSelectChipsAction,
|
||||||
processMessage,
|
processMessage,
|
||||||
serializeActionStates,
|
serializeActionStates,
|
||||||
} from '@modrinth/moderation'
|
|
||||||
import {
|
|
||||||
type Action,
|
|
||||||
type ButtonAction,
|
|
||||||
type ConditionalButtonAction,
|
|
||||||
type DropdownAction,
|
|
||||||
finalPermissionMessages,
|
|
||||||
type MultiSelectChipsAction,
|
|
||||||
type Stage,
|
type Stage,
|
||||||
type ToggleAction,
|
type ToggleAction,
|
||||||
} from '@modrinth/moderation'
|
} from '@modrinth/moderation'
|
||||||
@ -363,8 +361,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
type ModerationJudgements,
|
type ModerationJudgements,
|
||||||
type ModerationModpackItem,
|
type ModerationModpackItem,
|
||||||
type ProjectStatus,
|
|
||||||
type Project,
|
type Project,
|
||||||
|
type ProjectStatus,
|
||||||
renderHighlightedString,
|
renderHighlightedString,
|
||||||
} from '@modrinth/utils'
|
} from '@modrinth/utils'
|
||||||
import { computedAsync, useLocalStorage } from '@vueuse/core'
|
import { computedAsync, useLocalStorage } from '@vueuse/core'
|
||||||
|
|||||||
@ -137,7 +137,7 @@
|
|||||||
<ButtonStyled v-if="modPackData && currentIndex < modPackData.length" color="blue">
|
<ButtonStyled v-if="modPackData && currentIndex < modPackData.length" color="blue">
|
||||||
<button :disabled="!canGoNext" @click="goToNext">
|
<button :disabled="!canGoNext" @click="goToNext">
|
||||||
<RightArrowIcon aria-hidden="true" />
|
<RightArrowIcon aria-hidden="true" />
|
||||||
{{ currentIndex + 1 >= modPackData.length ? "Complete" : "Next" }}
|
{{ currentIndex + 1 >= modPackData.length ? 'Complete' : 'Next' }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
@ -145,8 +145,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { LeftArrowIcon, RightArrowIcon } from "@modrinth/assets";
|
import { LeftArrowIcon, RightArrowIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
import { ButtonStyled } from '@modrinth/ui'
|
||||||
import type {
|
import type {
|
||||||
ModerationFlameModpackItem,
|
ModerationFlameModpackItem,
|
||||||
ModerationJudgements,
|
ModerationJudgements,
|
||||||
@ -155,19 +155,19 @@ import type {
|
|||||||
ModerationModpackResponse,
|
ModerationModpackResponse,
|
||||||
ModerationPermissionType,
|
ModerationPermissionType,
|
||||||
ModerationUnknownModpackItem,
|
ModerationUnknownModpackItem,
|
||||||
} from "@modrinth/utils";
|
} from '@modrinth/utils'
|
||||||
import { useLocalStorage, useSessionStorage } from "@vueuse/core";
|
import { useLocalStorage, useSessionStorage } from '@vueuse/core'
|
||||||
import { computed, onMounted,ref, watch } from "vue";
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
projectId: string;
|
projectId: string
|
||||||
modelValue?: ModerationJudgements;
|
modelValue?: ModerationJudgements
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
complete: [];
|
complete: []
|
||||||
"update:modelValue": [judgements: ModerationJudgements];
|
'update:modelValue': [judgements: ModerationJudgements]
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const persistedModPackData = useLocalStorage<ModerationModpackItem[] | null>(
|
const persistedModPackData = useLocalStorage<ModerationModpackItem[] | null>(
|
||||||
`modpack-permissions-${props.projectId}`,
|
`modpack-permissions-${props.projectId}`,
|
||||||
@ -178,9 +178,9 @@ const persistedModPackData = useLocalStorage<ModerationModpackItem[] | null>(
|
|||||||
write: (v: any) => JSON.stringify(v),
|
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>(
|
const modPackData = useSessionStorage<ModerationModpackItem[] | null>(
|
||||||
`modpack-permissions-data-${props.projectId}`,
|
`modpack-permissions-data-${props.projectId}`,
|
||||||
@ -191,7 +191,7 @@ const modPackData = useSessionStorage<ModerationModpackItem[] | null>(
|
|||||||
write: (v: any) => JSON.stringify(v),
|
write: (v: any) => JSON.stringify(v),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
const permanentNoFiles = useSessionStorage<ModerationModpackItem[]>(
|
const permanentNoFiles = useSessionStorage<ModerationModpackItem[]>(
|
||||||
`modpack-permissions-permanent-no-${props.projectId}`,
|
`modpack-permissions-permanent-no-${props.projectId}`,
|
||||||
[],
|
[],
|
||||||
@ -201,110 +201,110 @@ const permanentNoFiles = useSessionStorage<ModerationModpackItem[]>(
|
|||||||
write: (v: any) => JSON.stringify(v),
|
write: (v: any) => JSON.stringify(v),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
const currentIndex = ref(0);
|
const currentIndex = ref(0)
|
||||||
|
|
||||||
const fileApprovalTypes: ModerationModpackPermissionApprovalType[] = [
|
const fileApprovalTypes: ModerationModpackPermissionApprovalType[] = [
|
||||||
{
|
{
|
||||||
id: "yes",
|
id: 'yes',
|
||||||
name: "Yes",
|
name: 'Yes',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "with-attribution-and-source",
|
id: 'with-attribution-and-source',
|
||||||
name: "With attribution and source",
|
name: 'With attribution and source',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "with-attribution",
|
id: 'with-attribution',
|
||||||
name: "With attribution",
|
name: 'With attribution',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "no",
|
id: 'no',
|
||||||
name: "No",
|
name: 'No',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "permanent-no",
|
id: 'permanent-no',
|
||||||
name: "Permanent no",
|
name: 'Permanent no',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "unidentified",
|
id: 'unidentified',
|
||||||
name: "Unidentified",
|
name: 'Unidentified',
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
|
|
||||||
const filePermissionTypes: ModerationPermissionType[] = [
|
const filePermissionTypes: ModerationPermissionType[] = [
|
||||||
{ id: "yes", name: "Yes" },
|
{ id: 'yes', name: 'Yes' },
|
||||||
{ id: "no", name: "No" },
|
{ id: 'no', name: 'No' },
|
||||||
];
|
]
|
||||||
|
|
||||||
function persistAll() {
|
function persistAll() {
|
||||||
persistedModPackData.value = modPackData.value;
|
persistedModPackData.value = modPackData.value
|
||||||
persistedIndex.value = currentIndex.value;
|
persistedIndex.value = currentIndex.value
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
modPackData,
|
modPackData,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
persistedModPackData.value = newValue;
|
persistedModPackData.value = newValue
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
);
|
)
|
||||||
|
|
||||||
watch(currentIndex, (newValue) => {
|
watch(currentIndex, (newValue) => {
|
||||||
persistedIndex.value = newValue;
|
persistedIndex.value = newValue
|
||||||
});
|
})
|
||||||
|
|
||||||
function loadPersistedData(): void {
|
function loadPersistedData(): void {
|
||||||
if (persistedModPackData.value) {
|
if (persistedModPackData.value) {
|
||||||
modPackData.value = persistedModPackData.value;
|
modPackData.value = persistedModPackData.value
|
||||||
}
|
}
|
||||||
currentIndex.value = persistedIndex.value;
|
currentIndex.value = persistedIndex.value
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearPersistedData(): void {
|
function clearPersistedData(): void {
|
||||||
persistedModPackData.value = null;
|
persistedModPackData.value = null
|
||||||
persistedIndex.value = 0;
|
persistedIndex.value = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchModPackData(): Promise<void> {
|
async function fetchModPackData(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const data = (await useBaseFetch(`moderation/project/${props.projectId}`, {
|
const data = (await useBaseFetch(`moderation/project/${props.projectId}`, {
|
||||||
internal: true,
|
internal: true,
|
||||||
})) as ModerationModpackResponse;
|
})) as ModerationModpackResponse
|
||||||
|
|
||||||
const permanentNoItems: ModerationModpackItem[] = Object.entries(data.identified || {})
|
const permanentNoItems: ModerationModpackItem[] = Object.entries(data.identified || {})
|
||||||
.filter(([_, file]) => file.status === "permanent-no")
|
.filter(([_, file]) => file.status === 'permanent-no')
|
||||||
.map(
|
.map(
|
||||||
([sha1, file]): ModerationModpackItem => ({
|
([sha1, file]): ModerationModpackItem => ({
|
||||||
sha1,
|
sha1,
|
||||||
file_name: file.file_name,
|
file_name: file.file_name,
|
||||||
type: "identified",
|
type: 'identified',
|
||||||
status: file.status,
|
status: file.status,
|
||||||
approved: null,
|
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[] = [
|
const sortedData: ModerationModpackItem[] = [
|
||||||
...Object.entries(data.identified || {})
|
...Object.entries(data.identified || {})
|
||||||
.filter(
|
.filter(
|
||||||
([_, file]) =>
|
([_, file]) =>
|
||||||
file.status !== "yes" &&
|
file.status !== 'yes' &&
|
||||||
file.status !== "with-attribution-and-source" &&
|
file.status !== 'with-attribution-and-source' &&
|
||||||
file.status !== "permanent-no",
|
file.status !== 'permanent-no',
|
||||||
)
|
)
|
||||||
.map(
|
.map(
|
||||||
([sha1, file]): ModerationModpackItem => ({
|
([sha1, file]): ModerationModpackItem => ({
|
||||||
sha1,
|
sha1,
|
||||||
file_name: file.file_name,
|
file_name: file.file_name,
|
||||||
type: "identified",
|
type: 'identified',
|
||||||
status: file.status,
|
status: file.status,
|
||||||
approved: null,
|
approved: null,
|
||||||
...(file.status === "unidentified" && {
|
...(file.status === 'unidentified' && {
|
||||||
proof: "",
|
proof: '',
|
||||||
url: "",
|
url: '',
|
||||||
title: "",
|
title: '',
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ -314,12 +314,12 @@ async function fetchModPackData(): Promise<void> {
|
|||||||
([sha1, fileName]): ModerationUnknownModpackItem => ({
|
([sha1, fileName]): ModerationUnknownModpackItem => ({
|
||||||
sha1,
|
sha1,
|
||||||
file_name: fileName,
|
file_name: fileName,
|
||||||
type: "unknown",
|
type: 'unknown',
|
||||||
status: null,
|
status: null,
|
||||||
approved: null,
|
approved: null,
|
||||||
proof: "",
|
proof: '',
|
||||||
url: "",
|
url: '',
|
||||||
title: "",
|
title: '',
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.sort((a, b) => a.file_name.localeCompare(b.file_name)),
|
.sort((a, b) => a.file_name.localeCompare(b.file_name)),
|
||||||
@ -328,7 +328,7 @@ async function fetchModPackData(): Promise<void> {
|
|||||||
([sha1, info]): ModerationFlameModpackItem => ({
|
([sha1, info]): ModerationFlameModpackItem => ({
|
||||||
sha1,
|
sha1,
|
||||||
file_name: info.file_name,
|
file_name: info.file_name,
|
||||||
type: "flame",
|
type: 'flame',
|
||||||
status: null,
|
status: null,
|
||||||
approved: null,
|
approved: null,
|
||||||
id: info.id,
|
id: info.id,
|
||||||
@ -337,166 +337,166 @@ async function fetchModPackData(): Promise<void> {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.sort((a, b) => a.file_name.localeCompare(b.file_name)),
|
.sort((a, b) => a.file_name.localeCompare(b.file_name)),
|
||||||
];
|
]
|
||||||
|
|
||||||
if (modPackData.value) {
|
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) => {
|
sortedData.forEach((item) => {
|
||||||
const existing = existingMap.get(item.sha1);
|
const existing = existingMap.get(item.sha1)
|
||||||
if (existing) {
|
if (existing) {
|
||||||
Object.assign(item, {
|
Object.assign(item, {
|
||||||
status: existing.status,
|
status: existing.status,
|
||||||
approved: existing.approved,
|
approved: existing.approved,
|
||||||
...(item.type === "unknown" && {
|
...(item.type === 'unknown' && {
|
||||||
proof: (existing as ModerationUnknownModpackItem).proof || "",
|
proof: (existing as ModerationUnknownModpackItem).proof || '',
|
||||||
url: (existing as ModerationUnknownModpackItem).url || "",
|
url: (existing as ModerationUnknownModpackItem).url || '',
|
||||||
title: (existing as ModerationUnknownModpackItem).title || "",
|
title: (existing as ModerationUnknownModpackItem).title || '',
|
||||||
}),
|
}),
|
||||||
...(item.type === "flame" && {
|
...(item.type === 'flame' && {
|
||||||
url: (existing as ModerationFlameModpackItem).url || item.url,
|
url: (existing as ModerationFlameModpackItem).url || item.url,
|
||||||
title: (existing as ModerationFlameModpackItem).title || item.title,
|
title: (existing as ModerationFlameModpackItem).title || item.title,
|
||||||
}),
|
}),
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
modPackData.value = sortedData;
|
modPackData.value = sortedData
|
||||||
persistAll();
|
persistAll()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch modpack data:", error);
|
console.error('Failed to fetch modpack data:', error)
|
||||||
modPackData.value = [];
|
modPackData.value = []
|
||||||
permanentNoFiles.value = [];
|
permanentNoFiles.value = []
|
||||||
persistAll();
|
persistAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToPrevious(): void {
|
function goToPrevious(): void {
|
||||||
if (currentIndex.value > 0) {
|
if (currentIndex.value > 0) {
|
||||||
currentIndex.value--;
|
currentIndex.value--
|
||||||
persistAll();
|
persistAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
modPackData,
|
modPackData,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
persistedModPackData.value = newValue;
|
persistedModPackData.value = newValue
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
);
|
)
|
||||||
|
|
||||||
function goToNext(): void {
|
function goToNext(): void {
|
||||||
if (modPackData.value && currentIndex.value < modPackData.value.length) {
|
if (modPackData.value && currentIndex.value < modPackData.value.length) {
|
||||||
currentIndex.value++;
|
currentIndex.value++
|
||||||
|
|
||||||
if (currentIndex.value >= modPackData.value.length) {
|
if (currentIndex.value >= modPackData.value.length) {
|
||||||
const judgements = getJudgements();
|
const judgements = getJudgements()
|
||||||
emit("update:modelValue", judgements);
|
emit('update:modelValue', judgements)
|
||||||
emit("complete");
|
emit('complete')
|
||||||
clearPersistedData();
|
clearPersistedData()
|
||||||
} else {
|
} 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]) {
|
if (modPackData.value && modPackData.value[index]) {
|
||||||
modPackData.value[index].status = status;
|
modPackData.value[index].status = status
|
||||||
modPackData.value[index].approved = null;
|
modPackData.value[index].approved = null
|
||||||
persistAll();
|
persistAll()
|
||||||
emit("update:modelValue", getJudgements());
|
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]) {
|
if (modPackData.value && modPackData.value[index]) {
|
||||||
modPackData.value[index].approved = approved;
|
modPackData.value[index].approved = approved
|
||||||
persistAll();
|
persistAll()
|
||||||
emit("update:modelValue", getJudgements());
|
emit('update:modelValue', getJudgements())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const canGoNext = computed(() => {
|
const canGoNext = computed(() => {
|
||||||
if (!modPackData.value || !modPackData.value[currentIndex.value]) return false;
|
if (!modPackData.value || !modPackData.value[currentIndex.value]) return false
|
||||||
const current = modPackData.value[currentIndex.value];
|
const current = modPackData.value[currentIndex.value]
|
||||||
return current.status !== null;
|
return current.status !== null
|
||||||
});
|
})
|
||||||
|
|
||||||
function getJudgements(): ModerationJudgements {
|
function getJudgements(): ModerationJudgements {
|
||||||
if (!modPackData.value) return {};
|
if (!modPackData.value) return {}
|
||||||
|
|
||||||
const judgements: ModerationJudgements = {};
|
const judgements: ModerationJudgements = {}
|
||||||
|
|
||||||
modPackData.value.forEach((item) => {
|
modPackData.value.forEach((item) => {
|
||||||
if (item.type === "flame") {
|
if (item.type === 'flame') {
|
||||||
judgements[item.sha1] = {
|
judgements[item.sha1] = {
|
||||||
type: "flame",
|
type: 'flame',
|
||||||
id: item.id,
|
id: item.id,
|
||||||
status: item.status,
|
status: item.status,
|
||||||
link: item.url,
|
link: item.url,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
file_name: item.file_name,
|
file_name: item.file_name,
|
||||||
};
|
}
|
||||||
} else if (item.type === "unknown") {
|
} else if (item.type === 'unknown') {
|
||||||
judgements[item.sha1] = {
|
judgements[item.sha1] = {
|
||||||
type: "unknown",
|
type: 'unknown',
|
||||||
status: item.status,
|
status: item.status,
|
||||||
proof: item.proof,
|
proof: item.proof,
|
||||||
link: item.url,
|
link: item.url,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
file_name: item.file_name,
|
file_name: item.file_name,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
return judgements;
|
return judgements
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadPersistedData();
|
loadPersistedData()
|
||||||
if (!modPackData.value) {
|
if (!modPackData.value) {
|
||||||
fetchModPackData();
|
fetchModPackData()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
modPackData,
|
modPackData,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
if (newValue && newValue.length === 0) {
|
if (newValue && newValue.length === 0) {
|
||||||
emit("complete");
|
emit('complete')
|
||||||
clearPersistedData();
|
clearPersistedData()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.projectId,
|
() => props.projectId,
|
||||||
() => {
|
() => {
|
||||||
clearPersistedData();
|
clearPersistedData()
|
||||||
loadPersistedData();
|
loadPersistedData()
|
||||||
if (!modPackData.value) {
|
if (!modPackData.value) {
|
||||||
fetchModPackData();
|
fetchModPackData()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
function getModpackFiles(): {
|
function getModpackFiles(): {
|
||||||
interactive: ModerationModpackItem[];
|
interactive: ModerationModpackItem[]
|
||||||
permanentNo: ModerationModpackItem[];
|
permanentNo: ModerationModpackItem[]
|
||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
interactive: modPackData.value || [],
|
interactive: modPackData.value || [],
|
||||||
permanentNo: permanentNoFiles.value,
|
permanentNo: permanentNoFiles.value,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
getModpackFiles,
|
getModpackFiles,
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<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({
|
export default defineComponent({
|
||||||
name: "ModrinthLoadingIndicator",
|
name: 'ModrinthLoadingIndicator',
|
||||||
props: {
|
props: {
|
||||||
throttle: {
|
throttle: {
|
||||||
type: Number,
|
type: Number,
|
||||||
@ -20,115 +20,115 @@ export default defineComponent({
|
|||||||
color: {
|
color: {
|
||||||
type: [String, Boolean],
|
type: [String, Boolean],
|
||||||
default:
|
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 }) {
|
setup(props, { slots }) {
|
||||||
const indicator = useLoadingIndicator({
|
const indicator = useLoadingIndicator({
|
||||||
duration: props.duration,
|
duration: props.duration,
|
||||||
throttle: props.throttle,
|
throttle: props.throttle,
|
||||||
});
|
})
|
||||||
|
|
||||||
const nuxtApp = useNuxtApp();
|
const nuxtApp = useNuxtApp()
|
||||||
nuxtApp.hook("page:start", () => {
|
nuxtApp.hook('page:start', () => {
|
||||||
startLoading();
|
startLoading()
|
||||||
indicator.start();
|
indicator.start()
|
||||||
});
|
})
|
||||||
nuxtApp.hook("page:finish", () => {
|
nuxtApp.hook('page:finish', () => {
|
||||||
stopLoading();
|
stopLoading()
|
||||||
indicator.finish();
|
indicator.finish()
|
||||||
});
|
})
|
||||||
onBeforeUnmount(() => indicator.clear);
|
onBeforeUnmount(() => indicator.clear)
|
||||||
|
|
||||||
const loading = useLoading();
|
const loading = useLoading()
|
||||||
|
|
||||||
watch(loading, (newValue) => {
|
watch(loading, (newValue) => {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
indicator.start();
|
indicator.start()
|
||||||
} else {
|
} else {
|
||||||
indicator.finish();
|
indicator.finish()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
return () =>
|
return () =>
|
||||||
h(
|
h(
|
||||||
"div",
|
'div',
|
||||||
{
|
{
|
||||||
class: "nuxt-loading-indicator",
|
class: 'nuxt-loading-indicator',
|
||||||
style: {
|
style: {
|
||||||
position: "fixed",
|
position: 'fixed',
|
||||||
top: 0,
|
top: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
pointerEvents: "none",
|
pointerEvents: 'none',
|
||||||
width: `${indicator.progress.value}%`,
|
width: `${indicator.progress.value}%`,
|
||||||
height: `${props.height}px`,
|
height: `${props.height}px`,
|
||||||
opacity: indicator.isLoading.value ? 1 : 0,
|
opacity: indicator.isLoading.value ? 1 : 0,
|
||||||
background: props.color || undefined,
|
background: props.color || undefined,
|
||||||
backgroundSize: `${(100 / indicator.progress.value) * 100}% auto`,
|
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,
|
zIndex: 999999,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
slots,
|
slots,
|
||||||
);
|
)
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
function useLoadingIndicator(opts: { duration: number; throttle: number }) {
|
function useLoadingIndicator(opts: { duration: number; throttle: number }) {
|
||||||
const progress = ref(0);
|
const progress = ref(0)
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false)
|
||||||
const step = computed(() => 10000 / opts.duration);
|
const step = computed(() => 10000 / opts.duration)
|
||||||
|
|
||||||
let _timer: any = null;
|
let _timer: any = null
|
||||||
let _throttle: any = null;
|
let _throttle: any = null
|
||||||
|
|
||||||
function start() {
|
function start() {
|
||||||
clear();
|
clear()
|
||||||
progress.value = 0;
|
progress.value = 0
|
||||||
if (opts.throttle && import.meta.client) {
|
if (opts.throttle && import.meta.client) {
|
||||||
_throttle = setTimeout(() => {
|
_throttle = setTimeout(() => {
|
||||||
isLoading.value = true;
|
isLoading.value = true
|
||||||
_startTimer();
|
_startTimer()
|
||||||
}, opts.throttle);
|
}, opts.throttle)
|
||||||
} else {
|
} else {
|
||||||
isLoading.value = true;
|
isLoading.value = true
|
||||||
_startTimer();
|
_startTimer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function finish() {
|
function finish() {
|
||||||
progress.value = 100;
|
progress.value = 100
|
||||||
_hide();
|
_hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
function clear() {
|
function clear() {
|
||||||
clearInterval(_timer);
|
clearInterval(_timer)
|
||||||
clearTimeout(_throttle);
|
clearTimeout(_throttle)
|
||||||
_timer = null;
|
_timer = null
|
||||||
_throttle = null;
|
_throttle = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function _increase(num: number) {
|
function _increase(num: number) {
|
||||||
progress.value = Math.min(100, progress.value + num);
|
progress.value = Math.min(100, progress.value + num)
|
||||||
}
|
}
|
||||||
|
|
||||||
function _hide() {
|
function _hide() {
|
||||||
clear();
|
clear()
|
||||||
if (import.meta.client) {
|
if (import.meta.client) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isLoading.value = false;
|
isLoading.value = false
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
progress.value = 0;
|
progress.value = 0
|
||||||
}, 400);
|
}, 400)
|
||||||
}, 500);
|
}, 500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _startTimer() {
|
function _startTimer() {
|
||||||
if (import.meta.client) {
|
if (import.meta.client) {
|
||||||
_timer = setInterval(() => {
|
_timer = setInterval(() => {
|
||||||
_increase(step.value);
|
_increase(step.value)
|
||||||
}, 100);
|
}, 100)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,5 +138,5 @@ function useLoadingIndicator(opts: { duration: number; throttle: number }) {
|
|||||||
start,
|
start,
|
||||||
finish,
|
finish,
|
||||||
clear,
|
clear,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,10 +25,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NewspaperIcon } from "@modrinth/assets";
|
import { NewspaperIcon } from '@modrinth/assets'
|
||||||
import { articles as rawArticles } from "@modrinth/blog";
|
import { articles as rawArticles } from '@modrinth/blog'
|
||||||
import { ButtonStyled, NewsArticleCard } from "@modrinth/ui";
|
import { ButtonStyled, NewsArticleCard } from '@modrinth/ui'
|
||||||
import { computed,ref } from "vue";
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
const articles = ref(
|
const articles = ref(
|
||||||
rawArticles
|
rawArticles
|
||||||
@ -43,7 +43,7 @@ const articles = ref(
|
|||||||
date: article.date,
|
date: article.date,
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()),
|
.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>
|
</script>
|
||||||
|
|||||||
@ -103,15 +103,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ReportIcon, UnknownIcon, VersionIcon } from "@modrinth/assets";
|
import { ReportIcon, UnknownIcon, VersionIcon } from '@modrinth/assets'
|
||||||
import { Avatar, Badge, CopyCode, useRelativeTime } from "@modrinth/ui";
|
import { Avatar, Badge, CopyCode, useRelativeTime } from '@modrinth/ui'
|
||||||
import { formatProjectType } from "@modrinth/utils";
|
import { formatProjectType } from '@modrinth/utils'
|
||||||
|
|
||||||
import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
|
import ThreadSummary from '~/components/ui/thread/ThreadSummary.vue'
|
||||||
import { renderHighlightedString } from "~/helpers/highlight.js";
|
import { renderHighlightedString } from '~/helpers/highlight.js'
|
||||||
import { getProjectTypeForUrl } from "~/helpers/projects.js";
|
import { getProjectTypeForUrl } from '~/helpers/projects.js'
|
||||||
|
|
||||||
const formatRelativeTime = useRelativeTime();
|
const formatRelativeTime = useRelativeTime()
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
report: {
|
report: {
|
||||||
@ -138,9 +138,9 @@ defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const flags = useFeatureFlags();
|
const flags = useFeatureFlags()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@ -21,10 +21,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
|
import Breadcrumbs from '~/components/ui/Breadcrumbs.vue'
|
||||||
import ReportInfo from "~/components/ui/report/ReportInfo.vue";
|
import ReportInfo from '~/components/ui/report/ReportInfo.vue'
|
||||||
import ConversationThread from "~/components/ui/thread/ConversationThread.vue";
|
import ConversationThread from '~/components/ui/thread/ConversationThread.vue'
|
||||||
import { addReportMessage } from "~/helpers/threads.js";
|
import { addReportMessage } from '~/helpers/threads.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
reportId: {
|
reportId: {
|
||||||
@ -39,76 +39,76 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const report = ref(null);
|
const report = ref(null)
|
||||||
|
|
||||||
await fetchReport().then((result) => {
|
await fetchReport().then((result) => {
|
||||||
report.value = result;
|
report.value = result
|
||||||
});
|
})
|
||||||
|
|
||||||
const { data: rawThread } = await useAsyncData(`thread/${report.value.thread_id}`, () =>
|
const { data: rawThread } = await useAsyncData(`thread/${report.value.thread_id}`, () =>
|
||||||
useBaseFetch(`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) {
|
async function updateThread(newThread) {
|
||||||
rawThread.value = newThread;
|
rawThread.value = newThread
|
||||||
report.value = await fetchReport();
|
report.value = await fetchReport()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchReport() {
|
async function fetchReport() {
|
||||||
const { data: rawReport } = await useAsyncData(`report/${props.reportId}`, () =>
|
const { data: rawReport } = await useAsyncData(`report/${props.reportId}`, () =>
|
||||||
useBaseFetch(`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 = [];
|
const userIds = []
|
||||||
userIds.push(rawReport.value.reporter);
|
userIds.push(rawReport.value.reporter)
|
||||||
if (rawReport.value.item_type === "user") {
|
if (rawReport.value.item_type === 'user') {
|
||||||
userIds.push(rawReport.value.item_id);
|
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) {
|
if (userIds.length > 0) {
|
||||||
const { data: usersVal } = await useAsyncData(`users?ids=${JSON.stringify(userIds)}`, () =>
|
const { data: usersVal } = await useAsyncData(`users?ids=${JSON.stringify(userIds)}`, () =>
|
||||||
useBaseFetch(`users?ids=${encodeURIComponent(JSON.stringify(userIds))}`),
|
useBaseFetch(`users?ids=${encodeURIComponent(JSON.stringify(userIds))}`),
|
||||||
);
|
)
|
||||||
users = usersVal.value;
|
users = usersVal.value
|
||||||
}
|
}
|
||||||
|
|
||||||
let version = null;
|
let version = null
|
||||||
if (versionId) {
|
if (versionId) {
|
||||||
const { data: versionVal } = await useAsyncData(`version/${versionId}`, () =>
|
const { data: versionVal } = await useAsyncData(`version/${versionId}`, () =>
|
||||||
useBaseFetch(`version/${versionId}`),
|
useBaseFetch(`version/${versionId}`),
|
||||||
);
|
)
|
||||||
version = versionVal.value;
|
version = versionVal.value
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectId = version
|
const projectId = version
|
||||||
? version.project_id
|
? version.project_id
|
||||||
: rawReport.value.item_type === "project"
|
: rawReport.value.item_type === 'project'
|
||||||
? rawReport.value.item_id
|
? rawReport.value.item_id
|
||||||
: null;
|
: null
|
||||||
|
|
||||||
let project = null;
|
let project = null
|
||||||
if (projectId) {
|
if (projectId) {
|
||||||
const { data: projectVal } = await useAsyncData(`project/${projectId}`, () =>
|
const { data: projectVal } = await useAsyncData(`project/${projectId}`, () =>
|
||||||
useBaseFetch(`project/${projectId}`),
|
useBaseFetch(`project/${projectId}`),
|
||||||
);
|
)
|
||||||
project = projectVal.value;
|
project = projectVal.value
|
||||||
}
|
}
|
||||||
|
|
||||||
const reportData = rawReport.value;
|
const reportData = rawReport.value
|
||||||
reportData.project = project;
|
reportData.project = project
|
||||||
reportData.version = version;
|
reportData.version = version
|
||||||
reportData.reporterUser = users.find((user) => user.id === rawReport.value.reporter);
|
reportData.reporterUser = users.find((user) => user.id === rawReport.value.reporter)
|
||||||
if (rawReport.value.item_type === "user") {
|
if (rawReport.value.item_type === 'user') {
|
||||||
reportData.user = users.find((user) => user.id === rawReport.value.item_id);
|
reportData.user = users.find((user) => user.id === rawReport.value.item_id)
|
||||||
}
|
}
|
||||||
return reportData;
|
return reportData
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@ -24,11 +24,11 @@
|
|||||||
<p v-if="reports.length === 0">You don't have any active reports.</p>
|
<p v-if="reports.length === 0">You don't have any active reports.</p>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Chips } from "@modrinth/ui";
|
import { Chips } from '@modrinth/ui'
|
||||||
|
|
||||||
import ReportInfo from "~/components/ui/report/ReportInfo.vue";
|
import ReportInfo from '~/components/ui/report/ReportInfo.vue'
|
||||||
import { addReportMessage } from "~/helpers/threads.js";
|
import { addReportMessage } from '~/helpers/threads.js'
|
||||||
import { asEncodedJsonArray, fetchSegmented } from "~/utils/fetch-helpers.ts";
|
import { asEncodedJsonArray, fetchSegmented } from '~/utils/fetch-helpers.ts'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
moderation: {
|
moderation: {
|
||||||
@ -39,34 +39,34 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const viewMode = ref("open");
|
const viewMode = ref('open')
|
||||||
const reasonFilter = ref("All");
|
const reasonFilter = ref('All')
|
||||||
const reports = ref([]);
|
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}`),
|
useBaseFetch(`report?count=${MAX_REPORTS}`),
|
||||||
);
|
)
|
||||||
|
|
||||||
rawReports = rawReports.value.map((report) => {
|
rawReports = rawReports.value.map((report) => {
|
||||||
report.item_id = report.item_id.replace(/"/g, "");
|
report.item_id = report.item_id.replace(/"/g, '')
|
||||||
return report;
|
return report
|
||||||
});
|
})
|
||||||
|
|
||||||
const reporterUsers = rawReports.map((report) => report.reporter);
|
const reporterUsers = rawReports.map((report) => report.reporter)
|
||||||
const reportedUsers = rawReports
|
const reportedUsers = rawReports
|
||||||
.filter((report) => report.item_type === "user")
|
.filter((report) => report.item_type === 'user')
|
||||||
.map((report) => report.item_id);
|
.map((report) => report.item_id)
|
||||||
const versionReports = rawReports.filter((report) => report.item_type === "version");
|
const versionReports = rawReports.filter((report) => report.item_type === 'version')
|
||||||
const versionIds = [...new Set(versionReports.map((report) => report.item_id))];
|
const versionIds = [...new Set(versionReports.map((report) => report.item_id))]
|
||||||
const userIds = [...new Set(reporterUsers.concat(reportedUsers))];
|
const userIds = [...new Set(reporterUsers.concat(reportedUsers))]
|
||||||
const threadIds = [
|
const threadIds = [
|
||||||
...new Set(rawReports.filter((report) => report.thread_id).map((report) => report.thread_id)),
|
...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([
|
const [{ data: users }, { data: versions }, { data: threads }] = await Promise.all([
|
||||||
await useAsyncData(`users?ids=${JSON.stringify(userIds)}`, () =>
|
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)}`, () =>
|
await useAsyncData(`threads?ids=${JSON.stringify(threadIds)}`, () =>
|
||||||
fetchSegmented(threadIds, (ids) => `threads?ids=${asEncodedJsonArray(ids)}`),
|
fetchSegmented(threadIds, (ids) => `threads?ids=${asEncodedJsonArray(ids)}`),
|
||||||
),
|
),
|
||||||
]);
|
])
|
||||||
|
|
||||||
const reportedProjects = rawReports
|
const reportedProjects = rawReports
|
||||||
.filter((report) => report.item_type === "project")
|
.filter((report) => report.item_type === 'project')
|
||||||
.map((report) => report.item_id);
|
.map((report) => report.item_id)
|
||||||
const versionProjects = versions.value.map((version) => version.project_id);
|
const versionProjects = versions.value.map((version) => version.project_id)
|
||||||
const projectIds = [...new Set(reportedProjects.concat(versionProjects))];
|
const projectIds = [...new Set(reportedProjects.concat(versionProjects))]
|
||||||
|
|
||||||
const { data: projects } = await useAsyncData(`projects?ids=${JSON.stringify(projectIds)}`, () =>
|
const { data: projects } = await useAsyncData(`projects?ids=${JSON.stringify(projectIds)}`, () =>
|
||||||
fetchSegmented(projectIds, (ids) => `projects?ids=${asEncodedJsonArray(ids)}`),
|
fetchSegmented(projectIds, (ids) => `projects?ids=${asEncodedJsonArray(ids)}`),
|
||||||
);
|
)
|
||||||
|
|
||||||
reports.value = rawReports.map((report) => {
|
reports.value = rawReports.map((report) => {
|
||||||
report.reporterUser = users.value.find((user) => user.id === report.reporter);
|
report.reporterUser = users.value.find((user) => user.id === report.reporter)
|
||||||
if (report.item_type === "user") {
|
if (report.item_type === 'user') {
|
||||||
report.user = users.value.find((user) => user.id === report.item_id);
|
report.user = users.value.find((user) => user.id === report.item_id)
|
||||||
} else if (report.item_type === "project") {
|
} else if (report.item_type === 'project') {
|
||||||
report.project = projects.value.find((project) => project.id === report.item_id);
|
report.project = projects.value.find((project) => project.id === report.item_id)
|
||||||
} else if (report.item_type === "version") {
|
} else if (report.item_type === 'version') {
|
||||||
report.version = versions.value.find((version) => version.id === report.item_id);
|
report.version = versions.value.find((version) => version.id === report.item_id)
|
||||||
report.project = projects.value.find((project) => project.id === report.version.project_id);
|
report.project = projects.value.find((project) => project.id === report.version.project_id)
|
||||||
}
|
}
|
||||||
if (report.thread_id) {
|
if (report.thread_id) {
|
||||||
report.thread = addReportMessage(
|
report.thread = addReportMessage(
|
||||||
threads.value.find((thread) => report.thread_id === thread.id),
|
threads.value.find((thread) => report.thread_id === thread.id),
|
||||||
report,
|
report,
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
report.open = true;
|
report.open = true
|
||||||
return report;
|
return report
|
||||||
});
|
})
|
||||||
|
|
||||||
const filteredReports = computed(() =>
|
const filteredReports = computed(() =>
|
||||||
reports.value?.filter(
|
reports.value?.filter(
|
||||||
(x) =>
|
(x) =>
|
||||||
(props.moderation || x.reporterUser.id === props.auth.user.id) &&
|
(props.moderation || x.reporterUser.id === props.auth.user.id) &&
|
||||||
(viewMode.value === "open" ? x.open : !x.open) &&
|
(viewMode.value === 'open' ? x.open : !x.open) &&
|
||||||
(reasonFilter.value === "All" || reasonFilter.value === x.report_type),
|
(reasonFilter.value === 'All' || reasonFilter.value === x.report_type),
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -10,14 +10,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { formatCategory } from "@modrinth/utils";
|
import { formatCategory } from '@modrinth/utils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
categories: {
|
categories: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default() {
|
default() {
|
||||||
return [];
|
return []
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
@ -26,9 +26,9 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const tags = useTags();
|
const tags = useTags()
|
||||||
|
|
||||||
return { tags };
|
return { tags }
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
categoriesFiltered() {
|
categoriesFiltered() {
|
||||||
@ -37,11 +37,11 @@ export default {
|
|||||||
.filter(
|
.filter(
|
||||||
(x) =>
|
(x) =>
|
||||||
this.categories.includes(x.name) && (!x.project_type || x.project_type === this.type),
|
this.categories.includes(x.name) && (!x.project_type || x.project_type === this.type),
|
||||||
);
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: { formatCategory },
|
methods: { formatCategory },
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Checkbox from "~/components/ui/Checkbox.vue";
|
import Checkbox from '~/components/ui/Checkbox.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -25,30 +25,30 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
facetName: {
|
facetName: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: '',
|
||||||
},
|
},
|
||||||
displayName: {
|
displayName: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: '',
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: '',
|
||||||
},
|
},
|
||||||
activeFilters: {
|
activeFilters: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default() {
|
default() {
|
||||||
return [];
|
return []
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
emits: ["toggle"],
|
emits: ['toggle'],
|
||||||
methods: {
|
methods: {
|
||||||
toggle() {
|
toggle() {
|
||||||
this.$emit("toggle", this.facetName);
|
this.$emit('toggle', this.facetName)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@ -42,83 +42,83 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { IssuesIcon, PlusIcon, XIcon } from "@modrinth/assets";
|
import { IssuesIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
import { ButtonStyled, NewModal } from '@modrinth/ui'
|
||||||
import { ModrinthServersFetchError, type ServerBackup } from "@modrinth/utils";
|
import { ModrinthServersFetchError, type ServerBackup } from '@modrinth/utils'
|
||||||
import { computed,nextTick, ref } from "vue";
|
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<{
|
const props = defineProps<{
|
||||||
server: ModrinthServer;
|
server: ModrinthServer
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const modal = ref<InstanceType<typeof NewModal>>();
|
const modal = ref<InstanceType<typeof NewModal>>()
|
||||||
const input = ref<HTMLInputElement>();
|
const input = ref<HTMLInputElement>()
|
||||||
const isCreating = ref(false);
|
const isCreating = ref(false)
|
||||||
const isRateLimited = ref(false);
|
const isRateLimited = ref(false)
|
||||||
const backupName = ref("");
|
const backupName = ref('')
|
||||||
const newBackupAmount = computed(() =>
|
const newBackupAmount = computed(() =>
|
||||||
props.server.backups?.data?.length === undefined ? 1 : props.server.backups?.data?.length + 1,
|
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(() => {
|
const nameExists = computed(() => {
|
||||||
if (!props.server.backups?.data) return false;
|
if (!props.server.backups?.data) return false
|
||||||
return props.server.backups.data.some(
|
return props.server.backups.data.some(
|
||||||
(backup: ServerBackup) => backup.name.trim().toLowerCase() === trimmedName.value.toLowerCase(),
|
(backup: ServerBackup) => backup.name.trim().toLowerCase() === trimmedName.value.toLowerCase(),
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
|
|
||||||
const focusInput = () => {
|
const focusInput = () => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
input.value?.focus();
|
input.value?.focus()
|
||||||
}, 100);
|
}, 100)
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
backupName.value = "";
|
backupName.value = ''
|
||||||
isCreating.value = false;
|
isCreating.value = false
|
||||||
modal.value?.show();
|
modal.value?.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideModal = () => {
|
const hideModal = () => {
|
||||||
modal.value?.hide();
|
modal.value?.hide()
|
||||||
};
|
}
|
||||||
|
|
||||||
const createBackup = async () => {
|
const createBackup = async () => {
|
||||||
if (backupName.value.trim().length === 0) {
|
if (backupName.value.trim().length === 0) {
|
||||||
backupName.value = `Backup #${newBackupAmount.value}`;
|
backupName.value = `Backup #${newBackupAmount.value}`
|
||||||
}
|
}
|
||||||
|
|
||||||
isCreating.value = true;
|
isCreating.value = true
|
||||||
isRateLimited.value = false;
|
isRateLimited.value = false
|
||||||
try {
|
try {
|
||||||
await props.server.backups?.create(trimmedName.value);
|
await props.server.backups?.create(trimmedName.value)
|
||||||
hideModal();
|
hideModal()
|
||||||
await props.server.refresh();
|
await props.server.refresh()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ModrinthServersFetchError && error?.statusCode === 429) {
|
if (error instanceof ModrinthServersFetchError && error?.statusCode === 429) {
|
||||||
isRateLimited.value = true;
|
isRateLimited.value = true
|
||||||
addNotification({
|
addNotification({
|
||||||
type: "error",
|
type: 'error',
|
||||||
title: "Error creating backup",
|
title: 'Error creating backup',
|
||||||
text: "You're creating backups too fast.",
|
text: "You're creating backups too fast.",
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
addNotification({ type: "error", title: "Error creating backup", text: message });
|
addNotification({ type: 'error', title: 'Error creating backup', text: message })
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isCreating.value = false;
|
isCreating.value = false
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show,
|
show,
|
||||||
hide: hideModal,
|
hide: hideModal,
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -18,25 +18,25 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ConfirmModal } from "@modrinth/ui";
|
import { ConfirmModal } from '@modrinth/ui'
|
||||||
import type { Backup } from "@modrinth/utils";
|
import type { Backup } from '@modrinth/utils'
|
||||||
import { ref } from "vue";
|
import { ref } from 'vue'
|
||||||
|
|
||||||
import BackupItem from "~/components/ui/servers/BackupItem.vue";
|
import BackupItem from '~/components/ui/servers/BackupItem.vue'
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "delete", backup: Backup | undefined): void;
|
(e: 'delete', backup: Backup | undefined): void
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const modal = ref<InstanceType<typeof ConfirmModal>>();
|
const modal = ref<InstanceType<typeof ConfirmModal>>()
|
||||||
const currentBackup = ref<Backup | undefined>(undefined);
|
const currentBackup = ref<Backup | undefined>(undefined)
|
||||||
|
|
||||||
function show(backup: Backup) {
|
function show(backup: Backup) {
|
||||||
currentBackup.value = backup;
|
currentBackup.value = backup
|
||||||
modal.value?.show();
|
modal.value?.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show,
|
show,
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -12,160 +12,160 @@ import {
|
|||||||
SpinnerIcon,
|
SpinnerIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from "@modrinth/assets";
|
} from '@modrinth/assets'
|
||||||
import { ButtonStyled, commonMessages, OverflowMenu, ProgressBar } from "@modrinth/ui";
|
import { ButtonStyled, commonMessages, OverflowMenu, ProgressBar } from '@modrinth/ui'
|
||||||
import type { Backup } from "@modrinth/utils";
|
import type { Backup } from '@modrinth/utils'
|
||||||
import { defineMessages, useVIntl } from "@vintl/vintl";
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import dayjs from "dayjs";
|
import dayjs from 'dayjs'
|
||||||
import { computed,ref } from "vue";
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
const flags = useFeatureFlags();
|
const flags = useFeatureFlags()
|
||||||
const { formatMessage } = useVIntl();
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "prepare" | "download" | "rename" | "restore" | "lock" | "retry"): void;
|
(e: 'prepare' | 'download' | 'rename' | 'restore' | 'lock' | 'retry'): void
|
||||||
(e: "delete", skipConfirmation?: boolean): void;
|
(e: 'delete', skipConfirmation?: boolean): void
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
backup: Backup;
|
backup: Backup
|
||||||
preview?: boolean;
|
preview?: boolean
|
||||||
kyrosUrl?: string;
|
kyrosUrl?: string
|
||||||
jwt?: string;
|
jwt?: string
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
preview: false,
|
preview: false,
|
||||||
kyrosUrl: undefined,
|
kyrosUrl: undefined,
|
||||||
jwt: undefined,
|
jwt: undefined,
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
const backupQueued = computed(
|
const backupQueued = computed(
|
||||||
() =>
|
() =>
|
||||||
props.backup.task?.create?.progress === 0 ||
|
props.backup.task?.create?.progress === 0 ||
|
||||||
(props.backup.ongoing && !props.backup.task?.create),
|
(props.backup.ongoing && !props.backup.task?.create),
|
||||||
);
|
)
|
||||||
const automated = computed(() => props.backup.automated);
|
const automated = computed(() => props.backup.automated)
|
||||||
const failedToCreate = computed(() => props.backup.interrupted);
|
const failedToCreate = computed(() => props.backup.interrupted)
|
||||||
|
|
||||||
const preparedDownloadStates = ["ready", "done"];
|
const preparedDownloadStates = ['ready', 'done']
|
||||||
const inactiveStates = ["failed", "cancelled"];
|
const inactiveStates = ['failed', 'cancelled']
|
||||||
|
|
||||||
const hasPreparedDownload = computed(() => {
|
const hasPreparedDownload = computed(() => {
|
||||||
const fileState = props.backup.task?.file?.state ?? "";
|
const fileState = props.backup.task?.file?.state ?? ''
|
||||||
return preparedDownloadStates.includes(fileState);
|
return preparedDownloadStates.includes(fileState)
|
||||||
});
|
})
|
||||||
|
|
||||||
const creating = computed(() => {
|
const creating = computed(() => {
|
||||||
const task = props.backup.task?.create;
|
const task = props.backup.task?.create
|
||||||
if (task && task.progress < 1 && !inactiveStates.includes(task.state)) {
|
if (task && task.progress < 1 && !inactiveStates.includes(task.state)) {
|
||||||
return task;
|
return task
|
||||||
}
|
}
|
||||||
if (props.backup.ongoing) {
|
if (props.backup.ongoing) {
|
||||||
return {
|
return {
|
||||||
progress: 0,
|
progress: 0,
|
||||||
state: "ongoing",
|
state: 'ongoing',
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined
|
||||||
});
|
})
|
||||||
|
|
||||||
const restoring = computed(() => {
|
const restoring = computed(() => {
|
||||||
const task = props.backup.task?.restore;
|
const task = props.backup.task?.restore
|
||||||
if (task && task.progress < 1 && !inactiveStates.includes(task.state)) {
|
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(() => {
|
const preparingFile = computed(() => {
|
||||||
if (hasPreparedDownload.value) {
|
if (hasPreparedDownload.value) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const task = props.backup.task?.file;
|
const task = props.backup.task?.file
|
||||||
return (
|
return (
|
||||||
(!task && initiatedPrepare.value) ||
|
(!task && initiatedPrepare.value) ||
|
||||||
(task && task.progress < 1 && !inactiveStates.includes(task.state))
|
(task && task.progress < 1 && !inactiveStates.includes(task.state))
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
|
|
||||||
const failedToRestore = computed(() => props.backup.task?.restore?.state === "failed");
|
const failedToRestore = computed(() => props.backup.task?.restore?.state === 'failed')
|
||||||
const failedToPrepareFile = computed(() => props.backup.task?.file?.state === "failed");
|
const failedToPrepareFile = computed(() => props.backup.task?.file?.state === 'failed')
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
locked: {
|
locked: {
|
||||||
id: "servers.backups.item.locked",
|
id: 'servers.backups.item.locked',
|
||||||
defaultMessage: "Locked",
|
defaultMessage: 'Locked',
|
||||||
},
|
},
|
||||||
lock: {
|
lock: {
|
||||||
id: "servers.backups.item.lock",
|
id: 'servers.backups.item.lock',
|
||||||
defaultMessage: "Lock",
|
defaultMessage: 'Lock',
|
||||||
},
|
},
|
||||||
unlock: {
|
unlock: {
|
||||||
id: "servers.backups.item.unlock",
|
id: 'servers.backups.item.unlock',
|
||||||
defaultMessage: "Unlock",
|
defaultMessage: 'Unlock',
|
||||||
},
|
},
|
||||||
restore: {
|
restore: {
|
||||||
id: "servers.backups.item.restore",
|
id: 'servers.backups.item.restore',
|
||||||
defaultMessage: "Restore",
|
defaultMessage: 'Restore',
|
||||||
},
|
},
|
||||||
rename: {
|
rename: {
|
||||||
id: "servers.backups.item.rename",
|
id: 'servers.backups.item.rename',
|
||||||
defaultMessage: "Rename",
|
defaultMessage: 'Rename',
|
||||||
},
|
},
|
||||||
queuedForBackup: {
|
queuedForBackup: {
|
||||||
id: "servers.backups.item.queued-for-backup",
|
id: 'servers.backups.item.queued-for-backup',
|
||||||
defaultMessage: "Queued for backup",
|
defaultMessage: 'Queued for backup',
|
||||||
},
|
},
|
||||||
preparingDownload: {
|
preparingDownload: {
|
||||||
id: "servers.backups.item.preparing-download",
|
id: 'servers.backups.item.preparing-download',
|
||||||
defaultMessage: "Preparing download...",
|
defaultMessage: 'Preparing download...',
|
||||||
},
|
},
|
||||||
prepareDownload: {
|
prepareDownload: {
|
||||||
id: "servers.backups.item.prepare-download",
|
id: 'servers.backups.item.prepare-download',
|
||||||
defaultMessage: "Prepare download",
|
defaultMessage: 'Prepare download',
|
||||||
},
|
},
|
||||||
prepareDownloadAgain: {
|
prepareDownloadAgain: {
|
||||||
id: "servers.backups.item.prepare-download-again",
|
id: 'servers.backups.item.prepare-download-again',
|
||||||
defaultMessage: "Try preparing again",
|
defaultMessage: 'Try preparing again',
|
||||||
},
|
},
|
||||||
alreadyPreparing: {
|
alreadyPreparing: {
|
||||||
id: "servers.backups.item.already-preparing",
|
id: 'servers.backups.item.already-preparing',
|
||||||
defaultMessage: "Already preparing backup for download",
|
defaultMessage: 'Already preparing backup for download',
|
||||||
},
|
},
|
||||||
creatingBackup: {
|
creatingBackup: {
|
||||||
id: "servers.backups.item.creating-backup",
|
id: 'servers.backups.item.creating-backup',
|
||||||
defaultMessage: "Creating backup...",
|
defaultMessage: 'Creating backup...',
|
||||||
},
|
},
|
||||||
restoringBackup: {
|
restoringBackup: {
|
||||||
id: "servers.backups.item.restoring-backup",
|
id: 'servers.backups.item.restoring-backup',
|
||||||
defaultMessage: "Restoring from backup...",
|
defaultMessage: 'Restoring from backup...',
|
||||||
},
|
},
|
||||||
failedToCreateBackup: {
|
failedToCreateBackup: {
|
||||||
id: "servers.backups.item.failed-to-create-backup",
|
id: 'servers.backups.item.failed-to-create-backup',
|
||||||
defaultMessage: "Failed to create backup",
|
defaultMessage: 'Failed to create backup',
|
||||||
},
|
},
|
||||||
failedToRestoreBackup: {
|
failedToRestoreBackup: {
|
||||||
id: "servers.backups.item.failed-to-restore-backup",
|
id: 'servers.backups.item.failed-to-restore-backup',
|
||||||
defaultMessage: "Failed to restore from backup",
|
defaultMessage: 'Failed to restore from backup',
|
||||||
},
|
},
|
||||||
failedToPrepareFile: {
|
failedToPrepareFile: {
|
||||||
id: "servers.backups.item.failed-to-prepare-backup",
|
id: 'servers.backups.item.failed-to-prepare-backup',
|
||||||
defaultMessage: "Failed to prepare download",
|
defaultMessage: 'Failed to prepare download',
|
||||||
},
|
},
|
||||||
automated: {
|
automated: {
|
||||||
id: "servers.backups.item.automated",
|
id: 'servers.backups.item.automated',
|
||||||
defaultMessage: "Automated",
|
defaultMessage: 'Automated',
|
||||||
},
|
},
|
||||||
retry: {
|
retry: {
|
||||||
id: "servers.backups.item.retry",
|
id: 'servers.backups.item.retry',
|
||||||
defaultMessage: "Retry",
|
defaultMessage: 'Retry',
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
@ -239,7 +239,7 @@ const messages = defineMessages({
|
|||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="col-span-2">
|
<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>
|
||||||
<div v-if="false">{{ 245 }} MiB</div>
|
<div v-if="false">{{ 245 }} MiB</div>
|
||||||
</template>
|
</template>
|
||||||
@ -285,8 +285,8 @@ const messages = defineMessages({
|
|||||||
:disabled="!!preparingFile"
|
:disabled="!!preparingFile"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
initiatedPrepare = true;
|
initiatedPrepare = true
|
||||||
emit('prepare');
|
emit('prepare')
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -45,98 +45,98 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { IssuesIcon,SaveIcon, SpinnerIcon, XIcon } from "@modrinth/assets";
|
import { IssuesIcon, SaveIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
import { ButtonStyled, NewModal } from '@modrinth/ui'
|
||||||
import type { Backup } from "@modrinth/utils";
|
import type { Backup } from '@modrinth/utils'
|
||||||
import { computed,nextTick, ref } from "vue";
|
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<{
|
const props = defineProps<{
|
||||||
server: ModrinthServer;
|
server: ModrinthServer
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const modal = ref<InstanceType<typeof NewModal>>();
|
const modal = ref<InstanceType<typeof NewModal>>()
|
||||||
const input = ref<HTMLInputElement>();
|
const input = ref<HTMLInputElement>()
|
||||||
const backupName = ref("");
|
const backupName = ref('')
|
||||||
const originalName = ref("");
|
const originalName = ref('')
|
||||||
const isRenaming = ref(false);
|
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(() => {
|
const nameExists = computed(() => {
|
||||||
if (!props.server.backups?.data || trimmedName.value === originalName.value || isRenaming.value) {
|
if (!props.server.backups?.data || trimmedName.value === originalName.value || isRenaming.value) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return props.server.backups.data.some(
|
return props.server.backups.data.some(
|
||||||
(backup: Backup) => backup.name.trim().toLowerCase() === trimmedName.value.toLowerCase(),
|
(backup: Backup) => backup.name.trim().toLowerCase() === trimmedName.value.toLowerCase(),
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
|
|
||||||
const backupNumber = computed(
|
const backupNumber = computed(
|
||||||
() => (props.server.backups?.data?.findIndex((b) => b.id === currentBackup.value?.id) ?? 0) + 1,
|
() => (props.server.backups?.data?.findIndex((b) => b.id === currentBackup.value?.id) ?? 0) + 1,
|
||||||
);
|
)
|
||||||
|
|
||||||
const focusInput = () => {
|
const focusInput = () => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
input.value?.focus();
|
input.value?.focus()
|
||||||
}, 100);
|
}, 100)
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
function show(backup: Backup) {
|
function show(backup: Backup) {
|
||||||
currentBackup.value = backup;
|
currentBackup.value = backup
|
||||||
backupName.value = backup.name;
|
backupName.value = backup.name
|
||||||
originalName.value = backup.name;
|
originalName.value = backup.name
|
||||||
isRenaming.value = false;
|
isRenaming.value = false
|
||||||
modal.value?.show();
|
modal.value?.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
modal.value?.hide();
|
modal.value?.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
const renameBackup = async () => {
|
const renameBackup = async () => {
|
||||||
if (!currentBackup.value) {
|
if (!currentBackup.value) {
|
||||||
addNotification({
|
addNotification({
|
||||||
type: "error",
|
type: 'error',
|
||||||
title: "Error renaming backup",
|
title: 'Error renaming backup',
|
||||||
text: "Current backup is null",
|
text: 'Current backup is null',
|
||||||
});
|
})
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trimmedName.value === originalName.value) {
|
if (trimmedName.value === originalName.value) {
|
||||||
hide();
|
hide()
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isRenaming.value = true;
|
isRenaming.value = true
|
||||||
try {
|
try {
|
||||||
let newName = trimmedName.value;
|
let newName = trimmedName.value
|
||||||
|
|
||||||
if (newName.length === 0) {
|
if (newName.length === 0) {
|
||||||
newName = `Backup #${backupNumber.value}`;
|
newName = `Backup #${backupNumber.value}`
|
||||||
}
|
}
|
||||||
|
|
||||||
await props.server.backups?.rename(currentBackup.value.id, newName);
|
await props.server.backups?.rename(currentBackup.value.id, newName)
|
||||||
hide();
|
hide()
|
||||||
await props.server.refresh();
|
await props.server.refresh()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
addNotification({ type: "error", title: "Error renaming backup", text: message });
|
addNotification({ type: 'error', title: 'Error renaming backup', text: message })
|
||||||
} finally {
|
} finally {
|
||||||
hide();
|
hide()
|
||||||
isRenaming.value = false;
|
isRenaming.value = false
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show,
|
show,
|
||||||
hide,
|
hide,
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -17,45 +17,45 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { NewModal } from "@modrinth/ui";
|
import type { NewModal } from '@modrinth/ui'
|
||||||
import { ConfirmModal } from "@modrinth/ui";
|
import { ConfirmModal } from '@modrinth/ui'
|
||||||
import type { Backup } from "@modrinth/utils";
|
import type { Backup } from '@modrinth/utils'
|
||||||
import { ref } from "vue";
|
import { ref } from 'vue'
|
||||||
|
|
||||||
import BackupItem from "~/components/ui/servers/BackupItem.vue";
|
import BackupItem from '~/components/ui/servers/BackupItem.vue'
|
||||||
import type { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: ModrinthServer;
|
server: ModrinthServer
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const modal = ref<InstanceType<typeof NewModal>>();
|
const modal = ref<InstanceType<typeof NewModal>>()
|
||||||
const currentBackup = ref<Backup | null>(null);
|
const currentBackup = ref<Backup | null>(null)
|
||||||
|
|
||||||
function show(backup: Backup) {
|
function show(backup: Backup) {
|
||||||
currentBackup.value = backup;
|
currentBackup.value = backup
|
||||||
modal.value?.show();
|
modal.value?.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
const restoreBackup = async () => {
|
const restoreBackup = async () => {
|
||||||
if (!currentBackup.value) {
|
if (!currentBackup.value) {
|
||||||
addNotification({
|
addNotification({
|
||||||
type: "error",
|
type: 'error',
|
||||||
title: "Failed to restore backup",
|
title: 'Failed to restore backup',
|
||||||
text: "Current backup is null",
|
text: 'Current backup is null',
|
||||||
});
|
})
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await props.server.backups?.restore(currentBackup.value.id);
|
await props.server.backups?.restore(currentBackup.value.id)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
addNotification({ type: "error", title: "Failed to restore backup", text: message });
|
addNotification({ type: 'error', title: 'Failed to restore backup', text: message })
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show,
|
show,
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -40,7 +40,7 @@
|
|||||||
<ButtonStyled color="brand">
|
<ButtonStyled color="brand">
|
||||||
<button :disabled="!hasChanges || isSaving" @click="saveSettings">
|
<button :disabled="!hasChanges || isSaving" @click="saveSettings">
|
||||||
<SaveIcon class="h-5 w-5" />
|
<SaveIcon class="h-5 w-5" />
|
||||||
{{ isSaving ? "Saving..." : "Save changes" }}
|
{{ isSaving ? 'Saving...' : 'Save changes' }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
@ -56,114 +56,114 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SaveIcon,XIcon } from "@modrinth/assets";
|
import { SaveIcon, XIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
import { ButtonStyled, NewModal } from '@modrinth/ui'
|
||||||
import { computed,ref } from "vue";
|
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<{
|
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 initialSettings = ref<{ interval: number; enabled: boolean } | null>(null)
|
||||||
const autoBackupEnabled = ref(false);
|
const autoBackupEnabled = ref(false)
|
||||||
const isLoadingSettings = ref(true);
|
const isLoadingSettings = ref(true)
|
||||||
const isSaving = ref(false);
|
const isSaving = ref(false)
|
||||||
|
|
||||||
const backupIntervals = {
|
const backupIntervals = {
|
||||||
"Every 3 hours": 3,
|
'Every 3 hours': 3,
|
||||||
"Every 6 hours": 6,
|
'Every 6 hours': 6,
|
||||||
"Every 12 hours": 12,
|
'Every 12 hours': 12,
|
||||||
Daily: 24,
|
Daily: 24,
|
||||||
};
|
}
|
||||||
|
|
||||||
const backupIntervalsLabel = ref<keyof typeof backupIntervals>("Every 6 hours");
|
const backupIntervalsLabel = ref<keyof typeof backupIntervals>('Every 6 hours')
|
||||||
|
|
||||||
const autoBackupInterval = computed({
|
const autoBackupInterval = computed({
|
||||||
get: () => backupIntervals[backupIntervalsLabel.value],
|
get: () => backupIntervals[backupIntervalsLabel.value],
|
||||||
set: (value) => {
|
set: (value) => {
|
||||||
const [label] =
|
const [label] =
|
||||||
Object.entries(backupIntervals).find(([_, interval]) => interval === value) || [];
|
Object.entries(backupIntervals).find(([_, interval]) => interval === value) || []
|
||||||
if (label) backupIntervalsLabel.value = label as keyof typeof backupIntervals;
|
if (label) backupIntervalsLabel.value = label as keyof typeof backupIntervals
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const hasChanges = computed(() => {
|
const hasChanges = computed(() => {
|
||||||
if (!initialSettings.value) return false;
|
if (!initialSettings.value) return false
|
||||||
|
|
||||||
return (
|
return (
|
||||||
autoBackupEnabled.value !== initialSettings.value.enabled ||
|
autoBackupEnabled.value !== initialSettings.value.enabled ||
|
||||||
(initialSettings.value.enabled && autoBackupInterval.value !== initialSettings.value.interval)
|
(initialSettings.value.enabled && autoBackupInterval.value !== initialSettings.value.interval)
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
|
|
||||||
const fetchSettings = async () => {
|
const fetchSettings = async () => {
|
||||||
isLoadingSettings.value = true;
|
isLoadingSettings.value = true
|
||||||
try {
|
try {
|
||||||
const settings = await props.server.backups?.getAutoBackup();
|
const settings = await props.server.backups?.getAutoBackup()
|
||||||
initialSettings.value = settings as { interval: number; enabled: boolean };
|
initialSettings.value = settings as { interval: number; enabled: boolean }
|
||||||
autoBackupEnabled.value = settings?.enabled ?? false;
|
autoBackupEnabled.value = settings?.enabled ?? false
|
||||||
autoBackupInterval.value = settings?.interval || 6;
|
autoBackupInterval.value = settings?.interval || 6
|
||||||
return true;
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching backup settings:", error);
|
console.error('Error fetching backup settings:', error)
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "server",
|
group: 'server',
|
||||||
title: "Error",
|
title: 'Error',
|
||||||
text: "Failed to load backup settings",
|
text: 'Failed to load backup settings',
|
||||||
type: "error",
|
type: 'error',
|
||||||
});
|
})
|
||||||
return false;
|
return false
|
||||||
} finally {
|
} finally {
|
||||||
isLoadingSettings.value = false;
|
isLoadingSettings.value = false
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const saveSettings = async () => {
|
const saveSettings = async () => {
|
||||||
isSaving.value = true;
|
isSaving.value = true
|
||||||
try {
|
try {
|
||||||
await props.server.backups?.updateAutoBackup(
|
await props.server.backups?.updateAutoBackup(
|
||||||
autoBackupEnabled.value ? "enable" : "disable",
|
autoBackupEnabled.value ? 'enable' : 'disable',
|
||||||
autoBackupInterval.value,
|
autoBackupInterval.value,
|
||||||
);
|
)
|
||||||
|
|
||||||
initialSettings.value = {
|
initialSettings.value = {
|
||||||
enabled: autoBackupEnabled.value,
|
enabled: autoBackupEnabled.value,
|
||||||
interval: autoBackupInterval.value,
|
interval: autoBackupInterval.value,
|
||||||
};
|
}
|
||||||
|
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "server",
|
group: 'server',
|
||||||
title: "Success",
|
title: 'Success',
|
||||||
text: "Backup settings updated successfully",
|
text: 'Backup settings updated successfully',
|
||||||
type: "success",
|
type: 'success',
|
||||||
});
|
})
|
||||||
|
|
||||||
modal.value?.hide();
|
modal.value?.hide()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving backup settings:", error);
|
console.error('Error saving backup settings:', error)
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "server",
|
group: 'server',
|
||||||
title: "Error",
|
title: 'Error',
|
||||||
text: "Failed to save backup settings",
|
text: 'Failed to save backup settings',
|
||||||
type: "error",
|
type: 'error',
|
||||||
});
|
})
|
||||||
} finally {
|
} finally {
|
||||||
isSaving.value = false;
|
isSaving.value = false
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: async () => {
|
show: async () => {
|
||||||
const success = await fetchSettings();
|
const success = await fetchSettings()
|
||||||
if (success) {
|
if (success) {
|
||||||
modal.value?.show();
|
modal.value?.show()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -89,8 +89,8 @@
|
|||||||
noCompatibleVersions
|
noCompatibleVersions
|
||||||
? `No compatible versions of this ${type.toLowerCase()} were found`
|
? `No compatible versions of this ${type.toLowerCase()} were found`
|
||||||
: versionFilter
|
: versionFilter
|
||||||
? "Game version and platform is provided by the server"
|
? 'Game version and platform is provided by the server'
|
||||||
: "Incompatible game version and platform versions are unlocked"
|
: 'Incompatible game version and platform versions are unlocked'
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
@ -133,8 +133,8 @@
|
|||||||
<div class="w-full truncate text-left">
|
<div class="w-full truncate text-left">
|
||||||
{{
|
{{
|
||||||
filtersRef?.selectedPlatforms.length === 0
|
filtersRef?.selectedPlatforms.length === 0
|
||||||
? "All platforms"
|
? 'All platforms'
|
||||||
: filtersRef?.selectedPlatforms.map((x) => formatCategory(x)).join(", ")
|
: filtersRef?.selectedPlatforms.map((x) => formatCategory(x)).join(', ')
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -143,8 +143,8 @@
|
|||||||
<div class="w-full truncate text-left">
|
<div class="w-full truncate text-left">
|
||||||
{{
|
{{
|
||||||
filtersRef?.selectedGameVersions.length === 0
|
filtersRef?.selectedGameVersions.length === 0
|
||||||
? "All game versions"
|
? 'All game versions'
|
||||||
: filtersRef?.selectedGameVersions.join(", ")
|
: filtersRef?.selectedGameVersions.join(', ')
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -156,19 +156,19 @@
|
|||||||
:disabled="gameVersions.length < 2 && platforms.length < 2"
|
:disabled="gameVersions.length < 2 && platforms.length < 2"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
versionFilter = !versionFilter;
|
versionFilter = !versionFilter
|
||||||
setInitialFilters();
|
setInitialFilters()
|
||||||
updateFiltersToUi();
|
updateFiltersToUi()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<LockOpenIcon />
|
<LockOpenIcon />
|
||||||
{{
|
{{
|
||||||
gameVersions.length < 2 && platforms.length < 2
|
gameVersions.length < 2 && platforms.length < 2
|
||||||
? "No other platforms or versions available"
|
? 'No other platforms or versions available'
|
||||||
: versionFilter
|
: versionFilter
|
||||||
? "Unlock"
|
? 'Unlock'
|
||||||
: "Return to compatibility"
|
: 'Return to compatibility'
|
||||||
}}
|
}}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
@ -235,81 +235,81 @@ import {
|
|||||||
GameIcon,
|
GameIcon,
|
||||||
LockOpenIcon,
|
LockOpenIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from "@modrinth/assets";
|
} from '@modrinth/assets'
|
||||||
import { Admonition, Avatar, ButtonStyled, CopyCode, NewModal } from "@modrinth/ui";
|
import { Admonition, Avatar, ButtonStyled, CopyCode, NewModal } from '@modrinth/ui'
|
||||||
import TagItem from "@modrinth/ui/src/components/base/TagItem.vue";
|
import TagItem from '@modrinth/ui/src/components/base/TagItem.vue'
|
||||||
import { formatCategory, formatVersionsForDisplay, type Mod, type Version } from "@modrinth/utils";
|
import { formatCategory, formatVersionsForDisplay, type Mod, type Version } from '@modrinth/utils'
|
||||||
import { computed,ref } from "vue";
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
import Accordion from "~/components/ui/Accordion.vue";
|
import Accordion from '~/components/ui/Accordion.vue'
|
||||||
import Checkbox from "~/components/ui/Checkbox.vue";
|
import Checkbox from '~/components/ui/Checkbox.vue'
|
||||||
import ContentVersionFilter, {
|
import ContentVersionFilter, {
|
||||||
type ListedGameVersion,
|
type ListedGameVersion,
|
||||||
type ListedPlatform,
|
type ListedPlatform,
|
||||||
} from "~/components/ui/servers/ContentVersionFilter.vue";
|
} from '~/components/ui/servers/ContentVersionFilter.vue'
|
||||||
import LoaderIcon from "~/components/ui/servers/icons/LoaderIcon.vue";
|
import LoaderIcon from '~/components/ui/servers/icons/LoaderIcon.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
type: "Mod" | "Plugin";
|
type: 'Mod' | 'Plugin'
|
||||||
loader: string;
|
loader: string
|
||||||
gameVersion: string;
|
gameVersion: string
|
||||||
modPack: boolean;
|
modPack: boolean
|
||||||
serverId: string;
|
serverId: string
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
interface ContentItem extends Mod {
|
interface ContentItem extends Mod {
|
||||||
changing?: boolean;
|
changing?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EditVersion extends Version {
|
interface EditVersion extends Version {
|
||||||
installed: boolean;
|
installed: boolean
|
||||||
upgrade?: boolean;
|
upgrade?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const modModal = ref();
|
const modModal = ref()
|
||||||
const modDetails = ref<ContentItem>();
|
const modDetails = ref<ContentItem>()
|
||||||
const currentVersions = ref<EditVersion[] | null>(null);
|
const currentVersions = ref<EditVersion[] | null>(null)
|
||||||
const versionsLoading = ref(false);
|
const versionsLoading = ref(false)
|
||||||
const versionsError = ref("");
|
const versionsError = ref('')
|
||||||
const showBetaAlphaReleases = ref(false);
|
const showBetaAlphaReleases = ref(false)
|
||||||
const unlockFilterAccordion = ref();
|
const unlockFilterAccordion = ref()
|
||||||
const versionFilter = ref(true);
|
const versionFilter = ref(true)
|
||||||
const tags = useTags();
|
const tags = useTags()
|
||||||
const noCompatibleVersions = ref(false);
|
const noCompatibleVersions = ref(false)
|
||||||
|
|
||||||
const { pluginLoaders, modLoaders } = tags.value.loaders.reduce(
|
const { pluginLoaders, modLoaders } = tags.value.loaders.reduce(
|
||||||
(acc, tag) => {
|
(acc, tag) => {
|
||||||
if (tag.supported_project_types.includes("plugin")) {
|
if (tag.supported_project_types.includes('plugin')) {
|
||||||
acc.pluginLoaders.push(tag.name);
|
acc.pluginLoaders.push(tag.name)
|
||||||
}
|
}
|
||||||
if (tag.supported_project_types.includes("mod")) {
|
if (tag.supported_project_types.includes('mod')) {
|
||||||
acc.modLoaders.push(tag.name);
|
acc.modLoaders.push(tag.name)
|
||||||
}
|
}
|
||||||
return acc;
|
return acc
|
||||||
},
|
},
|
||||||
{ pluginLoaders: [] as string[], modLoaders: [] as string[] },
|
{ pluginLoaders: [] as string[], modLoaders: [] as string[] },
|
||||||
);
|
)
|
||||||
|
|
||||||
const selectedVersion = ref();
|
const selectedVersion = ref()
|
||||||
const filtersRef: Ref<InstanceType<typeof ContentVersionFilter> | null> = ref(null);
|
const filtersRef: Ref<InstanceType<typeof ContentVersionFilter> | null> = ref(null)
|
||||||
interface SelectedContentFilters {
|
interface SelectedContentFilters {
|
||||||
selectedGameVersions: string[];
|
selectedGameVersions: string[]
|
||||||
selectedPlatforms: string[];
|
selectedPlatforms: string[]
|
||||||
}
|
}
|
||||||
const selectedFilters = ref<SelectedContentFilters>({
|
const selectedFilters = ref<SelectedContentFilters>({
|
||||||
selectedGameVersions: [],
|
selectedGameVersions: [],
|
||||||
selectedPlatforms: [],
|
selectedPlatforms: [],
|
||||||
});
|
})
|
||||||
|
|
||||||
const backwardCompatPlatformMap = {
|
const backwardCompatPlatformMap = {
|
||||||
purpur: ["purpur", "paper", "spigot", "bukkit"],
|
purpur: ['purpur', 'paper', 'spigot', 'bukkit'],
|
||||||
paper: ["paper", "spigot", "bukkit"],
|
paper: ['paper', 'spigot', 'bukkit'],
|
||||||
spigot: ["spigot", "bukkit"],
|
spigot: ['spigot', 'bukkit'],
|
||||||
};
|
}
|
||||||
|
|
||||||
const platforms = ref<ListedPlatform[]>([]);
|
const platforms = ref<ListedPlatform[]>([])
|
||||||
const gameVersions = ref<ListedGameVersion[]>([]);
|
const gameVersions = ref<ListedGameVersion[]>([])
|
||||||
const initPlatform = ref<string>("");
|
const initPlatform = ref<string>('')
|
||||||
|
|
||||||
const setInitialFilters = () => {
|
const setInitialFilters = () => {
|
||||||
selectedFilters.value = {
|
selectedFilters.value = {
|
||||||
@ -319,29 +319,29 @@ const setInitialFilters = () => {
|
|||||||
gameVersions.value[0]?.name,
|
gameVersions.value[0]?.name,
|
||||||
],
|
],
|
||||||
selectedPlatforms: [initPlatform.value],
|
selectedPlatforms: [initPlatform.value],
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const updateFiltersToUi = () => {
|
const updateFiltersToUi = () => {
|
||||||
if (!filtersRef.value) return;
|
if (!filtersRef.value) return
|
||||||
filtersRef.value.selectedGameVersions = selectedFilters.value.selectedGameVersions;
|
filtersRef.value.selectedGameVersions = selectedFilters.value.selectedGameVersions
|
||||||
filtersRef.value.selectedPlatforms = selectedFilters.value.selectedPlatforms;
|
filtersRef.value.selectedPlatforms = selectedFilters.value.selectedPlatforms
|
||||||
|
|
||||||
selectedVersion.value = filteredVersions.value[0];
|
selectedVersion.value = filteredVersions.value[0]
|
||||||
};
|
}
|
||||||
|
|
||||||
const updateFiltersFromUi = (event: { g: string[]; l: string[] }) => {
|
const updateFiltersFromUi = (event: { g: string[]; l: string[] }) => {
|
||||||
selectedFilters.value = {
|
selectedFilters.value = {
|
||||||
selectedGameVersions: event.g,
|
selectedGameVersions: event.g,
|
||||||
selectedPlatforms: event.l,
|
selectedPlatforms: event.l,
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const filteredVersions = computed(() => {
|
const filteredVersions = computed(() => {
|
||||||
if (!currentVersions.value) return [];
|
if (!currentVersions.value) return []
|
||||||
|
|
||||||
const versionsWithoutReleaseFilter = currentVersions.value.filter((version: EditVersion) => {
|
const versionsWithoutReleaseFilter = currentVersions.value.filter((version: EditVersion) => {
|
||||||
if (version.installed) return true;
|
if (version.installed) return true
|
||||||
return (
|
return (
|
||||||
filtersRef.value?.selectedPlatforms.every((platform) =>
|
filtersRef.value?.selectedPlatforms.every((platform) =>
|
||||||
(
|
(
|
||||||
@ -353,42 +353,40 @@ const filteredVersions = computed(() => {
|
|||||||
filtersRef.value?.selectedGameVersions.every((gameVersion) =>
|
filtersRef.value?.selectedGameVersions.every((gameVersion) =>
|
||||||
version.game_versions.includes(gameVersion),
|
version.game_versions.includes(gameVersion),
|
||||||
)
|
)
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
|
|
||||||
const versionTypes = new Set(
|
const versionTypes = new Set(versionsWithoutReleaseFilter.map((v: EditVersion) => v.version_type))
|
||||||
versionsWithoutReleaseFilter.map((v: EditVersion) => v.version_type),
|
const releaseVersions = versionTypes.has('release')
|
||||||
);
|
const betaVersions = versionTypes.has('beta')
|
||||||
const releaseVersions = versionTypes.has("release");
|
const alphaVersions = versionTypes.has('alpha')
|
||||||
const betaVersions = versionTypes.has("beta");
|
|
||||||
const alphaVersions = versionTypes.has("alpha");
|
|
||||||
|
|
||||||
const versions = versionsWithoutReleaseFilter.filter((version: EditVersion) => {
|
const versions = versionsWithoutReleaseFilter.filter((version: EditVersion) => {
|
||||||
if (showBetaAlphaReleases.value || version.installed) return true;
|
if (showBetaAlphaReleases.value || version.installed) return true
|
||||||
return releaseVersions
|
return releaseVersions
|
||||||
? version.version_type === "release"
|
? version.version_type === 'release'
|
||||||
: betaVersions
|
: betaVersions
|
||||||
? version.version_type === "beta"
|
? version.version_type === 'beta'
|
||||||
: alphaVersions
|
: alphaVersions
|
||||||
? version.version_type === "alpha"
|
? version.version_type === 'alpha'
|
||||||
: false;
|
: false
|
||||||
});
|
})
|
||||||
|
|
||||||
return versions.map((version: EditVersion) => {
|
return versions.map((version: EditVersion) => {
|
||||||
let suffix = "";
|
let suffix = ''
|
||||||
|
|
||||||
if (version.version_type === "alpha" && releaseVersions && betaVersions) {
|
if (version.version_type === 'alpha' && releaseVersions && betaVersions) {
|
||||||
suffix += " (alpha)";
|
suffix += ' (alpha)'
|
||||||
} else if (version.version_type === "beta" && releaseVersions) {
|
} else if (version.version_type === 'beta' && releaseVersions) {
|
||||||
suffix += " (beta)";
|
suffix += ' (beta)'
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...version,
|
...version,
|
||||||
version_number: version.version_number + suffix,
|
version_number: version.version_number + suffix,
|
||||||
};
|
}
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
||||||
const formattedVersions = computed(() => {
|
const formattedVersions = computed(() => {
|
||||||
return {
|
return {
|
||||||
@ -400,134 +398,134 @@ const formattedVersions = computed(() => {
|
|||||||
.sort((firstLoader: string, secondLoader: string) => {
|
.sort((firstLoader: string, secondLoader: string) => {
|
||||||
const loaderList = backwardCompatPlatformMap[
|
const loaderList = backwardCompatPlatformMap[
|
||||||
props.loader as keyof typeof backwardCompatPlatformMap
|
props.loader as keyof typeof backwardCompatPlatformMap
|
||||||
] || [props.loader];
|
] || [props.loader]
|
||||||
|
|
||||||
const firstLoaderPosition = loaderList.indexOf(firstLoader.toLowerCase());
|
const firstLoaderPosition = loaderList.indexOf(firstLoader.toLowerCase())
|
||||||
const secondLoaderPosition = loaderList.indexOf(secondLoader.toLowerCase());
|
const secondLoaderPosition = loaderList.indexOf(secondLoader.toLowerCase())
|
||||||
|
|
||||||
if (firstLoaderPosition === -1 && secondLoaderPosition === -1) return 0;
|
if (firstLoaderPosition === -1 && secondLoaderPosition === -1) return 0
|
||||||
if (firstLoaderPosition === -1) return 1;
|
if (firstLoaderPosition === -1) return 1
|
||||||
if (secondLoaderPosition === -1) return -1;
|
if (secondLoaderPosition === -1) return -1
|
||||||
return firstLoaderPosition - secondLoaderPosition;
|
return firstLoaderPosition - secondLoaderPosition
|
||||||
})
|
})
|
||||||
.map((loader: string) => formatCategory(loader)),
|
.map((loader: string) => formatCategory(loader)),
|
||||||
};
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
async function show(mod: ContentItem) {
|
async function show(mod: ContentItem) {
|
||||||
versionFilter.value = true;
|
versionFilter.value = true
|
||||||
modModal.value.show();
|
modModal.value.show()
|
||||||
versionsLoading.value = true;
|
versionsLoading.value = true
|
||||||
modDetails.value = mod;
|
modDetails.value = mod
|
||||||
versionsError.value = "";
|
versionsError.value = ''
|
||||||
currentVersions.value = null;
|
currentVersions.value = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await useBaseFetch(`project/${mod.project_id}/version`, {}, false);
|
const result = await useBaseFetch(`project/${mod.project_id}/version`, {}, false)
|
||||||
if (
|
if (
|
||||||
Array.isArray(result) &&
|
Array.isArray(result) &&
|
||||||
result.every(
|
result.every(
|
||||||
(item) =>
|
(item) =>
|
||||||
"id" in item &&
|
'id' in item &&
|
||||||
"version_number" in item &&
|
'version_number' in item &&
|
||||||
"version_type" in item &&
|
'version_type' in item &&
|
||||||
"loaders" in item &&
|
'loaders' in item &&
|
||||||
"game_versions" in item,
|
'game_versions' in item,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
currentVersions.value = result as EditVersion[];
|
currentVersions.value = result as EditVersion[]
|
||||||
} else {
|
} 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
|
// find the installed version and move it to the top of the list
|
||||||
const currentModIndex = currentVersions.value.findIndex(
|
const currentModIndex = currentVersions.value.findIndex(
|
||||||
(item: { id: string }) => item.id === mod.version_id,
|
(item: { id: string }) => item.id === mod.version_id,
|
||||||
);
|
)
|
||||||
if (currentModIndex === -1) {
|
if (currentModIndex === -1) {
|
||||||
currentVersions.value[currentModIndex] = {
|
currentVersions.value[currentModIndex] = {
|
||||||
...currentVersions.value[currentModIndex],
|
...currentVersions.value[currentModIndex],
|
||||||
installed: true,
|
installed: true,
|
||||||
version_number: `${mod.version_number} (current) (external)`,
|
version_number: `${mod.version_number} (current) (external)`,
|
||||||
};
|
}
|
||||||
} else {
|
} else {
|
||||||
currentVersions.value[currentModIndex].version_number = `${mod.version_number} (current)`;
|
currentVersions.value[currentModIndex].version_number = `${mod.version_number} (current)`
|
||||||
currentVersions.value[currentModIndex].installed = true;
|
currentVersions.value[currentModIndex].installed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// initially filter the platform and game versions for the server config
|
// initially filter the platform and game versions for the server config
|
||||||
const platformSet = new Set<string>();
|
const platformSet = new Set<string>()
|
||||||
const gameVersionSet = new Set<string>();
|
const gameVersionSet = new Set<string>()
|
||||||
for (const version of currentVersions.value) {
|
for (const version of currentVersions.value) {
|
||||||
for (const loader of version.loaders) {
|
for (const loader of version.loaders) {
|
||||||
platformSet.add(loader);
|
platformSet.add(loader)
|
||||||
}
|
}
|
||||||
for (const gameVersion of version.game_versions) {
|
for (const gameVersion of version.game_versions) {
|
||||||
gameVersionSet.add(gameVersion);
|
gameVersionSet.add(gameVersion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (gameVersionSet.size > 0) {
|
if (gameVersionSet.size > 0) {
|
||||||
const filteredGameVersions = tags.value.gameVersions.filter((x) =>
|
const filteredGameVersions = tags.value.gameVersions.filter((x) =>
|
||||||
gameVersionSet.has(x.version),
|
gameVersionSet.has(x.version),
|
||||||
);
|
)
|
||||||
|
|
||||||
gameVersions.value = filteredGameVersions.map((x) => ({
|
gameVersions.value = filteredGameVersions.map((x) => ({
|
||||||
name: x.version,
|
name: x.version,
|
||||||
release: x.version_type === "release",
|
release: x.version_type === 'release',
|
||||||
}));
|
}))
|
||||||
}
|
}
|
||||||
if (platformSet.size > 0) {
|
if (platformSet.size > 0) {
|
||||||
const tempPlatforms = Array.from(platformSet).map((platform) => ({
|
const tempPlatforms = Array.from(platformSet).map((platform) => ({
|
||||||
name: platform,
|
name: platform,
|
||||||
isType:
|
isType:
|
||||||
props.type === "Plugin"
|
props.type === 'Plugin'
|
||||||
? pluginLoaders.includes(platform)
|
? pluginLoaders.includes(platform)
|
||||||
: props.type === "Mod"
|
: props.type === 'Mod'
|
||||||
? modLoaders.includes(platform)
|
? modLoaders.includes(platform)
|
||||||
: false,
|
: false,
|
||||||
}));
|
}))
|
||||||
platforms.value = tempPlatforms;
|
platforms.value = tempPlatforms
|
||||||
}
|
}
|
||||||
|
|
||||||
// set default platform
|
// set default platform
|
||||||
const defaultPlatform = Array.from(platformSet)[0];
|
const defaultPlatform = Array.from(platformSet)[0]
|
||||||
initPlatform.value = platformSet.has(props.loader)
|
initPlatform.value = platformSet.has(props.loader)
|
||||||
? props.loader
|
? props.loader
|
||||||
: props.loader in backwardCompatPlatformMap
|
: props.loader in backwardCompatPlatformMap
|
||||||
? backwardCompatPlatformMap[props.loader as keyof typeof backwardCompatPlatformMap].find(
|
? backwardCompatPlatformMap[props.loader as keyof typeof backwardCompatPlatformMap].find(
|
||||||
(p) => platformSet.has(p),
|
(p) => platformSet.has(p),
|
||||||
) || defaultPlatform
|
) || defaultPlatform
|
||||||
: defaultPlatform;
|
: defaultPlatform
|
||||||
|
|
||||||
// check if there's nothing compatible with the server config
|
// check if there's nothing compatible with the server config
|
||||||
noCompatibleVersions.value =
|
noCompatibleVersions.value =
|
||||||
!platforms.value.some((p) => p.isType) ||
|
!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) {
|
if (noCompatibleVersions.value) {
|
||||||
unlockFilterAccordion.value.open();
|
unlockFilterAccordion.value.open()
|
||||||
versionFilter.value = false;
|
versionFilter.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
setInitialFilters();
|
setInitialFilters()
|
||||||
versionsLoading.value = false;
|
versionsLoading.value = false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading versions:", error);
|
console.error('Error loading versions:', error)
|
||||||
versionsError.value = error instanceof Error ? error.message : "Unknown";
|
versionsError.value = error instanceof Error ? error.message : 'Unknown'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
changeVersion: [string];
|
changeVersion: [string]
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
function emitChangeModVersion() {
|
function emitChangeModVersion() {
|
||||||
if (!selectedVersion.value) return;
|
if (!selectedVersion.value) return
|
||||||
emit("changeVersion", selectedVersion.value.id.toString());
|
emit('changeVersion', selectedVersion.value.id.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show,
|
show,
|
||||||
hide: () => modModal.value.hide(),
|
hide: () => modModal.value.hide(),
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -57,114 +57,114 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { FilterIcon } from "@modrinth/assets";
|
import { FilterIcon } from '@modrinth/assets'
|
||||||
import Checkbox from "@modrinth/ui/src/components/base/Checkbox.vue";
|
import Checkbox from '@modrinth/ui/src/components/base/Checkbox.vue'
|
||||||
import ManySelect from "@modrinth/ui/src/components/base/ManySelect.vue";
|
import ManySelect from '@modrinth/ui/src/components/base/ManySelect.vue'
|
||||||
import { formatCategory, type GameVersionTag,type Version } from "@modrinth/utils";
|
import { formatCategory, type GameVersionTag, type Version } from '@modrinth/utils'
|
||||||
import { computed,ref } from "vue";
|
import { computed, ref } from 'vue'
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
export type ListedGameVersion = {
|
export type ListedGameVersion = {
|
||||||
name: string;
|
name: string
|
||||||
release: boolean;
|
release: boolean
|
||||||
};
|
}
|
||||||
|
|
||||||
export type ListedPlatform = {
|
export type ListedPlatform = {
|
||||||
name: string;
|
name: string
|
||||||
isType: boolean;
|
isType: boolean
|
||||||
};
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
versions: Version[];
|
versions: Version[]
|
||||||
gameVersions: GameVersionTag[];
|
gameVersions: GameVersionTag[]
|
||||||
listedGameVersions: ListedGameVersion[];
|
listedGameVersions: ListedGameVersion[]
|
||||||
listedPlatforms: ListedPlatform[];
|
listedPlatforms: ListedPlatform[]
|
||||||
baseId?: string;
|
baseId?: string
|
||||||
type: "Mod" | "Plugin";
|
type: 'Mod' | 'Plugin'
|
||||||
platformTags: {
|
platformTags: {
|
||||||
name: string;
|
name: string
|
||||||
supported_project_types: string[];
|
supported_project_types: string[]
|
||||||
}[];
|
}[]
|
||||||
disabled?: boolean;
|
disabled?: boolean
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits(["update:query"]);
|
const emit = defineEmits(['update:query'])
|
||||||
const route = useRoute();
|
const route = useRoute()
|
||||||
|
|
||||||
const showSnapshots = ref(false);
|
const showSnapshots = ref(false)
|
||||||
const hasAnySnapshots = computed(() => {
|
const hasAnySnapshots = computed(() => {
|
||||||
return props.versions.some((x) =>
|
return props.versions.some((x) =>
|
||||||
props.gameVersions.some(
|
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(() => {
|
const hasOnlySnapshots = computed(() => {
|
||||||
return props.versions.every((version) => {
|
return props.versions.every((version) => {
|
||||||
return version.game_versions.every((gv) => {
|
return version.game_versions.every((gv) => {
|
||||||
const matched = props.gameVersions.find((tag) => tag.version === gv);
|
const matched = props.gameVersions.find((tag) => tag.version === gv)
|
||||||
return matched && matched.version_type !== "release";
|
return matched && matched.version_type !== 'release'
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
||||||
const hasAnyUnsupportedPlatforms = computed(() => {
|
const hasAnyUnsupportedPlatforms = computed(() => {
|
||||||
return props.listedPlatforms.some((x) => !x.isType);
|
return props.listedPlatforms.some((x) => !x.isType)
|
||||||
});
|
})
|
||||||
|
|
||||||
const hasOnlyUnsupportedPlatforms = computed(() => {
|
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 filterOptions = computed(() => {
|
||||||
const filters: Record<"gameVersion" | "platform", string[]> = {
|
const filters: Record<'gameVersion' | 'platform', string[]> = {
|
||||||
gameVersion: [],
|
gameVersion: [],
|
||||||
platform: [],
|
platform: [],
|
||||||
};
|
}
|
||||||
|
|
||||||
filters.gameVersion = props.listedGameVersions
|
filters.gameVersion = props.listedGameVersions
|
||||||
.filter((x) => {
|
.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
|
filters.platform = props.listedPlatforms
|
||||||
.filter((x) => {
|
.filter((x) => {
|
||||||
return !showSupportedPlatformsOnly.value || hasOnlyUnsupportedPlatforms.value
|
return !showSupportedPlatformsOnly.value || hasOnlyUnsupportedPlatforms.value
|
||||||
? true
|
? true
|
||||||
: x.isType;
|
: x.isType
|
||||||
})
|
})
|
||||||
.map((x) => x.name);
|
.map((x) => x.name)
|
||||||
|
|
||||||
return filters;
|
return filters
|
||||||
});
|
})
|
||||||
|
|
||||||
const selectedGameVersions = ref<string[]>([]);
|
const selectedGameVersions = ref<string[]>([])
|
||||||
const selectedPlatforms = ref<string[]>([]);
|
const selectedPlatforms = ref<string[]>([])
|
||||||
|
|
||||||
selectedGameVersions.value = route.query.g ? getArrayOrString(route.query.g) : [];
|
selectedGameVersions.value = route.query.g ? getArrayOrString(route.query.g) : []
|
||||||
selectedPlatforms.value = route.query.l ? getArrayOrString(route.query.l) : [];
|
selectedPlatforms.value = route.query.l ? getArrayOrString(route.query.l) : []
|
||||||
|
|
||||||
function updateFilters() {
|
function updateFilters() {
|
||||||
emit("update:query", {
|
emit('update:query', {
|
||||||
g: selectedGameVersions.value,
|
g: selectedGameVersions.value,
|
||||||
l: selectedPlatforms.value,
|
l: selectedPlatforms.value,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
selectedGameVersions,
|
selectedGameVersions,
|
||||||
selectedPlatforms,
|
selectedPlatforms,
|
||||||
});
|
})
|
||||||
|
|
||||||
function getArrayOrString(x: string | (string | null)[]): string[] {
|
function getArrayOrString(x: string | (string | null)[]): string[] {
|
||||||
if (typeof x === "string") {
|
if (typeof x === 'string') {
|
||||||
return [x];
|
return [x]
|
||||||
} else {
|
} else {
|
||||||
return x.filter((item): item is string => item !== null);
|
return x.filter((item): item is string => item !== null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -75,11 +75,11 @@ import {
|
|||||||
PackageOpenIcon,
|
PackageOpenIcon,
|
||||||
RightArrowIcon,
|
RightArrowIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from "@modrinth/assets";
|
} from '@modrinth/assets'
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
import { ButtonStyled } from '@modrinth/ui'
|
||||||
import { computed, ref,shallowRef } from "vue";
|
import { computed, ref, shallowRef } from 'vue'
|
||||||
import { renderToString } from "vue/server-renderer";
|
import { renderToString } from 'vue/server-renderer'
|
||||||
import { useRoute,useRouter } from "vue-router";
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
UiServersIconsCodeFileIcon,
|
UiServersIconsCodeFileIcon,
|
||||||
@ -87,327 +87,324 @@ import {
|
|||||||
UiServersIconsEarthIcon,
|
UiServersIconsEarthIcon,
|
||||||
UiServersIconsImageFileIcon,
|
UiServersIconsImageFileIcon,
|
||||||
UiServersIconsTextFileIcon,
|
UiServersIconsTextFileIcon,
|
||||||
} from "#components";
|
} from '#components'
|
||||||
import PaletteIcon from "~/assets/icons/palette.svg?component";
|
import PaletteIcon from '~/assets/icons/palette.svg?component'
|
||||||
|
|
||||||
interface FileItemProps {
|
interface FileItemProps {
|
||||||
name: string;
|
name: string
|
||||||
type: "directory" | "file";
|
type: 'directory' | 'file'
|
||||||
size?: number;
|
size?: number
|
||||||
count?: number;
|
count?: number
|
||||||
modified: number;
|
modified: number
|
||||||
created: number;
|
created: number
|
||||||
path: string;
|
path: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<FileItemProps>();
|
const props = defineProps<FileItemProps>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(
|
(
|
||||||
e: "rename" | "move" | "download" | "delete" | "edit" | "extract",
|
e: 'rename' | 'move' | 'download' | 'delete' | 'edit' | 'extract',
|
||||||
item: { name: string; type: string; path: string },
|
item: { name: string; type: string; path: string },
|
||||||
): void;
|
): void
|
||||||
(
|
(e: 'moveDirectTo', item: { name: string; type: string; path: string; destination: string }): void
|
||||||
e: "moveDirectTo",
|
(e: 'contextmenu', x: number, y: number): void
|
||||||
item: { name: string; type: string; path: string; destination: string },
|
}>()
|
||||||
): void;
|
|
||||||
(e: "contextmenu", x: number, y: number): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const isDragOver = ref(false);
|
const isDragOver = ref(false)
|
||||||
const isDragging = ref(false);
|
const isDragging = ref(false)
|
||||||
|
|
||||||
const codeExtensions = Object.freeze([
|
const codeExtensions = Object.freeze([
|
||||||
"json",
|
'json',
|
||||||
"json5",
|
'json5',
|
||||||
"jsonc",
|
'jsonc',
|
||||||
"java",
|
'java',
|
||||||
"kt",
|
'kt',
|
||||||
"kts",
|
'kts',
|
||||||
"sh",
|
'sh',
|
||||||
"bat",
|
'bat',
|
||||||
"ps1",
|
'ps1',
|
||||||
"yml",
|
'yml',
|
||||||
"yaml",
|
'yaml',
|
||||||
"toml",
|
'toml',
|
||||||
"js",
|
'js',
|
||||||
"ts",
|
'ts',
|
||||||
"py",
|
'py',
|
||||||
"rb",
|
'rb',
|
||||||
"php",
|
'php',
|
||||||
"html",
|
'html',
|
||||||
"css",
|
'css',
|
||||||
"cpp",
|
'cpp',
|
||||||
"c",
|
'c',
|
||||||
"h",
|
'h',
|
||||||
"rs",
|
'rs',
|
||||||
"go",
|
'go',
|
||||||
]);
|
])
|
||||||
|
|
||||||
const textExtensions = Object.freeze(["txt", "md", "log", "cfg", "conf", "properties", "ini"]);
|
const textExtensions = Object.freeze(['txt', 'md', 'log', 'cfg', 'conf', 'properties', 'ini'])
|
||||||
const imageExtensions = Object.freeze(["png", "jpg", "jpeg", "gif", "svg", "webp"]);
|
const imageExtensions = Object.freeze(['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp'])
|
||||||
const supportedArchiveExtensions = Object.freeze(["zip"]);
|
const supportedArchiveExtensions = Object.freeze(['zip'])
|
||||||
const units = Object.freeze(["B", "KB", "MB", "GB", "TB", "PB", "EB"]);
|
const units = Object.freeze(['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'])
|
||||||
|
|
||||||
const route = shallowRef(useRoute());
|
const route = shallowRef(useRoute())
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
|
|
||||||
const containerClasses = computed(() => [
|
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",
|
'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" : "",
|
isEditableFile.value ? 'cursor-pointer' : props.type === 'directory' ? 'cursor-pointer' : '',
|
||||||
isDragOver.value ? "bg-brand-highlight" : "",
|
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(() => [
|
const menuOptions = computed(() => [
|
||||||
{
|
{
|
||||||
id: "extract",
|
id: 'extract',
|
||||||
shown: isZip.value,
|
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,
|
divider: true,
|
||||||
shown: isZip.value,
|
shown: isZip.value,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "rename",
|
id: 'rename',
|
||||||
action: () => emit("rename", { name: props.name, type: props.type, path: props.path }),
|
action: () => emit('rename', { name: props.name, type: props.type, path: props.path }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "move",
|
id: 'move',
|
||||||
action: () => emit("move", { name: props.name, type: props.type, path: props.path }),
|
action: () => emit('move', { name: props.name, type: props.type, path: props.path }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "download",
|
id: 'download',
|
||||||
action: () => emit("download", { name: props.name, type: props.type, path: props.path }),
|
action: () => emit('download', { name: props.name, type: props.type, path: props.path }),
|
||||||
shown: props.type !== "directory",
|
shown: props.type !== 'directory',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "delete",
|
id: 'delete',
|
||||||
action: () => emit("delete", { name: props.name, type: props.type, path: props.path }),
|
action: () => emit('delete', { name: props.name, type: props.type, path: props.path }),
|
||||||
color: "red" as const,
|
color: 'red' as const,
|
||||||
},
|
},
|
||||||
]);
|
])
|
||||||
|
|
||||||
const iconComponent = computed(() => {
|
const iconComponent = computed(() => {
|
||||||
if (props.type === "directory") {
|
if (props.type === 'directory') {
|
||||||
if (props.name === "config") return UiServersIconsCogFolderIcon;
|
if (props.name === 'config') return UiServersIconsCogFolderIcon
|
||||||
if (props.name === "world") return UiServersIconsEarthIcon;
|
if (props.name === 'world') return UiServersIconsEarthIcon
|
||||||
if (props.name === "resourcepacks") return PaletteIcon;
|
if (props.name === 'resourcepacks') return PaletteIcon
|
||||||
return FolderOpenIcon;
|
return FolderOpenIcon
|
||||||
}
|
}
|
||||||
|
|
||||||
const ext = fileExtension.value;
|
const ext = fileExtension.value
|
||||||
if (codeExtensions.includes(ext)) return UiServersIconsCodeFileIcon;
|
if (codeExtensions.includes(ext)) return UiServersIconsCodeFileIcon
|
||||||
if (textExtensions.includes(ext)) return UiServersIconsTextFileIcon;
|
if (textExtensions.includes(ext)) return UiServersIconsTextFileIcon
|
||||||
if (imageExtensions.includes(ext)) return UiServersIconsImageFileIcon;
|
if (imageExtensions.includes(ext)) return UiServersIconsImageFileIcon
|
||||||
if (supportedArchiveExtensions.includes(ext)) return FileArchiveIcon;
|
if (supportedArchiveExtensions.includes(ext)) return FileArchiveIcon
|
||||||
return FileIcon;
|
return FileIcon
|
||||||
});
|
})
|
||||||
|
|
||||||
const subText = computed(() => {
|
const subText = computed(() => {
|
||||||
if (props.type === "directory") {
|
if (props.type === 'directory') {
|
||||||
return `${props.count} ${props.count === 1 ? "item" : "items"}`;
|
return `${props.count} ${props.count === 1 ? 'item' : 'items'}`
|
||||||
}
|
}
|
||||||
return formattedSize.value;
|
return formattedSize.value
|
||||||
});
|
})
|
||||||
|
|
||||||
const formattedModifiedDate = computed(() => {
|
const formattedModifiedDate = computed(() => {
|
||||||
const date = new Date(props.modified * 1000);
|
const date = new Date(props.modified * 1000)
|
||||||
return `${date.toLocaleDateString("en-US", {
|
return `${date.toLocaleDateString('en-US', {
|
||||||
month: "2-digit",
|
month: '2-digit',
|
||||||
day: "2-digit",
|
day: '2-digit',
|
||||||
year: "2-digit",
|
year: '2-digit',
|
||||||
})}, ${date.toLocaleTimeString("en-US", {
|
})}, ${date.toLocaleTimeString('en-US', {
|
||||||
hour: "numeric",
|
hour: 'numeric',
|
||||||
minute: "numeric",
|
minute: 'numeric',
|
||||||
hour12: true,
|
hour12: true,
|
||||||
})}`;
|
})}`
|
||||||
});
|
})
|
||||||
|
|
||||||
const formattedCreationDate = computed(() => {
|
const formattedCreationDate = computed(() => {
|
||||||
const date = new Date(props.created * 1000);
|
const date = new Date(props.created * 1000)
|
||||||
return `${date.toLocaleDateString("en-US", {
|
return `${date.toLocaleDateString('en-US', {
|
||||||
month: "2-digit",
|
month: '2-digit',
|
||||||
day: "2-digit",
|
day: '2-digit',
|
||||||
year: "2-digit",
|
year: '2-digit',
|
||||||
})}, ${date.toLocaleTimeString("en-US", {
|
})}, ${date.toLocaleTimeString('en-US', {
|
||||||
hour: "numeric",
|
hour: 'numeric',
|
||||||
minute: "numeric",
|
minute: 'numeric',
|
||||||
hour12: true,
|
hour12: true,
|
||||||
})}`;
|
})}`
|
||||||
});
|
})
|
||||||
|
|
||||||
const isEditableFile = computed(() => {
|
const isEditableFile = computed(() => {
|
||||||
if (props.type === "file") {
|
if (props.type === 'file') {
|
||||||
const ext = fileExtension.value;
|
const ext = fileExtension.value
|
||||||
return (
|
return (
|
||||||
!props.name.includes(".") ||
|
!props.name.includes('.') ||
|
||||||
textExtensions.includes(ext) ||
|
textExtensions.includes(ext) ||
|
||||||
codeExtensions.includes(ext) ||
|
codeExtensions.includes(ext) ||
|
||||||
imageExtensions.includes(ext)
|
imageExtensions.includes(ext)
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
return false;
|
return false
|
||||||
});
|
})
|
||||||
|
|
||||||
const formattedSize = computed(() => {
|
const formattedSize = computed(() => {
|
||||||
if (props.size === undefined) return "";
|
if (props.size === undefined) return ''
|
||||||
const bytes = props.size;
|
const bytes = props.size
|
||||||
if (bytes === 0) return "0 B";
|
if (bytes === 0) return '0 B'
|
||||||
|
|
||||||
const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
|
||||||
const size = (bytes / Math.pow(1024, exponent)).toFixed(2);
|
const size = (bytes / Math.pow(1024, exponent)).toFixed(2)
|
||||||
return `${size} ${units[exponent]}`;
|
return `${size} ${units[exponent]}`
|
||||||
});
|
})
|
||||||
|
|
||||||
const openContextMenu = (event: MouseEvent) => {
|
const openContextMenu = (event: MouseEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
emit("contextmenu", event.clientX, event.clientY);
|
emit('contextmenu', event.clientX, event.clientY)
|
||||||
};
|
}
|
||||||
|
|
||||||
const navigateToFolder = () => {
|
const navigateToFolder = () => {
|
||||||
const currentPath = route.value.query.path?.toString() || "";
|
const currentPath = route.value.query.path?.toString() || ''
|
||||||
const newPath = currentPath.endsWith("/")
|
const newPath = currentPath.endsWith('/')
|
||||||
? `${currentPath}${props.name}`
|
? `${currentPath}${props.name}`
|
||||||
: `${currentPath}/${props.name}`;
|
: `${currentPath}/${props.name}`
|
||||||
router.push({ query: { path: newPath, page: 1 } });
|
router.push({ query: { path: newPath, page: 1 } })
|
||||||
};
|
}
|
||||||
|
|
||||||
const isNavigating = ref(false);
|
const isNavigating = ref(false)
|
||||||
|
|
||||||
const selectItem = () => {
|
const selectItem = () => {
|
||||||
if (isNavigating.value) return;
|
if (isNavigating.value) return
|
||||||
isNavigating.value = true;
|
isNavigating.value = true
|
||||||
|
|
||||||
if (props.type === "directory") {
|
if (props.type === 'directory') {
|
||||||
navigateToFolder();
|
navigateToFolder()
|
||||||
} else if (props.type === "file" && isEditableFile.value) {
|
} else if (props.type === 'file' && isEditableFile.value) {
|
||||||
emit("edit", { name: props.name, type: props.type, path: props.path });
|
emit('edit', { name: props.name, type: props.type, path: props.path })
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isNavigating.value = false;
|
isNavigating.value = false
|
||||||
}, 500);
|
}, 500)
|
||||||
};
|
}
|
||||||
|
|
||||||
const getDragIcon = async () => {
|
const getDragIcon = async () => {
|
||||||
let iconToUse;
|
let iconToUse
|
||||||
|
|
||||||
if (props.type === "directory") {
|
if (props.type === 'directory') {
|
||||||
if (props.name === "config") {
|
if (props.name === 'config') {
|
||||||
iconToUse = UiServersIconsCogFolderIcon;
|
iconToUse = UiServersIconsCogFolderIcon
|
||||||
} else if (props.name === "world") {
|
} else if (props.name === 'world') {
|
||||||
iconToUse = UiServersIconsEarthIcon;
|
iconToUse = UiServersIconsEarthIcon
|
||||||
} else if (props.name === "resourcepacks") {
|
} else if (props.name === 'resourcepacks') {
|
||||||
iconToUse = PaletteIcon;
|
iconToUse = PaletteIcon
|
||||||
} else {
|
} else {
|
||||||
iconToUse = FolderOpenIcon;
|
iconToUse = FolderOpenIcon
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const ext = fileExtension.value;
|
const ext = fileExtension.value
|
||||||
if (codeExtensions.includes(ext)) {
|
if (codeExtensions.includes(ext)) {
|
||||||
iconToUse = UiServersIconsCodeFileIcon;
|
iconToUse = UiServersIconsCodeFileIcon
|
||||||
} else if (textExtensions.includes(ext)) {
|
} else if (textExtensions.includes(ext)) {
|
||||||
iconToUse = UiServersIconsTextFileIcon;
|
iconToUse = UiServersIconsTextFileIcon
|
||||||
} else if (imageExtensions.includes(ext)) {
|
} else if (imageExtensions.includes(ext)) {
|
||||||
iconToUse = UiServersIconsImageFileIcon;
|
iconToUse = UiServersIconsImageFileIcon
|
||||||
} else {
|
} else {
|
||||||
iconToUse = FileIcon;
|
iconToUse = FileIcon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await renderToString(h(iconToUse));
|
return await renderToString(h(iconToUse))
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDragStart = async (event: DragEvent) => {
|
const handleDragStart = async (event: DragEvent) => {
|
||||||
if (!event.dataTransfer) return;
|
if (!event.dataTransfer) return
|
||||||
isDragging.value = true;
|
isDragging.value = true
|
||||||
|
|
||||||
const dragGhost = document.createElement("div");
|
const dragGhost = document.createElement('div')
|
||||||
dragGhost.className =
|
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");
|
const iconContainer = document.createElement('div')
|
||||||
iconContainer.className = "flex size-6 items-center justify-center";
|
iconContainer.className = 'flex size-6 items-center justify-center'
|
||||||
|
|
||||||
const icon = document.createElement("div");
|
const icon = document.createElement('div')
|
||||||
icon.className = "size-4";
|
icon.className = 'size-4'
|
||||||
icon.innerHTML = await getDragIcon();
|
icon.innerHTML = await getDragIcon()
|
||||||
iconContainer.appendChild(icon);
|
iconContainer.appendChild(icon)
|
||||||
|
|
||||||
const nameSpan = document.createElement("span");
|
const nameSpan = document.createElement('span')
|
||||||
nameSpan.className = "font-bold truncate text-contrast";
|
nameSpan.className = 'font-bold truncate text-contrast'
|
||||||
nameSpan.textContent = props.name;
|
nameSpan.textContent = props.name
|
||||||
|
|
||||||
dragGhost.appendChild(iconContainer);
|
dragGhost.appendChild(iconContainer)
|
||||||
dragGhost.appendChild(nameSpan);
|
dragGhost.appendChild(nameSpan)
|
||||||
document.body.appendChild(dragGhost);
|
document.body.appendChild(dragGhost)
|
||||||
|
|
||||||
event.dataTransfer.setDragImage(dragGhost, 0, 0);
|
event.dataTransfer.setDragImage(dragGhost, 0, 0)
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
document.body.removeChild(dragGhost);
|
document.body.removeChild(dragGhost)
|
||||||
});
|
})
|
||||||
|
|
||||||
event.dataTransfer.setData(
|
event.dataTransfer.setData(
|
||||||
"application/pyro-file-move",
|
'application/pyro-file-move',
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
name: props.name,
|
name: props.name,
|
||||||
type: props.type,
|
type: props.type,
|
||||||
path: props.path,
|
path: props.path,
|
||||||
}),
|
}),
|
||||||
);
|
)
|
||||||
event.dataTransfer.effectAllowed = "move";
|
event.dataTransfer.effectAllowed = 'move'
|
||||||
};
|
}
|
||||||
|
|
||||||
const isChildPath = (parentPath: string, childPath: string) => {
|
const isChildPath = (parentPath: string, childPath: string) => {
|
||||||
return childPath.startsWith(parentPath + "/");
|
return childPath.startsWith(parentPath + '/')
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDragEnd = () => {
|
const handleDragEnd = () => {
|
||||||
isDragging.value = false;
|
isDragging.value = false
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDragEnter = () => {
|
const handleDragEnter = () => {
|
||||||
if (props.type !== "directory") return;
|
if (props.type !== 'directory') return
|
||||||
isDragOver.value = true;
|
isDragOver.value = true
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDragOver = (event: DragEvent) => {
|
const handleDragOver = (event: DragEvent) => {
|
||||||
if (props.type !== "directory" || !event.dataTransfer) return;
|
if (props.type !== 'directory' || !event.dataTransfer) return
|
||||||
event.dataTransfer.dropEffect = "move";
|
event.dataTransfer.dropEffect = 'move'
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDragLeave = () => {
|
const handleDragLeave = () => {
|
||||||
isDragOver.value = false;
|
isDragOver.value = false
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDrop = (event: DragEvent) => {
|
const handleDrop = (event: DragEvent) => {
|
||||||
isDragOver.value = false;
|
isDragOver.value = false
|
||||||
if (props.type !== "directory" || !event.dataTransfer) return;
|
if (props.type !== 'directory' || !event.dataTransfer) return
|
||||||
|
|
||||||
try {
|
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)) {
|
if (dragData.type === 'directory' && isChildPath(dragData.path, props.path)) {
|
||||||
console.error("Cannot move a folder into its own subfolder");
|
console.error('Cannot move a folder into its own subfolder')
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
emit("moveDirectTo", {
|
emit('moveDirectTo', {
|
||||||
name: dragData.name,
|
name: dragData.name,
|
||||||
type: dragData.type,
|
type: dragData.type,
|
||||||
path: dragData.path,
|
path: dragData.path,
|
||||||
destination: props.path,
|
destination: props.path,
|
||||||
});
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error handling file drop:", error);
|
console.error('Error handling file drop:', error)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -43,79 +43,79 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, onUnmounted,ref } from "vue";
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
items: any[];
|
items: any[]
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(
|
(
|
||||||
e: "delete" | "rename" | "download" | "move" | "edit" | "moveDirectTo" | "extract",
|
e: 'delete' | 'rename' | 'download' | 'move' | 'edit' | 'moveDirectTo' | 'extract',
|
||||||
item: any,
|
item: any,
|
||||||
): void;
|
): void
|
||||||
(e: "contextmenu", item: any, x: number, y: number): void;
|
(e: 'contextmenu', item: any, x: number, y: number): void
|
||||||
(e: "loadMore"): void;
|
(e: 'loadMore'): void
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const ITEM_HEIGHT = 61;
|
const ITEM_HEIGHT = 61
|
||||||
const BUFFER_SIZE = 5;
|
const BUFFER_SIZE = 5
|
||||||
|
|
||||||
const listContainer = ref<HTMLElement | null>(null);
|
const listContainer = ref<HTMLElement | null>(null)
|
||||||
const windowScrollY = ref(0);
|
const windowScrollY = ref(0)
|
||||||
const windowHeight = 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(() => {
|
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 containerTop = listContainer.value.getBoundingClientRect().top + window.scrollY
|
||||||
const relativeScrollTop = Math.max(0, windowScrollY.value - containerTop);
|
const relativeScrollTop = Math.max(0, windowScrollY.value - containerTop)
|
||||||
|
|
||||||
const start = Math.floor(relativeScrollTop / ITEM_HEIGHT);
|
const start = Math.floor(relativeScrollTop / ITEM_HEIGHT)
|
||||||
const visibleCount = Math.ceil(windowHeight.value / ITEM_HEIGHT);
|
const visibleCount = Math.ceil(windowHeight.value / ITEM_HEIGHT)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
start: Math.max(0, start - BUFFER_SIZE),
|
start: Math.max(0, start - BUFFER_SIZE),
|
||||||
end: Math.min(props.items.length, start + visibleCount + BUFFER_SIZE * 2),
|
end: Math.min(props.items.length, start + visibleCount + BUFFER_SIZE * 2),
|
||||||
};
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
const visibleTop = computed(() => {
|
const visibleTop = computed(() => {
|
||||||
return visibleRange.value.start * ITEM_HEIGHT;
|
return visibleRange.value.start * ITEM_HEIGHT
|
||||||
});
|
})
|
||||||
|
|
||||||
const visibleItems = computed(() => {
|
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 = () => {
|
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 containerBottom = listContainer.value.getBoundingClientRect().bottom
|
||||||
const remainingScroll = containerBottom - window.innerHeight;
|
const remainingScroll = containerBottom - window.innerHeight
|
||||||
|
|
||||||
if (remainingScroll < windowHeight.value * 0.2) {
|
if (remainingScroll < windowHeight.value * 0.2) {
|
||||||
emit("loadMore");
|
emit('loadMore')
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
windowHeight.value = window.innerHeight;
|
windowHeight.value = window.innerHeight
|
||||||
};
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
windowHeight.value = window.innerHeight;
|
windowHeight.value = window.innerHeight
|
||||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||||
window.addEventListener("resize", handleResize, { passive: true });
|
window.addEventListener('resize', handleResize, { passive: true })
|
||||||
handleScroll();
|
handleScroll()
|
||||||
});
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener("scroll", handleScroll);
|
window.removeEventListener('scroll', handleScroll)
|
||||||
window.removeEventListener("resize", handleResize);
|
window.removeEventListener('resize', handleResize)
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -33,64 +33,64 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { PlusIcon, XIcon } from "@modrinth/assets";
|
import { PlusIcon, XIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
import { ButtonStyled, NewModal } from '@modrinth/ui'
|
||||||
import { computed, nextTick,ref } from "vue";
|
import { computed, nextTick, ref } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
type: "file" | "directory";
|
type: 'file' | 'directory'
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "create", name: string): void;
|
(e: 'create', name: string): void
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const modal = ref<typeof NewModal>();
|
const modal = ref<typeof NewModal>()
|
||||||
const displayType = computed(() => (props.type === "directory" ? "folder" : props.type));
|
const displayType = computed(() => (props.type === 'directory' ? 'folder' : props.type))
|
||||||
const createInput = ref<HTMLInputElement | null>(null);
|
const createInput = ref<HTMLInputElement | null>(null)
|
||||||
const itemName = ref("");
|
const itemName = ref('')
|
||||||
const submitted = ref(false);
|
const submitted = ref(false)
|
||||||
|
|
||||||
const error = computed(() => {
|
const error = computed(() => {
|
||||||
if (!itemName.value) {
|
if (!itemName.value) {
|
||||||
return "Name is required.";
|
return 'Name is required.'
|
||||||
}
|
}
|
||||||
if (props.type === "file") {
|
if (props.type === 'file') {
|
||||||
const validPattern = /^[a-zA-Z0-9-_.\s]+$/;
|
const validPattern = /^[a-zA-Z0-9-_.\s]+$/
|
||||||
if (!validPattern.test(itemName.value)) {
|
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 {
|
} else {
|
||||||
const validPattern = /^[a-zA-Z0-9-_\s]+$/;
|
const validPattern = /^[a-zA-Z0-9-_\s]+$/
|
||||||
if (!validPattern.test(itemName.value)) {
|
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 = () => {
|
const handleSubmit = () => {
|
||||||
submitted.value = true;
|
submitted.value = true
|
||||||
if (!error.value) {
|
if (!error.value) {
|
||||||
emit("create", itemName.value);
|
emit('create', itemName.value)
|
||||||
hide();
|
hide()
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const show = () => {
|
const show = () => {
|
||||||
itemName.value = "";
|
itemName.value = ''
|
||||||
submitted.value = false;
|
submitted.value = false
|
||||||
modal.value?.show();
|
modal.value?.show()
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
createInput.value?.focus();
|
createInput.value?.focus()
|
||||||
}, 100);
|
}, 100)
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const hide = () => {
|
const hide = () => {
|
||||||
modal.value?.hide();
|
modal.value?.hide()
|
||||||
};
|
}
|
||||||
|
|
||||||
defineExpose({ show, hide });
|
defineExpose({ show, hide })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -42,36 +42,36 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { FileIcon, FolderOpenIcon, TrashIcon, XIcon } from "@modrinth/assets";
|
import { FileIcon, FolderOpenIcon, TrashIcon, XIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
import { ButtonStyled, NewModal } from '@modrinth/ui'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
item: {
|
item: {
|
||||||
name: string;
|
name: string
|
||||||
type: string;
|
type: string
|
||||||
count?: number;
|
count?: number
|
||||||
size?: number;
|
size?: number
|
||||||
} | null;
|
} | null
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "delete"): void;
|
(e: 'delete'): void
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const modal = ref<typeof NewModal>();
|
const modal = ref<typeof NewModal>()
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
emit("delete");
|
emit('delete')
|
||||||
hide();
|
hide()
|
||||||
};
|
}
|
||||||
|
|
||||||
const show = () => {
|
const show = () => {
|
||||||
modal.value?.show();
|
modal.value?.show()
|
||||||
};
|
}
|
||||||
|
|
||||||
const hide = () => {
|
const hide = () => {
|
||||||
modal.value?.hide();
|
modal.value?.hide()
|
||||||
};
|
}
|
||||||
|
|
||||||
defineExpose({ show, hide });
|
defineExpose({ show, hide })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
class="flex h-full w-full flex-col items-center justify-center gap-8"
|
class="flex h-full w-full flex-col items-center justify-center gap-8"
|
||||||
>
|
>
|
||||||
<UiServersIconsPanelErrorIcon />
|
<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>
|
</div>
|
||||||
<img
|
<img
|
||||||
v-show="isReady"
|
v-show="isReady"
|
||||||
@ -53,20 +53,20 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ZoomInIcon, ZoomOutIcon } from "@modrinth/assets";
|
import { ZoomInIcon, ZoomOutIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
import { ButtonStyled } from '@modrinth/ui'
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
const ZOOM_MIN = 0.1;
|
const ZOOM_MIN = 0.1
|
||||||
const ZOOM_MAX = 5;
|
const ZOOM_MAX = 5
|
||||||
const ZOOM_IN_FACTOR = 1.2;
|
const ZOOM_IN_FACTOR = 1.2
|
||||||
const ZOOM_OUT_FACTOR = 0.8;
|
const ZOOM_OUT_FACTOR = 0.8
|
||||||
const INITIAL_SCALE = 0.5;
|
const INITIAL_SCALE = 0.5
|
||||||
const MAX_IMAGE_DIMENSION = 4096;
|
const MAX_IMAGE_DIMENSION = 4096
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
imageBlob: Blob;
|
imageBlob: Blob
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const state = ref({
|
const state = ref({
|
||||||
scale: INITIAL_SCALE,
|
scale: INITIAL_SCALE,
|
||||||
@ -77,102 +77,102 @@ const state = ref({
|
|||||||
startY: 0,
|
startY: 0,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
hasError: false,
|
hasError: false,
|
||||||
errorMessage: "",
|
errorMessage: '',
|
||||||
});
|
})
|
||||||
|
|
||||||
const imageRef = ref<HTMLImageElement | null>(null);
|
const imageRef = ref<HTMLImageElement | null>(null)
|
||||||
const container = ref<HTMLElement | null>(null);
|
const container = ref<HTMLElement | null>(null)
|
||||||
const imageObjectUrl = ref("");
|
const imageObjectUrl = ref('')
|
||||||
const rafId = ref(0);
|
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(() => ({
|
const imageStyle = computed(() => ({
|
||||||
transform: `translate(-50%, -50%) scale(${state.value.scale}) translate(${state.value.translateX}px, ${state.value.translateY}px)`,
|
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 => {
|
const validateImageDimensions = (img: HTMLImageElement): boolean => {
|
||||||
if (img.naturalWidth > MAX_IMAGE_DIMENSION || img.naturalHeight > MAX_IMAGE_DIMENSION) {
|
if (img.naturalWidth > MAX_IMAGE_DIMENSION || img.naturalHeight > MAX_IMAGE_DIMENSION) {
|
||||||
state.value.hasError = true;
|
state.value.hasError = true
|
||||||
state.value.errorMessage = `Image too large to view (max ${MAX_IMAGE_DIMENSION}x${MAX_IMAGE_DIMENSION} pixels)`;
|
state.value.errorMessage = `Image too large to view (max ${MAX_IMAGE_DIMENSION}x${MAX_IMAGE_DIMENSION} pixels)`
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
return true;
|
return true
|
||||||
};
|
}
|
||||||
|
|
||||||
const updateImageUrl = (blob: Blob) => {
|
const updateImageUrl = (blob: Blob) => {
|
||||||
if (imageObjectUrl.value) URL.revokeObjectURL(imageObjectUrl.value);
|
if (imageObjectUrl.value) URL.revokeObjectURL(imageObjectUrl.value)
|
||||||
imageObjectUrl.value = URL.createObjectURL(blob);
|
imageObjectUrl.value = URL.createObjectURL(blob)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleImageLoad = () => {
|
const handleImageLoad = () => {
|
||||||
if (!imageRef.value || !validateImageDimensions(imageRef.value)) {
|
if (!imageRef.value || !validateImageDimensions(imageRef.value)) {
|
||||||
state.value.isLoading = false;
|
state.value.isLoading = false
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
state.value.isLoading = false;
|
state.value.isLoading = false
|
||||||
reset();
|
reset()
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleImageError = () => {
|
const handleImageError = () => {
|
||||||
state.value.isLoading = false;
|
state.value.isLoading = false
|
||||||
state.value.hasError = true;
|
state.value.hasError = true
|
||||||
state.value.errorMessage = "Failed to load image";
|
state.value.errorMessage = 'Failed to load image'
|
||||||
};
|
}
|
||||||
|
|
||||||
const zoom = (factor: number) => {
|
const zoom = (factor: number) => {
|
||||||
const newScale = state.value.scale * factor;
|
const newScale = state.value.scale * factor
|
||||||
state.value.scale = Math.max(ZOOM_MIN, Math.min(newScale, ZOOM_MAX));
|
state.value.scale = Math.max(ZOOM_MIN, Math.min(newScale, ZOOM_MAX))
|
||||||
};
|
}
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
state.value.scale = INITIAL_SCALE;
|
state.value.scale = INITIAL_SCALE
|
||||||
state.value.translateX = 0;
|
state.value.translateX = 0
|
||||||
state.value.translateY = 0;
|
state.value.translateY = 0
|
||||||
};
|
}
|
||||||
|
|
||||||
const startPan = (e: MouseEvent) => {
|
const startPan = (e: MouseEvent) => {
|
||||||
state.value.isPanning = true;
|
state.value.isPanning = true
|
||||||
state.value.startX = e.clientX - state.value.translateX;
|
state.value.startX = e.clientX - state.value.translateX
|
||||||
state.value.startY = e.clientY - state.value.translateY;
|
state.value.startY = e.clientY - state.value.translateY
|
||||||
};
|
}
|
||||||
|
|
||||||
const handlePan = (e: MouseEvent) => {
|
const handlePan = (e: MouseEvent) => {
|
||||||
if (!state.value.isPanning) return;
|
if (!state.value.isPanning) return
|
||||||
cancelAnimationFrame(rafId.value);
|
cancelAnimationFrame(rafId.value)
|
||||||
rafId.value = requestAnimationFrame(() => {
|
rafId.value = requestAnimationFrame(() => {
|
||||||
state.value.translateX = e.clientX - state.value.startX;
|
state.value.translateX = e.clientX - state.value.startX
|
||||||
state.value.translateY = e.clientY - state.value.startY;
|
state.value.translateY = e.clientY - state.value.startY
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const stopPan = () => {
|
const stopPan = () => {
|
||||||
state.value.isPanning = false;
|
state.value.isPanning = false
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleWheel = (e: WheelEvent) => {
|
const handleWheel = (e: WheelEvent) => {
|
||||||
const delta = e.deltaY * -0.001;
|
const delta = e.deltaY * -0.001
|
||||||
const factor = 1 + delta;
|
const factor = 1 + delta
|
||||||
zoom(factor);
|
zoom(factor)
|
||||||
};
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.imageBlob,
|
() => props.imageBlob,
|
||||||
(newBlob) => {
|
(newBlob) => {
|
||||||
if (!newBlob) return;
|
if (!newBlob) return
|
||||||
state.value.isLoading = true;
|
state.value.isLoading = true
|
||||||
state.value.hasError = false;
|
state.value.hasError = false
|
||||||
updateImageUrl(newBlob);
|
updateImageUrl(newBlob)
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.imageBlob) updateImageUrl(props.imageBlob);
|
if (props.imageBlob) updateImageUrl(props.imageBlob)
|
||||||
});
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (imageObjectUrl.value) URL.revokeObjectURL(imageObjectUrl.value);
|
if (imageObjectUrl.value) URL.revokeObjectURL(imageObjectUrl.value)
|
||||||
cancelAnimationFrame(rafId.value);
|
cancelAnimationFrame(rafId.value)
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -51,15 +51,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ChevronDownIcon from "./icons/ChevronDownIcon.vue";
|
import ChevronDownIcon from './icons/ChevronDownIcon.vue'
|
||||||
import ChevronUpIcon from "./icons/ChevronUpIcon.vue";
|
import ChevronUpIcon from './icons/ChevronUpIcon.vue'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
sortField: string;
|
sortField: string
|
||||||
sortDesc: boolean;
|
sortDesc: boolean
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
(e: "sort", field: string): void;
|
(e: 'sort', field: string): void
|
||||||
}>();
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -37,46 +37,46 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ArrowBigUpDashIcon, XIcon } from "@modrinth/assets";
|
import { ArrowBigUpDashIcon, XIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
import { ButtonStyled, NewModal } from '@modrinth/ui'
|
||||||
import { computed,nextTick, ref } from "vue";
|
import { computed, nextTick, ref } from 'vue'
|
||||||
|
|
||||||
const destinationInput = ref<HTMLInputElement | null>(null);
|
const destinationInput = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
item: { name: string } | null;
|
item: { name: string } | null
|
||||||
currentPath: string;
|
currentPath: string
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "move", destination: string): void;
|
(e: 'move', destination: string): void
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const modal = ref<typeof NewModal>();
|
const modal = ref<typeof NewModal>()
|
||||||
const destination = ref("");
|
const destination = ref('')
|
||||||
const newpath = computed(() => {
|
const newpath = computed(() => {
|
||||||
const path = destination.value.replace("//", "/");
|
const path = destination.value.replace('//', '/')
|
||||||
return path.startsWith("/") ? path : `/${path}`;
|
return path.startsWith('/') ? path : `/${path}`
|
||||||
});
|
})
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
emit("move", newpath.value);
|
emit('move', newpath.value)
|
||||||
hide();
|
hide()
|
||||||
};
|
}
|
||||||
|
|
||||||
const show = () => {
|
const show = () => {
|
||||||
destination.value = props.currentPath;
|
destination.value = props.currentPath
|
||||||
modal.value?.show();
|
modal.value?.show()
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
destinationInput.value?.focus();
|
destinationInput.value?.focus()
|
||||||
}, 100);
|
}, 100)
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const hide = () => {
|
const hide = () => {
|
||||||
modal.value?.hide();
|
modal.value?.hide()
|
||||||
};
|
}
|
||||||
|
|
||||||
defineExpose({ show, hide });
|
defineExpose({ show, hide })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -32,63 +32,63 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { EditIcon, XIcon } from "@modrinth/assets";
|
import { EditIcon, XIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
import { ButtonStyled, NewModal } from '@modrinth/ui'
|
||||||
import { computed, nextTick,ref } from "vue";
|
import { computed, nextTick, ref } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
item: { name: string; type: string } | null;
|
item: { name: string; type: string } | null
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "rename", newName: string): void;
|
(e: 'rename', newName: string): void
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const modal = ref<typeof NewModal>();
|
const modal = ref<typeof NewModal>()
|
||||||
const renameInput = ref<HTMLInputElement | null>(null);
|
const renameInput = ref<HTMLInputElement | null>(null)
|
||||||
const itemName = ref("");
|
const itemName = ref('')
|
||||||
const submitted = ref(false);
|
const submitted = ref(false)
|
||||||
|
|
||||||
const error = computed(() => {
|
const error = computed(() => {
|
||||||
if (!itemName.value) {
|
if (!itemName.value) {
|
||||||
return "Name is required.";
|
return 'Name is required.'
|
||||||
}
|
}
|
||||||
if (props.item?.type === "file") {
|
if (props.item?.type === 'file') {
|
||||||
const validPattern = /^[a-zA-Z0-9-_.\s]+$/;
|
const validPattern = /^[a-zA-Z0-9-_.\s]+$/
|
||||||
if (!validPattern.test(itemName.value)) {
|
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 {
|
} else {
|
||||||
const validPattern = /^[a-zA-Z0-9-_\s]+$/;
|
const validPattern = /^[a-zA-Z0-9-_\s]+$/
|
||||||
if (!validPattern.test(itemName.value)) {
|
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 = () => {
|
const handleSubmit = () => {
|
||||||
submitted.value = true;
|
submitted.value = true
|
||||||
if (!error.value) {
|
if (!error.value) {
|
||||||
emit("rename", itemName.value);
|
emit('rename', itemName.value)
|
||||||
hide();
|
hide()
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const show = (item: { name: string; type: string }) => {
|
const show = (item: { name: string; type: string }) => {
|
||||||
itemName.value = item.name;
|
itemName.value = item.name
|
||||||
submitted.value = false;
|
submitted.value = false
|
||||||
modal.value?.show();
|
modal.value?.show()
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
renameInput.value?.focus();
|
renameInput.value?.focus()
|
||||||
}, 100);
|
}, 100)
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const hide = () => {
|
const hide = () => {
|
||||||
modal.value?.hide();
|
modal.value?.hide()
|
||||||
};
|
}
|
||||||
|
|
||||||
defineExpose({ show, hide });
|
defineExpose({ show, hide })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -27,30 +27,30 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CheckIcon,XIcon } from "@modrinth/assets";
|
import { CheckIcon, XIcon } from '@modrinth/assets'
|
||||||
import { ConfirmModal } from "@modrinth/ui";
|
import { ConfirmModal } from '@modrinth/ui'
|
||||||
import { ref } from "vue";
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const path = ref("");
|
const path = ref('')
|
||||||
const files = ref<string[]>([]);
|
const files = ref<string[]>([])
|
||||||
|
|
||||||
const emit = defineEmits<{
|
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[]) => {
|
const show = (zipPath: string, conflictingFiles: string[]) => {
|
||||||
path.value = zipPath;
|
path.value = zipPath
|
||||||
files.value = conflictingFiles;
|
files.value = conflictingFiles
|
||||||
modal.value?.show();
|
modal.value?.show()
|
||||||
};
|
}
|
||||||
|
|
||||||
const proceed = () => {
|
const proceed = () => {
|
||||||
emit("proceed", path.value);
|
emit('proceed', path.value)
|
||||||
};
|
}
|
||||||
|
|
||||||
defineExpose({ show });
|
defineExpose({ show })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<UploadIcon class="mx-auto h-16 w-16" />
|
<UploadIcon class="mx-auto h-16 w-16" />
|
||||||
<p class="mt-2 text-xl">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -24,52 +24,52 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { UploadIcon } from "@modrinth/assets";
|
import { UploadIcon } from '@modrinth/assets'
|
||||||
import { ref } from "vue";
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: "filesDropped", files: File[]): void;
|
(event: 'filesDropped', files: File[]): void
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
overlayClass?: string;
|
overlayClass?: string
|
||||||
type?: string;
|
type?: string
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const isDragging = ref(false);
|
const isDragging = ref(false)
|
||||||
const dragCounter = ref(0);
|
const dragCounter = ref(0)
|
||||||
|
|
||||||
const handleDragEnter = (event: DragEvent) => {
|
const handleDragEnter = (event: DragEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
if (!event.dataTransfer?.types.includes("application/pyro-file-move")) {
|
if (!event.dataTransfer?.types.includes('application/pyro-file-move')) {
|
||||||
dragCounter.value++;
|
dragCounter.value++
|
||||||
isDragging.value = true;
|
isDragging.value = true
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDragOver = (event: DragEvent) => {
|
const handleDragOver = (event: DragEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDragLeave = (event: DragEvent) => {
|
const handleDragLeave = (event: DragEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
dragCounter.value--;
|
dragCounter.value--
|
||||||
if (dragCounter.value === 0) {
|
if (dragCounter.value === 0) {
|
||||||
isDragging.value = false;
|
isDragging.value = false
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDrop = (event: DragEvent) => {
|
const handleDrop = (event: DragEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
isDragging.value = false;
|
isDragging.value = false
|
||||||
dragCounter.value = 0;
|
dragCounter.value = 0
|
||||||
|
|
||||||
const isInternalMove = event.dataTransfer?.types.includes("application/pyro-file-move");
|
const isInternalMove = event.dataTransfer?.types.includes('application/pyro-file-move')
|
||||||
if (isInternalMove) return;
|
if (isInternalMove) return
|
||||||
|
|
||||||
const files = event.dataTransfer?.files;
|
const files = event.dataTransfer?.files
|
||||||
if (files) {
|
if (files) {
|
||||||
emit("filesDropped", Array.from(files));
|
emit('filesDropped', Array.from(files))
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -12,9 +12,9 @@
|
|||||||
<FolderOpenIcon class="size-4" />
|
<FolderOpenIcon class="size-4" />
|
||||||
<span>
|
<span>
|
||||||
<span class="capitalize">
|
<span class="capitalize">
|
||||||
{{ props.fileType ? props.fileType : "File" }} uploads
|
{{ props.fileType ? props.fileType : 'File' }} uploads
|
||||||
</span>
|
</span>
|
||||||
<span>{{ activeUploads.length > 0 ? ` - ${activeUploads.length} left` : "" }}</span>
|
<span>{{ activeUploads.length > 0 ? ` - ${activeUploads.length} left` : '' }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -59,7 +59,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else-if="item.status === 'error-generic'">
|
<template v-else-if="item.status === 'error-generic'">
|
||||||
<span class="text-red"
|
<span class="text-red"
|
||||||
>Failed - {{ item.error?.message || "An unexpected error occured." }}</span
|
>Failed - {{ item.error?.message || 'An unexpected error occured.' }}</span
|
||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="item.status === 'incorrect-type'">
|
<template v-else-if="item.status === 'incorrect-type'">
|
||||||
@ -101,118 +101,118 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CheckCircleIcon, FolderOpenIcon, XCircleIcon } from "@modrinth/assets";
|
import { CheckCircleIcon, FolderOpenIcon, XCircleIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
import { ButtonStyled } from '@modrinth/ui'
|
||||||
import { computed, nextTick,ref, watch } from "vue";
|
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 {
|
interface UploadItem {
|
||||||
file: File;
|
file: File
|
||||||
progress: number;
|
progress: number
|
||||||
status:
|
status:
|
||||||
| "pending"
|
| 'pending'
|
||||||
| "uploading"
|
| 'uploading'
|
||||||
| "completed"
|
| 'completed'
|
||||||
| "error-file-exists"
|
| 'error-file-exists'
|
||||||
| "error-generic"
|
| 'error-generic'
|
||||||
| "cancelled"
|
| 'cancelled'
|
||||||
| "incorrect-type";
|
| 'incorrect-type'
|
||||||
size: string;
|
size: string
|
||||||
uploader?: any;
|
uploader?: any
|
||||||
error?: Error;
|
error?: Error
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
currentPath: string;
|
currentPath: string
|
||||||
fileType?: string;
|
fileType?: string
|
||||||
marginBottom?: number;
|
marginBottom?: number
|
||||||
acceptedTypes?: Array<string>;
|
acceptedTypes?: Array<string>
|
||||||
fs: FSModule;
|
fs: FSModule
|
||||||
}
|
}
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
});
|
})
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "uploadComplete"): void;
|
(e: 'uploadComplete'): void
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const uploadStatusRef = ref<HTMLElement | null>(null);
|
const uploadStatusRef = ref<HTMLElement | null>(null)
|
||||||
const statusContentRef = ref<HTMLElement | null>(null);
|
const statusContentRef = ref<HTMLElement | null>(null)
|
||||||
const uploadQueue = ref<UploadItem[]>([]);
|
const uploadQueue = ref<UploadItem[]>([])
|
||||||
|
|
||||||
const isUploading = computed(() => uploadQueue.value.length > 0);
|
const isUploading = computed(() => uploadQueue.value.length > 0)
|
||||||
const activeUploads = computed(() =>
|
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 onUploadStatusEnter = (el: Element) => {
|
||||||
const height = (el as HTMLElement).scrollHeight + (props.marginBottom || 0);
|
const height = (el as HTMLElement).scrollHeight + (props.marginBottom || 0)
|
||||||
(el as HTMLElement).style.height = "0";
|
;(el as HTMLElement).style.height = '0'
|
||||||
|
|
||||||
void (el as HTMLElement).offsetHeight;
|
void (el as HTMLElement).offsetHeight
|
||||||
(el as HTMLElement).style.height = `${height}px`;
|
;(el as HTMLElement).style.height = `${height}px`
|
||||||
};
|
}
|
||||||
|
|
||||||
const onUploadStatusLeave = (el: Element) => {
|
const onUploadStatusLeave = (el: Element) => {
|
||||||
const height = (el as HTMLElement).scrollHeight + (props.marginBottom || 0);
|
const height = (el as HTMLElement).scrollHeight + (props.marginBottom || 0)
|
||||||
(el as HTMLElement).style.height = `${height}px`;
|
;(el as HTMLElement).style.height = `${height}px`
|
||||||
|
|
||||||
void (el as HTMLElement).offsetHeight;
|
void (el as HTMLElement).offsetHeight
|
||||||
(el as HTMLElement).style.height = "0";
|
;(el as HTMLElement).style.height = '0'
|
||||||
};
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
uploadQueue,
|
uploadQueue,
|
||||||
() => {
|
() => {
|
||||||
if (!uploadStatusRef.value) return;
|
if (!uploadStatusRef.value) return
|
||||||
const el = uploadStatusRef.value;
|
const el = uploadStatusRef.value
|
||||||
const itemsHeight = uploadQueue.value.length * 32;
|
const itemsHeight = uploadQueue.value.length * 32
|
||||||
const headerHeight = 12;
|
const headerHeight = 12
|
||||||
const gap = 8;
|
const gap = 8
|
||||||
const padding = 32;
|
const padding = 32
|
||||||
const totalHeight = padding + headerHeight + gap + itemsHeight + (props.marginBottom || 0);
|
const totalHeight = padding + headerHeight + gap + itemsHeight + (props.marginBottom || 0)
|
||||||
el.style.height = `${totalHeight}px`;
|
el.style.height = `${totalHeight}px`
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
);
|
)
|
||||||
|
|
||||||
const formatFileSize = (bytes: number): string => {
|
const formatFileSize = (bytes: number): string => {
|
||||||
if (bytes < 1024) return bytes + " B";
|
if (bytes < 1024) return bytes + ' B'
|
||||||
if (bytes < 1024 ** 2) return (bytes / 1024).toFixed(1) + " KB";
|
if (bytes < 1024 ** 2) return (bytes / 1024).toFixed(1) + ' KB'
|
||||||
if (bytes < 1024 ** 3) return (bytes / 1024 ** 2).toFixed(1) + " MB";
|
if (bytes < 1024 ** 3) return (bytes / 1024 ** 2).toFixed(1) + ' MB'
|
||||||
return (bytes / 1024 ** 3).toFixed(1) + " GB";
|
return (bytes / 1024 ** 3).toFixed(1) + ' GB'
|
||||||
};
|
}
|
||||||
|
|
||||||
const cancelUpload = (item: UploadItem) => {
|
const cancelUpload = (item: UploadItem) => {
|
||||||
if (item.uploader && item.status === "uploading") {
|
if (item.uploader && item.status === 'uploading') {
|
||||||
item.uploader.cancel();
|
item.uploader.cancel()
|
||||||
item.status = "cancelled";
|
item.status = 'cancelled'
|
||||||
|
|
||||||
setTimeout(async () => {
|
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) {
|
if (index !== -1) {
|
||||||
uploadQueue.value.splice(index, 1);
|
uploadQueue.value.splice(index, 1)
|
||||||
await nextTick();
|
await nextTick()
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const badFileTypeMsg = "Upload had incorrect file type";
|
const badFileTypeMsg = 'Upload had incorrect file type'
|
||||||
const uploadFile = async (file: File) => {
|
const uploadFile = async (file: File) => {
|
||||||
const uploadItem: UploadItem = {
|
const uploadItem: UploadItem = {
|
||||||
file,
|
file,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
status: "pending",
|
status: 'pending',
|
||||||
size: formatFileSize(file.size),
|
size: formatFileSize(file.size),
|
||||||
};
|
}
|
||||||
|
|
||||||
uploadQueue.value.push(uploadItem);
|
uploadQueue.value.push(uploadItem)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
@ -220,82 +220,82 @@ const uploadFile = async (file: File) => {
|
|||||||
!props.acceptedTypes.includes(file.type) &&
|
!props.acceptedTypes.includes(file.type) &&
|
||||||
!props.acceptedTypes.some((type) => file.name.endsWith(type))
|
!props.acceptedTypes.some((type) => file.name.endsWith(type))
|
||||||
) {
|
) {
|
||||||
throw new Error(badFileTypeMsg);
|
throw new Error(badFileTypeMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadItem.status = "uploading";
|
uploadItem.status = 'uploading'
|
||||||
const filePath = `${props.currentPath}/${file.name}`.replace("//", "/");
|
const filePath = `${props.currentPath}/${file.name}`.replace('//', '/')
|
||||||
const uploader = await props.fs.uploadFile(filePath, file);
|
const uploader = await props.fs.uploadFile(filePath, file)
|
||||||
uploadItem.uploader = uploader;
|
uploadItem.uploader = uploader
|
||||||
|
|
||||||
if (uploader?.onProgress) {
|
if (uploader?.onProgress) {
|
||||||
uploader.onProgress(({ progress }: { progress: number }) => {
|
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) {
|
if (index !== -1) {
|
||||||
uploadQueue.value[index].progress = Math.round(progress);
|
uploadQueue.value[index].progress = Math.round(progress)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
await uploader?.promise;
|
await uploader?.promise
|
||||||
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].status !== "cancelled") {
|
if (index !== -1 && uploadQueue.value[index].status !== 'cancelled') {
|
||||||
uploadQueue.value[index].status = "completed";
|
uploadQueue.value[index].status = 'completed'
|
||||||
uploadQueue.value[index].progress = 100;
|
uploadQueue.value[index].progress = 100
|
||||||
}
|
}
|
||||||
|
|
||||||
await nextTick();
|
await nextTick()
|
||||||
|
|
||||||
setTimeout(async () => {
|
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) {
|
if (removeIndex !== -1) {
|
||||||
uploadQueue.value.splice(removeIndex, 1);
|
uploadQueue.value.splice(removeIndex, 1)
|
||||||
await nextTick();
|
await nextTick()
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000)
|
||||||
|
|
||||||
emit("uploadComplete");
|
emit('uploadComplete')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error uploading file:", error);
|
console.error('Error uploading file:', error)
|
||||||
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].status !== "cancelled") {
|
if (index !== -1 && uploadQueue.value[index].status !== 'cancelled') {
|
||||||
const target = uploadQueue.value[index];
|
const target = uploadQueue.value[index]
|
||||||
|
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
if (error.message === badFileTypeMsg) {
|
if (error.message === badFileTypeMsg) {
|
||||||
target.status = "incorrect-type";
|
target.status = 'incorrect-type'
|
||||||
} else if (target.progress === 100 && error.message.includes("401")) {
|
} else if (target.progress === 100 && error.message.includes('401')) {
|
||||||
target.status = "error-file-exists";
|
target.status = 'error-file-exists'
|
||||||
} else {
|
} else {
|
||||||
target.status = "error-generic";
|
target.status = 'error-generic'
|
||||||
target.error = error;
|
target.error = error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(async () => {
|
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) {
|
if (removeIndex !== -1) {
|
||||||
uploadQueue.value.splice(removeIndex, 1);
|
uploadQueue.value.splice(removeIndex, 1)
|
||||||
await nextTick();
|
await nextTick()
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000)
|
||||||
|
|
||||||
if (error instanceof Error && error.message !== "Upload cancelled") {
|
if (error instanceof Error && error.message !== 'Upload cancelled') {
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "files",
|
group: 'files',
|
||||||
title: "Upload failed",
|
title: 'Upload failed',
|
||||||
text: `Failed to upload ${file.name}`,
|
text: `Failed to upload ${file.name}`,
|
||||||
type: "error",
|
type: 'error',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
uploadFile,
|
uploadFile,
|
||||||
cancelUpload,
|
cancelUpload,
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
<form class="flex flex-col gap-4 md:w-[600px]" @submit.prevent="handleSubmit">
|
<form class="flex flex-col gap-4 md:w-[600px]" @submit.prevent="handleSubmit">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="font-bold text-contrast">
|
<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>
|
</div>
|
||||||
<ol v-if="cf" class="mb-1 mt-0 flex flex-col gap-1 pl-8 leading-normal text-secondary">
|
<ol v-if="cf" class="mb-1 mt-0 flex flex-col gap-1 pl-8 leading-normal text-secondary">
|
||||||
<li>
|
<li>
|
||||||
@ -58,13 +58,13 @@
|
|||||||
<button v-tooltip="error" :disabled="submitted || !!error" type="submit">
|
<button v-tooltip="error" :disabled="submitted || !!error" type="submit">
|
||||||
<SpinnerIcon v-if="submitted" class="animate-spin" />
|
<SpinnerIcon v-if="submitted" class="animate-spin" />
|
||||||
<DownloadIcon v-else class="h-5 w-5" />
|
<DownloadIcon v-else class="h-5 w-5" />
|
||||||
{{ submitted ? "Installing..." : "Install" }}
|
{{ submitted ? 'Installing...' : 'Install' }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button type="button" @click="hide">
|
<button type="button" @click="hide">
|
||||||
<XIcon class="h-5 w-5" />
|
<XIcon class="h-5 w-5" />
|
||||||
{{ submitted ? "Close" : "Cancel" }}
|
{{ submitted ? 'Close' : 'Cancel' }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
@ -73,84 +73,84 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DownloadIcon, ExternalIcon, SpinnerIcon, XIcon } from "@modrinth/assets";
|
import { DownloadIcon, ExternalIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
|
||||||
import { BackupWarning, ButtonStyled, NewModal } from "@modrinth/ui";
|
import { BackupWarning, ButtonStyled, NewModal } from '@modrinth/ui'
|
||||||
import { ModrinthServersFetchError } from "@modrinth/utils";
|
import { ModrinthServersFetchError } from '@modrinth/utils'
|
||||||
import { computed, nextTick,ref } from "vue";
|
import { computed, nextTick, ref } from 'vue'
|
||||||
|
|
||||||
import type { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||||
import { handleError } 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<{
|
const props = defineProps<{
|
||||||
server: ModrinthServer;
|
server: ModrinthServer
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const modal = ref<typeof NewModal>();
|
const modal = ref<typeof NewModal>()
|
||||||
const urlInput = ref<HTMLInputElement | null>(null);
|
const urlInput = ref<HTMLInputElement | null>(null)
|
||||||
const url = ref("");
|
const url = ref('')
|
||||||
const submitted = ref(false);
|
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(() => {
|
const error = computed(() => {
|
||||||
if (trimmedUrl.value.length === 0) {
|
if (trimmedUrl.value.length === 0) {
|
||||||
return "URL is required.";
|
return 'URL is required.'
|
||||||
}
|
}
|
||||||
if (cf.value && !regex.test(trimmedUrl.value)) {
|
if (cf.value && !regex.test(trimmedUrl.value)) {
|
||||||
return "URL must be a CurseForge modpack version URL.";
|
return 'URL must be a CurseForge modpack version URL.'
|
||||||
} else if (!cf.value && !trimmedUrl.value.includes("/")) {
|
} else if (!cf.value && !trimmedUrl.value.includes('/')) {
|
||||||
return "URL must be valid.";
|
return 'URL must be valid.'
|
||||||
}
|
}
|
||||||
return "";
|
return ''
|
||||||
});
|
})
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
submitted.value = true;
|
submitted.value = true
|
||||||
if (!error.value) {
|
if (!error.value) {
|
||||||
// hide();
|
// hide();
|
||||||
try {
|
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) {
|
if (!cf.value || dry.modpack_name) {
|
||||||
await props.server.fs.extractFile(trimmedUrl.value, true, false, true);
|
await props.server.fs.extractFile(trimmedUrl.value, true, false, true)
|
||||||
hide();
|
hide()
|
||||||
} else {
|
} else {
|
||||||
submitted.value = false;
|
submitted.value = false
|
||||||
handleError(
|
handleError(
|
||||||
new ModrinthServersFetchError(
|
new ModrinthServersFetchError(
|
||||||
"Could not find CurseForge modpack at that URL.",
|
'Could not find CurseForge modpack at that URL.',
|
||||||
404,
|
404,
|
||||||
new Error(`No modpack found at ${url.value}`),
|
new Error(`No modpack found at ${url.value}`),
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
submitted.value = false;
|
submitted.value = false
|
||||||
console.error("Error installing:", error);
|
console.error('Error installing:', error)
|
||||||
handleError(error);
|
handleError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const show = (isCf: boolean) => {
|
const show = (isCf: boolean) => {
|
||||||
cf.value = isCf;
|
cf.value = isCf
|
||||||
url.value = "";
|
url.value = ''
|
||||||
submitted.value = false;
|
submitted.value = false
|
||||||
modal.value?.show();
|
modal.value?.show()
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
urlInput.value?.focus();
|
urlInput.value?.focus()
|
||||||
}, 100);
|
}, 100)
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const hide = () => {
|
const hide = () => {
|
||||||
modal.value?.hide();
|
modal.value?.hide()
|
||||||
};
|
}
|
||||||
|
|
||||||
defineExpose({ show, hide });
|
defineExpose({ show, hide })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -42,20 +42,20 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import * as THREE from "three";
|
import * as THREE from 'three'
|
||||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||||
import { onMounted, onUnmounted,ref } from "vue";
|
import { onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
const container = ref(null);
|
const container = ref(null)
|
||||||
const showLabels = ref(false);
|
const showLabels = ref(false)
|
||||||
|
|
||||||
const locations = ref([
|
const locations = ref([
|
||||||
// Active locations
|
// Active locations
|
||||||
{ name: "New York", lat: 40.7128, lng: -74.006, 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: '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: 'Miami', lat: 25.7617, lng: -80.1918, active: true, clicked: false },
|
||||||
{ name: "Spokane", lat: 47.667309, lng: -117.411922, 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: 'Dallas', lat: 32.78372, lng: -96.7947, active: true, clicked: false },
|
||||||
// Future Locations
|
// Future Locations
|
||||||
// { name: "London", lat: 51.5074, lng: -0.1278, active: false, clicked: false },
|
// { name: "London", lat: 51.5074, lng: -0.1278, active: false, clicked: false },
|
||||||
// { name: "Frankfurt", lat: 50.1109, lng: 8.6821, 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: "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: "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 },
|
// { name: "Toronto", lat: 43.6532, lng: -79.3832, active: false, clicked: false },
|
||||||
]);
|
])
|
||||||
|
|
||||||
const isLocationVisible = (location) => {
|
const isLocationVisible = (location) => {
|
||||||
if (!location.screenPosition || !globe) return false;
|
if (!location.screenPosition || !globe) return false
|
||||||
|
|
||||||
const vector = latLngToVector3(location.lat, location.lng).clone();
|
const vector = latLngToVector3(location.lat, location.lng).clone()
|
||||||
vector.applyMatrix4(globe.matrixWorld);
|
vector.applyMatrix4(globe.matrixWorld)
|
||||||
|
|
||||||
const cameraVector = new THREE.Vector3();
|
const cameraVector = new THREE.Vector3()
|
||||||
camera.getWorldPosition(cameraVector);
|
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) => {
|
const toggleLocationClicked = (location) => {
|
||||||
console.log("clicked", location.name);
|
console.log('clicked', location.name)
|
||||||
locations.value.find((loc) => loc.name === location.name).clicked = !location.clicked;
|
locations.value.find((loc) => loc.name === location.name).clicked = !location.clicked
|
||||||
};
|
}
|
||||||
|
|
||||||
let scene, camera, renderer, globe, controls;
|
let scene, camera, renderer, globe, controls
|
||||||
let animationFrame;
|
let animationFrame
|
||||||
|
|
||||||
const init = () => {
|
const init = () => {
|
||||||
scene = new THREE.Scene();
|
scene = new THREE.Scene()
|
||||||
camera = new THREE.PerspectiveCamera(
|
camera = new THREE.PerspectiveCamera(
|
||||||
45,
|
45,
|
||||||
container.value.clientWidth / container.value.clientHeight,
|
container.value.clientWidth / container.value.clientHeight,
|
||||||
0.1,
|
0.1,
|
||||||
1000,
|
1000,
|
||||||
);
|
)
|
||||||
renderer = new THREE.WebGLRenderer({
|
renderer = new THREE.WebGLRenderer({
|
||||||
antialias: true,
|
antialias: true,
|
||||||
alpha: true,
|
alpha: true,
|
||||||
powerPreference: "low-power",
|
powerPreference: 'low-power',
|
||||||
});
|
})
|
||||||
renderer.setPixelRatio(window.devicePixelRatio);
|
renderer.setPixelRatio(window.devicePixelRatio)
|
||||||
renderer.setSize(container.value.clientWidth, container.value.clientHeight);
|
renderer.setSize(container.value.clientWidth, container.value.clientHeight)
|
||||||
container.value.appendChild(renderer.domElement);
|
container.value.appendChild(renderer.domElement)
|
||||||
|
|
||||||
const geometry = new THREE.SphereGeometry(5, 64, 64);
|
const geometry = new THREE.SphereGeometry(5, 64, 64)
|
||||||
const outlineTexture = new THREE.TextureLoader().load("/earth-outline.png");
|
const outlineTexture = new THREE.TextureLoader().load('/earth-outline.png')
|
||||||
outlineTexture.minFilter = THREE.LinearFilter;
|
outlineTexture.minFilter = THREE.LinearFilter
|
||||||
outlineTexture.magFilter = THREE.LinearFilter;
|
outlineTexture.magFilter = THREE.LinearFilter
|
||||||
|
|
||||||
const material = new THREE.ShaderMaterial({
|
const material = new THREE.ShaderMaterial({
|
||||||
uniforms: {
|
uniforms: {
|
||||||
outlineTexture: { value: outlineTexture },
|
outlineTexture: { value: outlineTexture },
|
||||||
globeColor: { value: new THREE.Color("#60fbb5") },
|
globeColor: { value: new THREE.Color('#60fbb5') },
|
||||||
},
|
},
|
||||||
vertexShader: `
|
vertexShader: `
|
||||||
varying vec2 vUv;
|
varying vec2 vUv;
|
||||||
@ -141,17 +141,17 @@ const init = () => {
|
|||||||
`,
|
`,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
side: THREE.FrontSide,
|
side: THREE.FrontSide,
|
||||||
});
|
})
|
||||||
|
|
||||||
globe = new THREE.Mesh(geometry, material);
|
globe = new THREE.Mesh(geometry, material)
|
||||||
scene.add(globe);
|
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({
|
const atmosphereMaterial = new THREE.ShaderMaterial({
|
||||||
transparent: true,
|
transparent: true,
|
||||||
side: THREE.BackSide,
|
side: THREE.BackSide,
|
||||||
uniforms: {
|
uniforms: {
|
||||||
color: { value: new THREE.Color("#56f690") },
|
color: { value: new THREE.Color('#56f690') },
|
||||||
viewVector: { value: camera.position },
|
viewVector: { value: camera.position },
|
||||||
},
|
},
|
||||||
vertexShader: `
|
vertexShader: `
|
||||||
@ -171,92 +171,92 @@ const init = () => {
|
|||||||
gl_FragColor = vec4(color, intensity * 0.4);
|
gl_FragColor = vec4(color, intensity * 0.4);
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
});
|
})
|
||||||
|
|
||||||
const atmosphere = new THREE.Mesh(atmosphereGeometry, atmosphereMaterial);
|
const atmosphere = new THREE.Mesh(atmosphereGeometry, atmosphereMaterial)
|
||||||
scene.add(atmosphere);
|
scene.add(atmosphere)
|
||||||
|
|
||||||
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
|
const ambientLight = new THREE.AmbientLight(0x404040, 0.5)
|
||||||
scene.add(ambientLight);
|
scene.add(ambientLight)
|
||||||
|
|
||||||
camera.position.z = 15;
|
camera.position.z = 15
|
||||||
|
|
||||||
controls = new OrbitControls(camera, renderer.domElement);
|
controls = new OrbitControls(camera, renderer.domElement)
|
||||||
controls.enableDamping = true;
|
controls.enableDamping = true
|
||||||
controls.dampingFactor = 0.05;
|
controls.dampingFactor = 0.05
|
||||||
controls.rotateSpeed = 0.3;
|
controls.rotateSpeed = 0.3
|
||||||
controls.enableZoom = false;
|
controls.enableZoom = false
|
||||||
controls.enablePan = false;
|
controls.enablePan = false
|
||||||
controls.autoRotate = true;
|
controls.autoRotate = true
|
||||||
controls.autoRotateSpeed = 0.05;
|
controls.autoRotateSpeed = 0.05
|
||||||
controls.minPolarAngle = Math.PI * 0.3;
|
controls.minPolarAngle = Math.PI * 0.3
|
||||||
controls.maxPolarAngle = Math.PI * 0.7;
|
controls.maxPolarAngle = Math.PI * 0.7
|
||||||
|
|
||||||
globe.rotation.y = Math.PI * 1.9;
|
globe.rotation.y = Math.PI * 1.9
|
||||||
globe.rotation.x = Math.PI * 0.15;
|
globe.rotation.x = Math.PI * 0.15
|
||||||
};
|
}
|
||||||
|
|
||||||
const animate = () => {
|
const animate = () => {
|
||||||
animationFrame = requestAnimationFrame(animate);
|
animationFrame = requestAnimationFrame(animate)
|
||||||
controls.update();
|
controls.update()
|
||||||
|
|
||||||
locations.value.forEach((location) => {
|
locations.value.forEach((location) => {
|
||||||
const position = latLngToVector3(location.lat, location.lng);
|
const position = latLngToVector3(location.lat, location.lng)
|
||||||
const vector = position.clone();
|
const vector = position.clone()
|
||||||
vector.applyMatrix4(globe.matrixWorld);
|
vector.applyMatrix4(globe.matrixWorld)
|
||||||
|
|
||||||
const coords = vector.project(camera);
|
const coords = vector.project(camera)
|
||||||
const screenPosition = {
|
const screenPosition = {
|
||||||
x: (coords.x * 0.5 + 0.5) * container.value.clientWidth,
|
x: (coords.x * 0.5 + 0.5) * container.value.clientWidth,
|
||||||
y: (-coords.y * 0.5 + 0.5) * container.value.clientHeight,
|
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 latLngToVector3 = (lat, lng) => {
|
||||||
const phi = (90 - lat) * (Math.PI / 180);
|
const phi = (90 - lat) * (Math.PI / 180)
|
||||||
const theta = (lng + 180) * (Math.PI / 180);
|
const theta = (lng + 180) * (Math.PI / 180)
|
||||||
const radius = 5;
|
const radius = 5
|
||||||
|
|
||||||
return new THREE.Vector3(
|
return new THREE.Vector3(
|
||||||
-radius * Math.sin(phi) * Math.cos(theta),
|
-radius * Math.sin(phi) * Math.cos(theta),
|
||||||
radius * Math.cos(phi),
|
radius * Math.cos(phi),
|
||||||
radius * Math.sin(phi) * Math.sin(theta),
|
radius * Math.sin(phi) * Math.sin(theta),
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
if (!container.value) return;
|
if (!container.value) return
|
||||||
camera.aspect = container.value.clientWidth / container.value.clientHeight;
|
camera.aspect = container.value.clientWidth / container.value.clientHeight
|
||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix()
|
||||||
renderer.setSize(container.value.clientWidth, container.value.clientHeight);
|
renderer.setSize(container.value.clientWidth, container.value.clientHeight)
|
||||||
};
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
init();
|
init()
|
||||||
animate();
|
animate()
|
||||||
window.addEventListener("resize", handleResize);
|
window.addEventListener('resize', handleResize)
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
showLabels.value = true;
|
showLabels.value = true
|
||||||
}, 1000);
|
}, 1000)
|
||||||
});
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (animationFrame) {
|
if (animationFrame) {
|
||||||
cancelAnimationFrame(animationFrame);
|
cancelAnimationFrame(animationFrame)
|
||||||
}
|
}
|
||||||
window.removeEventListener("resize", handleResize);
|
window.removeEventListener('resize', handleResize)
|
||||||
if (renderer) {
|
if (renderer) {
|
||||||
renderer.dispose();
|
renderer.dispose()
|
||||||
}
|
}
|
||||||
if (container.value) {
|
if (container.value) {
|
||||||
container.value.innerHTML = "";
|
container.value.innerHTML = ''
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -14,31 +14,31 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, onUnmounted,ref } from "vue";
|
import { onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
const msgs = [
|
const msgs = [
|
||||||
"Organizing files...",
|
'Organizing files...',
|
||||||
"Downloading mods...",
|
'Downloading mods...',
|
||||||
"Configuring server...",
|
'Configuring server...',
|
||||||
"Setting up environment...",
|
'Setting up environment...',
|
||||||
"Adding Java...",
|
'Adding Java...',
|
||||||
];
|
]
|
||||||
|
|
||||||
const currentIndex = ref(0);
|
const currentIndex = ref(0)
|
||||||
|
|
||||||
let intervalId: NodeJS.Timeout | null = null;
|
let intervalId: NodeJS.Timeout | null = null
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
intervalId = setInterval(() => {
|
intervalId = setInterval(() => {
|
||||||
currentIndex.value = (currentIndex.value + 1) % msgs.length;
|
currentIndex.value = (currentIndex.value + 1) % msgs.length
|
||||||
}, 3000);
|
}, 3000)
|
||||||
});
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (intervalId) {
|
if (intervalId) {
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -60,36 +60,36 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
data: {
|
data: {
|
||||||
loader: string | null;
|
loader: string | null
|
||||||
loader_version: string | null;
|
loader_version: string | null
|
||||||
};
|
}
|
||||||
ignoreCurrentInstallation?: boolean;
|
ignoreCurrentInstallation?: boolean
|
||||||
isInstalling?: boolean;
|
isInstalling?: boolean
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
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 = [
|
const modLoaders = [
|
||||||
{ name: "Fabric" as const, displayName: "Fabric" },
|
{ name: 'Fabric' as const, displayName: 'Fabric' },
|
||||||
{ name: "Quilt" as const, displayName: "Quilt" },
|
{ name: 'Quilt' as const, displayName: 'Quilt' },
|
||||||
{ name: "Forge" as const, displayName: "Forge" },
|
{ name: 'Forge' as const, displayName: 'Forge' },
|
||||||
{ name: "NeoForge" as const, displayName: "NeoForge" },
|
{ name: 'NeoForge' as const, displayName: 'NeoForge' },
|
||||||
];
|
]
|
||||||
|
|
||||||
const pluginLoaders = [
|
const pluginLoaders = [
|
||||||
{ name: "Paper" as const, displayName: "Paper" },
|
{ name: 'Paper' as const, displayName: 'Paper' },
|
||||||
{ name: "Purpur" as const, displayName: "Purpur" },
|
{ name: 'Purpur' as const, displayName: 'Purpur' },
|
||||||
];
|
]
|
||||||
|
|
||||||
const isCurrentLoader = (loaderName: string) => {
|
const isCurrentLoader = (loaderName: string) => {
|
||||||
return props.data.loader?.toLowerCase() === loaderName.toLowerCase();
|
return props.data.loader?.toLowerCase() === loaderName.toLowerCase()
|
||||||
};
|
}
|
||||||
|
|
||||||
const selectLoader = (loader: string) => {
|
const selectLoader = (loader: string) => {
|
||||||
emit("selectLoader", loader);
|
emit('selectLoader', loader)
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -33,39 +33,39 @@
|
|||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button :disabled="isInstalling" @click="onSelect">
|
<button :disabled="isInstalling" @click="onSelect">
|
||||||
<DownloadIcon class="h-5 w-5" />
|
<DownloadIcon class="h-5 w-5" />
|
||||||
{{ isCurrentLoader ? "Reinstall" : "Install" }}
|
{{ isCurrentLoader ? 'Reinstall' : 'Install' }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CheckIcon, DownloadIcon } from "@modrinth/assets";
|
import { CheckIcon, DownloadIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
import { ButtonStyled } from '@modrinth/ui'
|
||||||
|
|
||||||
interface LoaderInfo {
|
interface LoaderInfo {
|
||||||
name: "Vanilla" | "Fabric" | "Forge" | "Quilt" | "Paper" | "NeoForge" | "Purpur";
|
name: 'Vanilla' | 'Fabric' | 'Forge' | 'Quilt' | 'Paper' | 'NeoForge' | 'Purpur'
|
||||||
displayName: string;
|
displayName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
loader: LoaderInfo;
|
loader: LoaderInfo
|
||||||
currentLoader: string | null;
|
currentLoader: string | null
|
||||||
loaderVersion: string | null;
|
loaderVersion: string | null
|
||||||
isInstalling?: boolean;
|
isInstalling?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "select", loader: string): void;
|
(e: 'select', loader: string): void
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const isCurrentLoader = computed(() => {
|
const isCurrentLoader = computed(() => {
|
||||||
return props.currentLoader?.toLowerCase() === props.loader.name.toLowerCase();
|
return props.currentLoader?.toLowerCase() === props.loader.name.toLowerCase()
|
||||||
});
|
})
|
||||||
|
|
||||||
const onSelect = () => {
|
const onSelect = () => {
|
||||||
emit("select", props.loader.name);
|
emit('select', props.loader.name)
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -19,54 +19,54 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Convert from "ansi-to-html";
|
import Convert from 'ansi-to-html'
|
||||||
import DOMPurify from "dompurify";
|
import DOMPurify from 'dompurify'
|
||||||
import { computed, onMounted, onUnmounted,ref } from "vue";
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
log: string;
|
log: string
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
"show-full-log": [log: string];
|
'show-full-log': [log: string]
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const logContent = ref<HTMLElement | null>(null);
|
const logContent = ref<HTMLElement | null>(null)
|
||||||
const isOverflowing = ref(false);
|
const isOverflowing = ref(false)
|
||||||
|
|
||||||
const checkOverflow = () => {
|
const checkOverflow = () => {
|
||||||
if (logContent.value && !isOverflowing.value) {
|
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({
|
const convert = new Convert({
|
||||||
fg: "#FFF",
|
fg: '#FFF',
|
||||||
bg: "#000",
|
bg: '#000',
|
||||||
newline: false,
|
newline: false,
|
||||||
escapeXML: true,
|
escapeXML: true,
|
||||||
stream: false,
|
stream: false,
|
||||||
});
|
})
|
||||||
|
|
||||||
const sanitizedLog = computed(() =>
|
const sanitizedLog = computed(() =>
|
||||||
DOMPurify.sanitize(convert.toHtml(props.log), {
|
DOMPurify.sanitize(convert.toHtml(props.log), {
|
||||||
ALLOWED_TAGS: ["span"],
|
ALLOWED_TAGS: ['span'],
|
||||||
ALLOWED_ATTR: ["style"],
|
ALLOWED_ATTR: ['style'],
|
||||||
USE_PROFILES: { html: true },
|
USE_PROFILES: { html: true },
|
||||||
}),
|
}),
|
||||||
);
|
)
|
||||||
|
|
||||||
const preventSelection = (e: MouseEvent) => {
|
const preventSelection = (e: MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
};
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
logContent.value?.addEventListener("mousedown", preventSelection);
|
logContent.value?.addEventListener('mousedown', preventSelection)
|
||||||
});
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
logContent.value?.removeEventListener("mousedown", preventSelection);
|
logContent.value?.removeEventListener('mousedown', preventSelection)
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -62,7 +62,7 @@
|
|||||||
<button :disabled="!canTakeAction" @click="initiateAction('Stop')">
|
<button :disabled="!canTakeAction" @click="initiateAction('Stop')">
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<StopCircleIcon class="h-5 w-5" />
|
<StopCircleIcon class="h-5 w-5" />
|
||||||
<span>{{ isStoppingState ? "Stopping..." : "Stop" }}</span>
|
<span>{{ isStoppingState ? 'Stopping...' : 'Stop' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
@ -115,177 +115,177 @@ import {
|
|||||||
StopCircleIcon,
|
StopCircleIcon,
|
||||||
UpdatedIcon,
|
UpdatedIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from "@modrinth/assets";
|
} from '@modrinth/assets'
|
||||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
import { ButtonStyled, NewModal } from '@modrinth/ui'
|
||||||
import type { PowerAction as ServerPowerAction, ServerState } from "@modrinth/utils";
|
import type { PowerAction as ServerPowerAction, ServerState } from '@modrinth/utils'
|
||||||
import { useStorage } from "@vueuse/core";
|
import { useStorage } from '@vueuse/core'
|
||||||
import { computed,ref } from "vue";
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const flags = useFeatureFlags();
|
const flags = useFeatureFlags()
|
||||||
|
|
||||||
interface PowerAction {
|
interface PowerAction {
|
||||||
action: ServerPowerAction;
|
action: ServerPowerAction
|
||||||
nextState: ServerState;
|
nextState: ServerState
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isOnline: boolean;
|
isOnline: boolean
|
||||||
isActioning: boolean;
|
isActioning: boolean
|
||||||
isInstalling: boolean;
|
isInstalling: boolean
|
||||||
disabled: boolean;
|
disabled: boolean
|
||||||
serverName?: string;
|
serverName?: string
|
||||||
serverData: object;
|
serverData: object
|
||||||
uptimeSeconds: number;
|
uptimeSeconds: number
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "action", action: ServerPowerAction): void;
|
(e: 'action', action: ServerPowerAction): void
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
const serverId = router.currentRoute.value.params.id;
|
const serverId = router.currentRoute.value.params.id
|
||||||
const confirmActionModal = ref<InstanceType<typeof NewModal> | null>(null);
|
const confirmActionModal = ref<InstanceType<typeof NewModal> | null>(null)
|
||||||
const detailsModal = ref<InstanceType<typeof NewModal> | null>(null);
|
const detailsModal = ref<InstanceType<typeof NewModal> | null>(null)
|
||||||
|
|
||||||
const userPreferences = useStorage(`pyro-server-${serverId}-preferences`, {
|
const userPreferences = useStorage(`pyro-server-${serverId}-preferences`, {
|
||||||
powerDontAskAgain: false,
|
powerDontAskAgain: false,
|
||||||
});
|
})
|
||||||
|
|
||||||
const serverState = ref<ServerState>(props.isOnline ? "running" : "stopped");
|
const serverState = ref<ServerState>(props.isOnline ? 'running' : 'stopped')
|
||||||
const powerAction = ref<PowerAction | null>(null);
|
const powerAction = ref<PowerAction | null>(null)
|
||||||
const dontAskAgain = ref(false);
|
const dontAskAgain = ref(false)
|
||||||
const startingDelay = ref(false);
|
const startingDelay = ref(false)
|
||||||
|
|
||||||
const canTakeAction = computed(
|
const canTakeAction = computed(
|
||||||
() => !props.isActioning && !startingDelay.value && !isTransitionState.value,
|
() => !props.isActioning && !startingDelay.value && !isTransitionState.value,
|
||||||
);
|
)
|
||||||
const isRunning = computed(() => serverState.value === "running");
|
const isRunning = computed(() => serverState.value === 'running')
|
||||||
const isTransitionState = computed(() =>
|
const isTransitionState = computed(() =>
|
||||||
["starting", "stopping", "restarting"].includes(serverState.value),
|
['starting', 'stopping', 'restarting'].includes(serverState.value),
|
||||||
);
|
)
|
||||||
const isStoppingState = computed(() => serverState.value === "stopping");
|
const isStoppingState = computed(() => serverState.value === 'stopping')
|
||||||
const showStopButton = computed(() => isRunning.value || isStoppingState.value);
|
const showStopButton = computed(() => isRunning.value || isStoppingState.value)
|
||||||
|
|
||||||
const primaryActionText = computed(() => {
|
const primaryActionText = computed(() => {
|
||||||
const states: Partial<Record<ServerState, string>> = {
|
const states: Partial<Record<ServerState, string>> = {
|
||||||
starting: "Starting...",
|
starting: 'Starting...',
|
||||||
restarting: "Restarting...",
|
restarting: 'Restarting...',
|
||||||
running: "Restart",
|
running: 'Restart',
|
||||||
stopping: "Stopping...",
|
stopping: 'Stopping...',
|
||||||
stopped: "Start",
|
stopped: 'Start',
|
||||||
};
|
}
|
||||||
return states[serverState.value];
|
return states[serverState.value]
|
||||||
});
|
})
|
||||||
|
|
||||||
const confirmActionText = computed(() => {
|
const confirmActionText = computed(() => {
|
||||||
if (!powerAction.value) return "";
|
if (!powerAction.value) return ''
|
||||||
return powerAction.value.action.charAt(0).toUpperCase() + powerAction.value.action.slice(1);
|
return powerAction.value.action.charAt(0).toUpperCase() + powerAction.value.action.slice(1)
|
||||||
});
|
})
|
||||||
|
|
||||||
const menuOptions = computed(() => [
|
const menuOptions = computed(() => [
|
||||||
...(props.isInstalling
|
...(props.isInstalling
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
id: "kill",
|
id: 'kill',
|
||||||
label: "Kill server",
|
label: 'Kill server',
|
||||||
icon: SlashIcon,
|
icon: SlashIcon,
|
||||||
action: () => initiateAction("Kill"),
|
action: () => initiateAction('Kill'),
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
{
|
{
|
||||||
id: "allServers",
|
id: 'allServers',
|
||||||
label: "All servers",
|
label: 'All servers',
|
||||||
icon: ServerIcon,
|
icon: ServerIcon,
|
||||||
action: () => router.push("/servers/manage"),
|
action: () => router.push('/servers/manage'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "details",
|
id: 'details',
|
||||||
label: "Details",
|
label: 'Details',
|
||||||
icon: InfoIcon,
|
icon: InfoIcon,
|
||||||
action: () => detailsModal.value?.show(),
|
action: () => detailsModal.value?.show(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "copy-id",
|
id: 'copy-id',
|
||||||
label: "Copy ID",
|
label: 'Copy ID',
|
||||||
icon: ClipboardCopyIcon,
|
icon: ClipboardCopyIcon,
|
||||||
action: () => copyId(),
|
action: () => copyId(),
|
||||||
shown: flags.value.developerMode,
|
shown: flags.value.developerMode,
|
||||||
},
|
},
|
||||||
]);
|
])
|
||||||
|
|
||||||
async function copyId() {
|
async function copyId() {
|
||||||
await navigator.clipboard.writeText(serverId as string);
|
await navigator.clipboard.writeText(serverId as string)
|
||||||
}
|
}
|
||||||
|
|
||||||
function initiateAction(action: ServerPowerAction) {
|
function initiateAction(action: ServerPowerAction) {
|
||||||
if (!canTakeAction.value) return;
|
if (!canTakeAction.value) return
|
||||||
|
|
||||||
const stateMap: Record<ServerPowerAction, ServerState> = {
|
const stateMap: Record<ServerPowerAction, ServerState> = {
|
||||||
Start: "starting",
|
Start: 'starting',
|
||||||
Stop: "stopping",
|
Stop: 'stopping',
|
||||||
Restart: "restarting",
|
Restart: 'restarting',
|
||||||
Kill: "stopping",
|
Kill: 'stopping',
|
||||||
};
|
|
||||||
|
|
||||||
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 (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) {
|
if (userPreferences.value.powerDontAskAgain) {
|
||||||
executePowerAction();
|
executePowerAction()
|
||||||
} else {
|
} else {
|
||||||
confirmActionModal.value?.show();
|
confirmActionModal.value?.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePrimaryAction() {
|
function handlePrimaryAction() {
|
||||||
initiateAction(isRunning.value ? "Restart" : "Start");
|
initiateAction(isRunning.value ? 'Restart' : 'Start')
|
||||||
}
|
}
|
||||||
|
|
||||||
function executePowerAction() {
|
function executePowerAction() {
|
||||||
if (!powerAction.value) return;
|
if (!powerAction.value) return
|
||||||
|
|
||||||
const { action, nextState } = powerAction.value;
|
const { action, nextState } = powerAction.value
|
||||||
emit("action", action);
|
emit('action', action)
|
||||||
serverState.value = nextState;
|
serverState.value = nextState
|
||||||
|
|
||||||
if (dontAskAgain.value) {
|
if (dontAskAgain.value) {
|
||||||
userPreferences.value.powerDontAskAgain = true;
|
userPreferences.value.powerDontAskAgain = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === "Start") {
|
if (action === 'Start') {
|
||||||
startingDelay.value = true;
|
startingDelay.value = true
|
||||||
setTimeout(() => (startingDelay.value = false), 5000);
|
setTimeout(() => (startingDelay.value = false), 5000)
|
||||||
}
|
}
|
||||||
|
|
||||||
resetPowerAction();
|
resetPowerAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetPowerAction() {
|
function resetPowerAction() {
|
||||||
confirmActionModal.value?.hide();
|
confirmActionModal.value?.hide()
|
||||||
powerAction.value = null;
|
powerAction.value = null
|
||||||
dontAskAgain.value = false;
|
dontAskAgain.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDetailsModal() {
|
function closeDetailsModal() {
|
||||||
detailsModal.value?.hide();
|
detailsModal.value?.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.isOnline,
|
() => props.isOnline,
|
||||||
(online) => (serverState.value = online ? "running" : "stopped"),
|
(online) => (serverState.value = online ? 'running' : 'stopped'),
|
||||||
);
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => router.currentRoute.value.fullPath,
|
() => router.currentRoute.value.fullPath,
|
||||||
() => closeDetailsModal(),
|
() => closeDetailsModal(),
|
||||||
);
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -39,37 +39,37 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ServerState } from "@modrinth/utils";
|
import type { ServerState } from '@modrinth/utils'
|
||||||
import { ref } from "vue";
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const STATUS_CLASSES = {
|
const STATUS_CLASSES = {
|
||||||
running: { main: "bg-brand", bg: "bg-bg-green" },
|
running: { main: 'bg-brand', bg: 'bg-bg-green' },
|
||||||
stopped: { main: "", bg: "" },
|
stopped: { main: '', bg: '' },
|
||||||
crashed: { main: "bg-brand-red", bg: "bg-bg-red" },
|
crashed: { main: 'bg-brand-red', bg: 'bg-bg-red' },
|
||||||
unknown: { main: "", bg: "" },
|
unknown: { main: '', bg: '' },
|
||||||
} as const;
|
} as const
|
||||||
|
|
||||||
const STATUS_TEXTS: Partial<Record<ServerState, string>> = {
|
const STATUS_TEXTS: Partial<Record<ServerState, string>> = {
|
||||||
running: "Running",
|
running: 'Running',
|
||||||
stopped: "",
|
stopped: '',
|
||||||
crashed: "Crashed",
|
crashed: 'Crashed',
|
||||||
unknown: "Unknown",
|
unknown: 'Unknown',
|
||||||
} as const;
|
} as const
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
state: ServerState;
|
state: ServerState
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const isExpanded = ref(false);
|
const isExpanded = ref(false)
|
||||||
|
|
||||||
function getStatusClass(state: ServerState) {
|
function getStatusClass(state: ServerState) {
|
||||||
if (state in STATUS_CLASSES) {
|
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) {
|
function getStatusText(state: ServerState) {
|
||||||
return STATUS_TEXTS[state] ?? STATUS_TEXTS.unknown;
|
return STATUS_TEXTS[state] ?? STATUS_TEXTS.unknown
|
||||||
}
|
}
|
||||||
</script>
|
</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-4 md:w-[600px]">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<p class="m-0">
|
<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.
|
your server.
|
||||||
</p>
|
</p>
|
||||||
<p v-if="props.currentVersion" class="m-0 text-sm text-secondary">
|
<p v-if="props.currentVersion" class="m-0 text-sm text-secondary">
|
||||||
@ -51,7 +51,7 @@
|
|||||||
@click="handleReinstall"
|
@click="handleReinstall"
|
||||||
>
|
>
|
||||||
<DownloadIcon class="size-4" />
|
<DownloadIcon class="size-4" />
|
||||||
{{ isLoading ? "Installing..." : hardReset ? "Erase and install" : "Install" }}
|
{{ isLoading ? 'Installing...' : hardReset ? 'Erase and install' : 'Install' }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
@ -67,38 +67,38 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DownloadIcon, XIcon } from "@modrinth/assets";
|
import { DownloadIcon, XIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
import { ButtonStyled, NewModal } from '@modrinth/ui'
|
||||||
import { ModrinthServersFetchError } from "@modrinth/utils";
|
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<{
|
const props = defineProps<{
|
||||||
server: ModrinthServer;
|
server: ModrinthServer
|
||||||
project: any;
|
project: any
|
||||||
versions: any[];
|
versions: any[]
|
||||||
currentVersion?: any;
|
currentVersion?: any
|
||||||
currentVersionId?: string;
|
currentVersionId?: string
|
||||||
serverStatus?: string;
|
serverStatus?: string
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
reinstall: [any?];
|
reinstall: [any?]
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const modal = ref();
|
const modal = ref()
|
||||||
const hardReset = ref(false);
|
const hardReset = ref(false)
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false)
|
||||||
const selectedVersion = ref("");
|
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 () => {
|
const handleReinstall = async () => {
|
||||||
if (!selectedVersion.value || !props.project?.id) return;
|
if (!selectedVersion.value || !props.project?.id) return
|
||||||
|
|
||||||
isLoading.value = true;
|
isLoading.value = true
|
||||||
try {
|
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(
|
await props.server.general.reinstall(
|
||||||
false,
|
false,
|
||||||
@ -106,56 +106,56 @@ const handleReinstall = async () => {
|
|||||||
versionId,
|
versionId,
|
||||||
undefined,
|
undefined,
|
||||||
hardReset.value,
|
hardReset.value,
|
||||||
);
|
)
|
||||||
|
|
||||||
emit("reinstall");
|
emit('reinstall')
|
||||||
hide();
|
hide()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ModrinthServersFetchError && error.statusCode === 429) {
|
if (error instanceof ModrinthServersFetchError && error.statusCode === 429) {
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "server",
|
group: 'server',
|
||||||
title: "Cannot reinstall server",
|
title: 'Cannot reinstall server',
|
||||||
text: "You are being rate limited. Please try again later.",
|
text: 'You are being rate limited. Please try again later.',
|
||||||
type: "error",
|
type: 'error',
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "server",
|
group: 'server',
|
||||||
title: "Reinstall Failed",
|
title: 'Reinstall Failed',
|
||||||
text: "An unexpected error occurred while reinstalling. Please try again later.",
|
text: 'An unexpected error occurred while reinstalling. Please try again later.',
|
||||||
type: "error",
|
type: 'error',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.serverStatus,
|
() => props.serverStatus,
|
||||||
(newStatus) => {
|
(newStatus) => {
|
||||||
if (newStatus === "installing") {
|
if (newStatus === 'installing') {
|
||||||
hide();
|
hide()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
const onShow = () => {
|
const onShow = () => {
|
||||||
hardReset.value = false;
|
hardReset.value = false
|
||||||
selectedVersion.value =
|
selectedVersion.value =
|
||||||
props.currentVersion?.version_number ?? props.versions?.[0]?.version_number ?? "";
|
props.currentVersion?.version_number ?? props.versions?.[0]?.version_number ?? ''
|
||||||
};
|
}
|
||||||
|
|
||||||
const onHide = () => {
|
const onHide = () => {
|
||||||
hardReset.value = false;
|
hardReset.value = false
|
||||||
selectedVersion.value = "";
|
selectedVersion.value = ''
|
||||||
isLoading.value = false;
|
isLoading.value = false
|
||||||
};
|
}
|
||||||
|
|
||||||
const show = () => modal.value?.show();
|
const show = () => modal.value?.show()
|
||||||
const hide = () => modal.value?.hide();
|
const hide = () => modal.value?.hide()
|
||||||
|
|
||||||
defineExpose({ show, hide });
|
defineExpose({ show, hide })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -110,12 +110,12 @@
|
|||||||
<RightArrowIcon />
|
<RightArrowIcon />
|
||||||
{{
|
{{
|
||||||
isMrpackModalSecondPhase
|
isMrpackModalSecondPhase
|
||||||
? "Erase and install"
|
? 'Erase and install'
|
||||||
: loadingServerCheck
|
: loadingServerCheck
|
||||||
? "Loading..."
|
? 'Loading...'
|
||||||
: isDangerous
|
: isDangerous
|
||||||
? "Erase and install"
|
? 'Erase and install'
|
||||||
: "Install"
|
: 'Install'
|
||||||
}}
|
}}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
@ -125,15 +125,15 @@
|
|||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
if (isMrpackModalSecondPhase) {
|
if (isMrpackModalSecondPhase) {
|
||||||
isMrpackModalSecondPhase = false;
|
isMrpackModalSecondPhase = false
|
||||||
} else {
|
} else {
|
||||||
hide();
|
hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
{{ isMrpackModalSecondPhase ? "Go back" : "Cancel" }}
|
{{ isMrpackModalSecondPhase ? 'Go back' : 'Cancel' }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
@ -150,200 +150,200 @@ import {
|
|||||||
ServerIcon,
|
ServerIcon,
|
||||||
UploadIcon,
|
UploadIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from "@modrinth/assets";
|
} from '@modrinth/assets'
|
||||||
import { BackupWarning, ButtonStyled, NewModal } from "@modrinth/ui";
|
import { BackupWarning, ButtonStyled, NewModal } from '@modrinth/ui'
|
||||||
import { formatBytes, ModrinthServersFetchError } from "@modrinth/utils";
|
import { formatBytes, ModrinthServersFetchError } from '@modrinth/utils'
|
||||||
import { onMounted, onUnmounted } from "vue";
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
import type { ModrinthServer } from "~/composables/servers/modrinth-servers";
|
import type { ModrinthServer } from '~/composables/servers/modrinth-servers'
|
||||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
import type { BackupInProgressReason } from '~/pages/servers/manage/[id].vue'
|
||||||
|
|
||||||
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
|
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
|
||||||
if (isLoading.value) {
|
if (isLoading.value) {
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
return "Upload in progress. Are you sure you want to leave?";
|
return 'Upload in progress. Are you sure you want to leave?'
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
window.addEventListener('beforeunload', handleBeforeUnload)
|
||||||
});
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
window.removeEventListener('beforeunload', handleBeforeUnload)
|
||||||
});
|
})
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: ModrinthServer;
|
server: ModrinthServer
|
||||||
backupInProgress?: BackupInProgressReason;
|
backupInProgress?: BackupInProgressReason
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
reinstall: [any?];
|
reinstall: [any?]
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const mrpackModal = ref();
|
const mrpackModal = ref()
|
||||||
const isMrpackModalSecondPhase = ref(false);
|
const isMrpackModalSecondPhase = ref(false)
|
||||||
const hardReset = ref(false);
|
const hardReset = ref(false)
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false)
|
||||||
const loadingServerCheck = ref(false);
|
const loadingServerCheck = ref(false)
|
||||||
const mrpackFile = ref<File | null>(null);
|
const mrpackFile = ref<File | null>(null)
|
||||||
const uploadProgress = ref(0);
|
const uploadProgress = ref(0)
|
||||||
const uploadedBytes = ref(0);
|
const uploadedBytes = ref(0)
|
||||||
const totalBytes = ref(0);
|
const totalBytes = ref(0)
|
||||||
|
|
||||||
const uploadPhrases = [
|
const uploadPhrases = [
|
||||||
"Removing Herobrine...",
|
'Removing Herobrine...',
|
||||||
"Feeding parrots...",
|
'Feeding parrots...',
|
||||||
"Teaching villagers new trades...",
|
'Teaching villagers new trades...',
|
||||||
"Convincing creepers to be friendly...",
|
'Convincing creepers to be friendly...',
|
||||||
"Polishing diamonds...",
|
'Polishing diamonds...',
|
||||||
"Training wolves to fetch...",
|
'Training wolves to fetch...',
|
||||||
"Building pixel art...",
|
'Building pixel art...',
|
||||||
"Explaining redstone to beginners...",
|
'Explaining redstone to beginners...',
|
||||||
"Collecting all the cats...",
|
'Collecting all the cats...',
|
||||||
"Negotiating with endermen...",
|
'Negotiating with endermen...',
|
||||||
"Planting suspicious stew ingredients...",
|
'Planting suspicious stew ingredients...',
|
||||||
"Calibrating TNT blast radius...",
|
'Calibrating TNT blast radius...',
|
||||||
"Teaching chickens to fly...",
|
'Teaching chickens to fly...',
|
||||||
"Sorting inventory alphabetically...",
|
'Sorting inventory alphabetically...',
|
||||||
"Convincing iron golems to smile...",
|
'Convincing iron golems to smile...',
|
||||||
];
|
]
|
||||||
|
|
||||||
const currentPhrase = ref("Uploading...");
|
const currentPhrase = ref('Uploading...')
|
||||||
let phraseInterval: NodeJS.Timeout | null = null;
|
let phraseInterval: NodeJS.Timeout | null = null
|
||||||
const usedPhrases = ref(new Set<number>());
|
const usedPhrases = ref(new Set<number>())
|
||||||
|
|
||||||
const getNextPhrase = () => {
|
const getNextPhrase = () => {
|
||||||
if (usedPhrases.value.size >= uploadPhrases.length) {
|
if (usedPhrases.value.size >= uploadPhrases.length) {
|
||||||
const currentPhraseIndex = uploadPhrases.indexOf(currentPhrase.value);
|
const currentPhraseIndex = uploadPhrases.indexOf(currentPhrase.value)
|
||||||
usedPhrases.value.clear();
|
usedPhrases.value.clear()
|
||||||
if (currentPhraseIndex !== -1) {
|
if (currentPhraseIndex !== -1) {
|
||||||
usedPhrases.value.add(currentPhraseIndex);
|
usedPhrases.value.add(currentPhraseIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const availableIndices = uploadPhrases
|
const availableIndices = uploadPhrases
|
||||||
.map((_, index) => index)
|
.map((_, index) => index)
|
||||||
.filter((index) => !usedPhrases.value.has(index));
|
.filter((index) => !usedPhrases.value.has(index))
|
||||||
|
|
||||||
const randomIndex = availableIndices[Math.floor(Math.random() * availableIndices.length)];
|
const randomIndex = availableIndices[Math.floor(Math.random() * availableIndices.length)]
|
||||||
usedPhrases.value.add(randomIndex);
|
usedPhrases.value.add(randomIndex)
|
||||||
|
|
||||||
return uploadPhrases[randomIndex];
|
return uploadPhrases[randomIndex]
|
||||||
};
|
}
|
||||||
|
|
||||||
const isDangerous = computed(() => hardReset.value);
|
const isDangerous = computed(() => hardReset.value)
|
||||||
const canInstall = computed(() => !mrpackFile.value || isLoading.value || loadingServerCheck.value);
|
const canInstall = computed(() => !mrpackFile.value || isLoading.value || loadingServerCheck.value)
|
||||||
|
|
||||||
const uploadMrpack = (event: Event) => {
|
const uploadMrpack = (event: Event) => {
|
||||||
const target = event.target as HTMLInputElement;
|
const target = event.target as HTMLInputElement
|
||||||
if (!target.files || target.files.length === 0) {
|
if (!target.files || target.files.length === 0) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
mrpackFile.value = target.files[0];
|
mrpackFile.value = target.files[0]
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleReinstall = async () => {
|
const handleReinstall = async () => {
|
||||||
if (hardReset.value && !isMrpackModalSecondPhase.value) {
|
if (hardReset.value && !isMrpackModalSecondPhase.value) {
|
||||||
isMrpackModalSecondPhase.value = true;
|
isMrpackModalSecondPhase.value = true
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mrpackFile.value) {
|
if (!mrpackFile.value) {
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "server",
|
group: 'server',
|
||||||
title: "No file selected",
|
title: 'No file selected',
|
||||||
text: "Choose a .mrpack file before installing.",
|
text: 'Choose a .mrpack file before installing.',
|
||||||
type: "error",
|
type: 'error',
|
||||||
});
|
})
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoading.value = true;
|
isLoading.value = true
|
||||||
uploadProgress.value = 0;
|
uploadProgress.value = 0
|
||||||
uploadProgress.value = 0;
|
uploadProgress.value = 0
|
||||||
uploadedBytes.value = 0;
|
uploadedBytes.value = 0
|
||||||
totalBytes.value = mrpackFile.value.size;
|
totalBytes.value = mrpackFile.value.size
|
||||||
|
|
||||||
currentPhrase.value = getNextPhrase();
|
currentPhrase.value = getNextPhrase()
|
||||||
phraseInterval = setInterval(() => {
|
phraseInterval = setInterval(() => {
|
||||||
currentPhrase.value = getNextPhrase();
|
currentPhrase.value = getNextPhrase()
|
||||||
}, 4500);
|
}, 4500)
|
||||||
|
|
||||||
const { onProgress, promise } = props.server.general.reinstallFromMrpack(
|
const { onProgress, promise } = props.server.general.reinstallFromMrpack(
|
||||||
mrpackFile.value,
|
mrpackFile.value,
|
||||||
hardReset.value,
|
hardReset.value,
|
||||||
);
|
)
|
||||||
|
|
||||||
onProgress(({ loaded, total, progress }) => {
|
onProgress(({ loaded, total, progress }) => {
|
||||||
uploadProgress.value = progress;
|
uploadProgress.value = progress
|
||||||
uploadedBytes.value = loaded;
|
uploadedBytes.value = loaded
|
||||||
totalBytes.value = total;
|
totalBytes.value = total
|
||||||
|
|
||||||
if (phraseInterval && progress >= 100) {
|
if (phraseInterval && progress >= 100) {
|
||||||
clearInterval(phraseInterval);
|
clearInterval(phraseInterval)
|
||||||
phraseInterval = null;
|
phraseInterval = null
|
||||||
currentPhrase.value = "Installing modpack...";
|
currentPhrase.value = 'Installing modpack...'
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await promise;
|
await promise
|
||||||
|
|
||||||
emit("reinstall", {
|
emit('reinstall', {
|
||||||
loader: "mrpack",
|
loader: 'mrpack',
|
||||||
lVersion: "",
|
lVersion: '',
|
||||||
mVersion: "",
|
mVersion: '',
|
||||||
});
|
})
|
||||||
|
|
||||||
await nextTick();
|
await nextTick()
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0)
|
||||||
hide();
|
hide()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ModrinthServersFetchError && error.statusCode === 429) {
|
if (error instanceof ModrinthServersFetchError && error.statusCode === 429) {
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "server",
|
group: 'server',
|
||||||
title: "Cannot upload and install modpack to server",
|
title: 'Cannot upload and install modpack to server',
|
||||||
text: "You are being rate limited. Please try again later.",
|
text: 'You are being rate limited. Please try again later.',
|
||||||
type: "error",
|
type: 'error',
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "server",
|
group: 'server',
|
||||||
title: "Modpack upload and install failed",
|
title: 'Modpack upload and install failed',
|
||||||
text: "An unexpected error occurred while uploading/installing. Please try again later.",
|
text: 'An unexpected error occurred while uploading/installing. Please try again later.',
|
||||||
type: "error",
|
type: 'error',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false
|
||||||
if (phraseInterval) {
|
if (phraseInterval) {
|
||||||
clearInterval(phraseInterval);
|
clearInterval(phraseInterval)
|
||||||
phraseInterval = null;
|
phraseInterval = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
const onShow = () => {
|
const onShow = () => {
|
||||||
hardReset.value = false;
|
hardReset.value = false
|
||||||
isMrpackModalSecondPhase.value = false;
|
isMrpackModalSecondPhase.value = false
|
||||||
loadingServerCheck.value = false;
|
loadingServerCheck.value = false
|
||||||
isLoading.value = false;
|
isLoading.value = false
|
||||||
mrpackFile.value = null;
|
mrpackFile.value = null
|
||||||
uploadProgress.value = 0;
|
uploadProgress.value = 0
|
||||||
uploadedBytes.value = 0;
|
uploadedBytes.value = 0
|
||||||
totalBytes.value = 0;
|
totalBytes.value = 0
|
||||||
currentPhrase.value = "Uploading...";
|
currentPhrase.value = 'Uploading...'
|
||||||
usedPhrases.value.clear();
|
usedPhrases.value.clear()
|
||||||
if (phraseInterval) {
|
if (phraseInterval) {
|
||||||
clearInterval(phraseInterval);
|
clearInterval(phraseInterval)
|
||||||
phraseInterval = null;
|
phraseInterval = null
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const show = () => mrpackModal.value?.show();
|
const show = () => mrpackModal.value?.show()
|
||||||
const hide = () => mrpackModal.value?.hide();
|
const hide = () => mrpackModal.value?.hide()
|
||||||
|
|
||||||
defineExpose({ show, hide });
|
defineExpose({ show, hide })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<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>
|
</p>
|
||||||
<div v-if="!isSecondPhase" class="flex flex-col gap-4">
|
<div v-if="!isSecondPhase" class="flex flex-col gap-4">
|
||||||
@ -165,12 +165,12 @@
|
|||||||
<RightArrowIcon />
|
<RightArrowIcon />
|
||||||
{{
|
{{
|
||||||
isLoading
|
isLoading
|
||||||
? "Installing..."
|
? 'Installing...'
|
||||||
: isSecondPhase
|
: isSecondPhase
|
||||||
? "Erase and install"
|
? 'Erase and install'
|
||||||
: hardReset
|
: hardReset
|
||||||
? "Continue"
|
? 'Continue'
|
||||||
: "Install"
|
: 'Install'
|
||||||
}}
|
}}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
@ -180,15 +180,15 @@
|
|||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
if (isSecondPhase) {
|
if (isSecondPhase) {
|
||||||
isSecondPhase = false;
|
isSecondPhase = false
|
||||||
} else {
|
} else {
|
||||||
hide();
|
hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
{{ isSecondPhase ? "Go back" : "Cancel" }}
|
{{ isSecondPhase ? 'Go back' : 'Cancel' }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
@ -197,170 +197,169 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DropdownIcon, RightArrowIcon, ServerIcon, XIcon } from "@modrinth/assets";
|
import { DropdownIcon, RightArrowIcon, ServerIcon, XIcon } from '@modrinth/assets'
|
||||||
import { BackupWarning, ButtonStyled, NewModal, Toggle } from "@modrinth/ui";
|
import { BackupWarning, ButtonStyled, NewModal, Toggle } from '@modrinth/ui'
|
||||||
import { type Loaders, ModrinthServersFetchError } from "@modrinth/utils";
|
import { type Loaders, ModrinthServersFetchError } from '@modrinth/utils'
|
||||||
import { $fetch } from "ofetch";
|
import { $fetch } from 'ofetch'
|
||||||
|
|
||||||
import type { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
import type { BackupInProgressReason } from '~/pages/servers/manage/[id].vue'
|
||||||
|
|
||||||
const { formatMessage } = useVIntl();
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
interface LoaderVersion {
|
interface LoaderVersion {
|
||||||
id: string;
|
id: string
|
||||||
stable: boolean;
|
stable: boolean
|
||||||
loaders: {
|
loaders: {
|
||||||
id: string;
|
id: string
|
||||||
url: string;
|
url: string
|
||||||
stable: boolean;
|
stable: boolean
|
||||||
}[];
|
}[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type VersionMap = Record<string, LoaderVersion[]>;
|
type VersionMap = Record<string, LoaderVersion[]>
|
||||||
type VersionCache = Record<string, any>;
|
type VersionCache = Record<string, any>
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: ModrinthServer;
|
server: ModrinthServer
|
||||||
currentLoader: Loaders | undefined;
|
currentLoader: Loaders | undefined
|
||||||
backupInProgress?: BackupInProgressReason;
|
backupInProgress?: BackupInProgressReason
|
||||||
initialSetup?: boolean;
|
initialSetup?: boolean
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
reinstall: [any?];
|
reinstall: [any?]
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const versionSelectModal = ref();
|
const versionSelectModal = ref()
|
||||||
const isSecondPhase = ref(false);
|
const isSecondPhase = ref(false)
|
||||||
const hardReset = ref(false);
|
const hardReset = ref(false)
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false)
|
||||||
const loadingServerCheck = ref(false);
|
const loadingServerCheck = ref(false)
|
||||||
const serverCheckError = ref("");
|
const serverCheckError = ref('')
|
||||||
const showSnapshots = ref(false);
|
const showSnapshots = ref(false)
|
||||||
|
|
||||||
const selectedLoader = ref<Loaders>("Vanilla");
|
const selectedLoader = ref<Loaders>('Vanilla')
|
||||||
const selectedMCVersion = ref("");
|
const selectedMCVersion = ref('')
|
||||||
const selectedLoaderVersion = ref("");
|
const selectedLoaderVersion = ref('')
|
||||||
|
|
||||||
const paperVersions = ref<Record<string, number[]>>({});
|
const paperVersions = ref<Record<string, number[]>>({})
|
||||||
const purpurVersions = ref<Record<string, string[]>>({});
|
const purpurVersions = ref<Record<string, string[]>>({})
|
||||||
const loaderVersions = ref<VersionMap>({});
|
const loaderVersions = ref<VersionMap>({})
|
||||||
const cachedVersions = ref<VersionCache>({});
|
const cachedVersions = ref<VersionCache>({})
|
||||||
|
|
||||||
const versionStrings = ["forge", "fabric", "quilt", "neo"] as const;
|
const versionStrings = ['forge', 'fabric', 'quilt', 'neo'] as const
|
||||||
|
|
||||||
const isSnapshotSelected = computed(() => {
|
const isSnapshotSelected = computed(() => {
|
||||||
if (selectedMCVersion.value) {
|
if (selectedMCVersion.value) {
|
||||||
const selected = tags.value.gameVersions.find((x) => x.version === selectedMCVersion.value);
|
const selected = tags.value.gameVersions.find((x) => x.version === selectedMCVersion.value)
|
||||||
if (selected?.version_type !== "release") {
|
if (selected?.version_type !== 'release') {
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false
|
||||||
});
|
})
|
||||||
|
|
||||||
const getLoaderVersions = async (loader: string) => {
|
const getLoaderVersions = async (loader: string) => {
|
||||||
return await $fetch(
|
return await $fetch(
|
||||||
`https://launcher-meta.modrinth.com/${loader?.toLowerCase()}/v0/manifest.json`,
|
`https://launcher-meta.modrinth.com/${loader?.toLowerCase()}/v0/manifest.json`,
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const fetchLoaderVersions = async () => {
|
const fetchLoaderVersions = async () => {
|
||||||
const versions = await Promise.all(
|
const versions = await Promise.all(
|
||||||
versionStrings.map(async (loader) => {
|
versionStrings.map(async (loader) => {
|
||||||
const runFetch = async (iterations: number) => {
|
const runFetch = async (iterations: number) => {
|
||||||
if (iterations > 5) {
|
if (iterations > 5) {
|
||||||
throw new Error("Failed to fetch loader versions");
|
throw new Error('Failed to fetch loader versions')
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await getLoaderVersions(loader);
|
const res = await getLoaderVersions(loader)
|
||||||
return { [loader]: (res as any).gameVersions };
|
return { [loader]: (res as any).gameVersions }
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return await runFetch(iterations + 1);
|
return await runFetch(iterations + 1)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
try {
|
try {
|
||||||
return await runFetch(0);
|
return await runFetch(0)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e)
|
||||||
return { [loader]: [] };
|
return { [loader]: [] }
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
)
|
||||||
|
|
||||||
loaderVersions.value = versions.reduce((acc, val) => ({ ...acc, ...val }), {});
|
loaderVersions.value = versions.reduce((acc, val) => ({ ...acc, ...val }), {})
|
||||||
};
|
}
|
||||||
|
|
||||||
const fetchPaperVersions = async (mcVersion: string) => {
|
const fetchPaperVersions = async (mcVersion: string) => {
|
||||||
try {
|
try {
|
||||||
const res = await $fetch(`https://api.papermc.io/v2/projects/paper/versions/${mcVersion}`);
|
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);
|
paperVersions.value[mcVersion] = (res as any).builds.sort((a: number, b: number) => b - a)
|
||||||
return res;
|
return res
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e)
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const fetchPurpurVersions = async (mcVersion: string) => {
|
const fetchPurpurVersions = async (mcVersion: string) => {
|
||||||
try {
|
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(
|
purpurVersions.value[mcVersion] = (res as any).builds.all.sort(
|
||||||
(a: string, b: string) => parseInt(b) - parseInt(a),
|
(a: string, b: string) => parseInt(b) - parseInt(a),
|
||||||
);
|
)
|
||||||
return res;
|
return res
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e)
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const selectedLoaderVersions = computed<string[]>(() => {
|
const selectedLoaderVersions = computed<string[]>(() => {
|
||||||
const loader = selectedLoader.value.toLowerCase();
|
const loader = selectedLoader.value.toLowerCase()
|
||||||
|
|
||||||
if (loader === "paper") {
|
if (loader === 'paper') {
|
||||||
return paperVersions.value[selectedMCVersion.value]?.map((x) => `${x}`) || [];
|
return paperVersions.value[selectedMCVersion.value]?.map((x) => `${x}`) || []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loader === "purpur") {
|
if (loader === 'purpur') {
|
||||||
return purpurVersions.value[selectedMCVersion.value] || [];
|
return purpurVersions.value[selectedMCVersion.value] || []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loader === "vanilla") {
|
if (loader === 'vanilla') {
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
let apiLoader = loader;
|
let apiLoader = loader
|
||||||
if (loader === "neoforge") {
|
if (loader === 'neoforge') {
|
||||||
apiLoader = "neo";
|
apiLoader = 'neo'
|
||||||
}
|
}
|
||||||
|
|
||||||
const backwardsCompatibleVersion = loaderVersions.value[apiLoader]?.find(
|
const backwardsCompatibleVersion = loaderVersions.value[apiLoader]?.find(
|
||||||
|
(x) => x.id === '${modrinth.gameVersion}',
|
||||||
(x) => x.id === "${modrinth.gameVersion}",
|
)
|
||||||
);
|
|
||||||
|
|
||||||
if (backwardsCompatibleVersion) {
|
if (backwardsCompatibleVersion) {
|
||||||
return backwardsCompatibleVersion.loaders.map((x) => x.id);
|
return backwardsCompatibleVersion.loaders.map((x) => x.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
loaderVersions.value[apiLoader]
|
loaderVersions.value[apiLoader]
|
||||||
?.find((x) => x.id === selectedMCVersion.value)
|
?.find((x) => x.id === selectedMCVersion.value)
|
||||||
?.loaders.map((x) => x.id) || []
|
?.loaders.map((x) => x.id) || []
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
|
|
||||||
watch(selectedLoader, async () => {
|
watch(selectedLoader, async () => {
|
||||||
if (selectedMCVersion.value) {
|
if (selectedMCVersion.value) {
|
||||||
selectedLoaderVersion.value = "";
|
selectedLoaderVersion.value = ''
|
||||||
serverCheckError.value = "";
|
serverCheckError.value = ''
|
||||||
|
|
||||||
await checkVersionAvailability(selectedMCVersion.value);
|
await checkVersionAvailability(selectedMCVersion.value)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
selectedLoaderVersions,
|
selectedLoaderVersions,
|
||||||
@ -369,161 +368,161 @@ watch(
|
|||||||
newVersions.length > 0 &&
|
newVersions.length > 0 &&
|
||||||
(!selectedLoaderVersion.value || !newVersions.includes(selectedLoaderVersion.value))
|
(!selectedLoaderVersion.value || !newVersions.includes(selectedLoaderVersion.value))
|
||||||
) {
|
) {
|
||||||
selectedLoaderVersion.value = String(newVersions[0]);
|
selectedLoaderVersion.value = String(newVersions[0])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
)
|
||||||
|
|
||||||
const getLoaderVersion = async (loader: string, version: string) => {
|
const getLoaderVersion = async (loader: string, version: string) => {
|
||||||
return await $fetch(
|
return await $fetch(
|
||||||
`https://launcher-meta.modrinth.com/${loader?.toLowerCase()}/v0/versions/${version}.json`,
|
`https://launcher-meta.modrinth.com/${loader?.toLowerCase()}/v0/versions/${version}.json`,
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const checkVersionAvailability = async (version: string) => {
|
const checkVersionAvailability = async (version: string) => {
|
||||||
if (!version || version.trim().length < 3) return;
|
if (!version || version.trim().length < 3) return
|
||||||
|
|
||||||
isLoading.value = true;
|
isLoading.value = true
|
||||||
loadingServerCheck.value = true;
|
loadingServerCheck.value = true
|
||||||
|
|
||||||
try {
|
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) {
|
if (!mcRes.downloads?.server) {
|
||||||
serverCheckError.value = "We couldn't find a server.jar for this version.";
|
serverCheckError.value = "We couldn't find a server.jar for this version."
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const loader = selectedLoader.value.toLowerCase();
|
const loader = selectedLoader.value.toLowerCase()
|
||||||
if (loader === "paper" || loader === "purpur") {
|
if (loader === 'paper' || loader === 'purpur') {
|
||||||
const fetchFn = loader === "paper" ? fetchPaperVersions : fetchPurpurVersions;
|
const fetchFn = loader === 'paper' ? fetchPaperVersions : fetchPurpurVersions
|
||||||
const result = await fetchFn(version);
|
const result = await fetchFn(version)
|
||||||
if (!result) {
|
if (!result) {
|
||||||
serverCheckError.value = `This Minecraft version is not supported by ${loader}.`;
|
serverCheckError.value = `This Minecraft version is not supported by ${loader}.`
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
serverCheckError.value = "";
|
serverCheckError.value = ''
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error)
|
||||||
serverCheckError.value = "Failed to fetch versions.";
|
serverCheckError.value = 'Failed to fetch versions.'
|
||||||
} finally {
|
} finally {
|
||||||
loadingServerCheck.value = false;
|
loadingServerCheck.value = false
|
||||||
isLoading.value = false;
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
watch(selectedMCVersion, checkVersionAvailability);
|
watch(selectedMCVersion, checkVersionAvailability)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchLoaderVersions();
|
fetchLoaderVersions()
|
||||||
});
|
})
|
||||||
|
|
||||||
const tags = useTags();
|
const tags = useTags()
|
||||||
const mcVersions = computed(() =>
|
const mcVersions = computed(() =>
|
||||||
tags.value.gameVersions
|
tags.value.gameVersions
|
||||||
.filter((x) =>
|
.filter((x) =>
|
||||||
showSnapshots.value
|
showSnapshots.value
|
||||||
? x.version_type === "snapshot" || x.version_type === "release"
|
? x.version_type === 'snapshot' || x.version_type === 'release'
|
||||||
: x.version_type === "release",
|
: x.version_type === 'release',
|
||||||
)
|
)
|
||||||
.map((x) => x.version),
|
.map((x) => x.version),
|
||||||
);
|
)
|
||||||
|
|
||||||
const isDangerous = computed(() => hardReset.value);
|
const isDangerous = computed(() => hardReset.value)
|
||||||
const canInstall = computed(() => {
|
const canInstall = computed(() => {
|
||||||
const conds =
|
const conds =
|
||||||
!selectedMCVersion.value ||
|
!selectedMCVersion.value ||
|
||||||
isLoading.value ||
|
isLoading.value ||
|
||||||
loadingServerCheck.value ||
|
loadingServerCheck.value ||
|
||||||
serverCheckError.value.trim().length > 0;
|
serverCheckError.value.trim().length > 0
|
||||||
|
|
||||||
if (selectedLoader.value.toLowerCase() === "vanilla") {
|
if (selectedLoader.value.toLowerCase() === 'vanilla') {
|
||||||
return conds;
|
return conds
|
||||||
}
|
}
|
||||||
|
|
||||||
return conds || !selectedLoaderVersion.value;
|
return conds || !selectedLoaderVersion.value
|
||||||
});
|
})
|
||||||
|
|
||||||
const handleReinstall = async () => {
|
const handleReinstall = async () => {
|
||||||
if (hardReset.value && !isSecondPhase.value) {
|
if (hardReset.value && !isSecondPhase.value) {
|
||||||
isSecondPhase.value = true;
|
isSecondPhase.value = true
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoading.value = true;
|
isLoading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await props.server.general?.reinstall(
|
await props.server.general?.reinstall(
|
||||||
true,
|
true,
|
||||||
selectedLoader.value,
|
selectedLoader.value,
|
||||||
selectedMCVersion.value,
|
selectedMCVersion.value,
|
||||||
selectedLoader.value === "Vanilla" ? "" : selectedLoaderVersion.value,
|
selectedLoader.value === 'Vanilla' ? '' : selectedLoaderVersion.value,
|
||||||
props.initialSetup ? true : hardReset.value,
|
props.initialSetup ? true : hardReset.value,
|
||||||
);
|
)
|
||||||
|
|
||||||
emit("reinstall", {
|
emit('reinstall', {
|
||||||
loader: selectedLoader.value,
|
loader: selectedLoader.value,
|
||||||
lVersion: selectedLoaderVersion.value,
|
lVersion: selectedLoaderVersion.value,
|
||||||
mVersion: selectedMCVersion.value,
|
mVersion: selectedMCVersion.value,
|
||||||
});
|
})
|
||||||
|
|
||||||
hide();
|
hide()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ModrinthServersFetchError && (error as any)?.statusCode === 429) {
|
if (error instanceof ModrinthServersFetchError && (error as any)?.statusCode === 429) {
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "server",
|
group: 'server',
|
||||||
title: "Cannot reinstall server",
|
title: 'Cannot reinstall server',
|
||||||
text: "You are being rate limited. Please try again later.",
|
text: 'You are being rate limited. Please try again later.',
|
||||||
type: "error",
|
type: 'error',
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "server",
|
group: 'server',
|
||||||
title: "Reinstall Failed",
|
title: 'Reinstall Failed',
|
||||||
text: "An unexpected error occurred while reinstalling. Please try again later.",
|
text: 'An unexpected error occurred while reinstalling. Please try again later.',
|
||||||
type: "error",
|
type: 'error',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const onShow = () => {
|
const onShow = () => {
|
||||||
selectedMCVersion.value = props.server.general?.mc_version || "";
|
selectedMCVersion.value = props.server.general?.mc_version || ''
|
||||||
if (isSnapshotSelected.value) {
|
if (isSnapshotSelected.value) {
|
||||||
showSnapshots.value = true;
|
showSnapshots.value = true
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const onHide = () => {
|
const onHide = () => {
|
||||||
hardReset.value = false;
|
hardReset.value = false
|
||||||
isSecondPhase.value = false;
|
isSecondPhase.value = false
|
||||||
serverCheckError.value = "";
|
serverCheckError.value = ''
|
||||||
loadingServerCheck.value = false;
|
loadingServerCheck.value = false
|
||||||
isLoading.value = false;
|
isLoading.value = false
|
||||||
selectedMCVersion.value = "";
|
selectedMCVersion.value = ''
|
||||||
serverCheckError.value = "";
|
serverCheckError.value = ''
|
||||||
paperVersions.value = {};
|
paperVersions.value = {}
|
||||||
purpurVersions.value = {};
|
purpurVersions.value = {}
|
||||||
};
|
}
|
||||||
|
|
||||||
const show = (loader: Loaders) => {
|
const show = (loader: Loaders) => {
|
||||||
if (selectedLoader.value !== loader) {
|
if (selectedLoader.value !== loader) {
|
||||||
selectedLoaderVersion.value = "";
|
selectedLoaderVersion.value = ''
|
||||||
}
|
}
|
||||||
selectedLoader.value = loader;
|
selectedLoader.value = loader
|
||||||
selectedMCVersion.value = props.server.general?.mc_version || "";
|
selectedMCVersion.value = props.server.general?.mc_version || ''
|
||||||
versionSelectModal.value?.show();
|
versionSelectModal.value?.show()
|
||||||
};
|
}
|
||||||
const hide = () => versionSelectModal.value?.hide();
|
const hide = () => versionSelectModal.value?.hide()
|
||||||
|
|
||||||
defineExpose({ show, hide });
|
defineExpose({ show, hide })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -14,12 +14,12 @@
|
|||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled type="standard" :color="props.restart ? 'standard' : 'brand'">
|
<ButtonStyled type="standard" :color="props.restart ? 'standard' : 'brand'">
|
||||||
<button :disabled="props.isUpdating" @click="props.save">
|
<button :disabled="props.isUpdating" @click="props.save">
|
||||||
{{ props.isUpdating ? "Saving..." : "Save" }}
|
{{ props.isUpdating ? 'Saving...' : 'Save' }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled v-if="props.restart" type="standard" color="brand">
|
<ButtonStyled v-if="props.restart" type="standard" color="brand">
|
||||||
<button :disabled="props.isUpdating" @click="saveAndRestart">
|
<button :disabled="props.isUpdating" @click="saveAndRestart">
|
||||||
{{ props.isUpdating ? "Saving..." : "Save & restart" }}
|
{{ props.isUpdating ? 'Saving...' : 'Save & restart' }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
@ -30,23 +30,23 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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<{
|
const props = defineProps<{
|
||||||
isUpdating: boolean;
|
isUpdating: boolean
|
||||||
restart?: boolean;
|
restart?: boolean
|
||||||
save: () => void;
|
save: () => void
|
||||||
reset: () => void;
|
reset: () => void
|
||||||
isVisible: boolean;
|
isVisible: boolean
|
||||||
server: ModrinthServer;
|
server: ModrinthServer
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const saveAndRestart = async () => {
|
const saveAndRestart = async () => {
|
||||||
props.save();
|
props.save()
|
||||||
await props.server.general?.power("Restart");
|
await props.server.general?.power('Restart')
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -26,14 +26,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { GameIcon } from "@modrinth/assets";
|
import { GameIcon } from '@modrinth/assets'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
game: string;
|
game: string
|
||||||
mcVersion: string;
|
mcVersion: string
|
||||||
isLink?: boolean;
|
isLink?: boolean
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const route = useNativeRoute();
|
const route = useNativeRoute()
|
||||||
const serverId = route.params.id as string;
|
const serverId = route.params.id as string
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -21,6 +21,6 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps<{
|
defineProps<{
|
||||||
image: string | undefined;
|
image: string | undefined
|
||||||
}>();
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -28,13 +28,13 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
interface ServerInfoLabelsProps {
|
interface ServerInfoLabelsProps {
|
||||||
serverData: Record<string, any>;
|
serverData: Record<string, any>
|
||||||
showGameLabel: boolean;
|
showGameLabel: boolean
|
||||||
showLoaderLabel: boolean;
|
showLoaderLabel: boolean
|
||||||
uptimeSeconds?: number;
|
uptimeSeconds?: number
|
||||||
column?: boolean;
|
column?: boolean
|
||||||
linked?: boolean;
|
linked?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
defineProps<ServerInfoLabelsProps>();
|
defineProps<ServerInfoLabelsProps>()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -39,7 +39,7 @@
|
|||||||
style="min-height: 20px; min-width: 20px; height: 20px; width: 20px"
|
style="min-height: 20px; min-width: 20px; height: 20px; width: 20px"
|
||||||
alt="Server Icon"
|
alt="Server Icon"
|
||||||
/>
|
/>
|
||||||
Using {{ projectData?.title || "Unknown" }}
|
Using {{ projectData?.title || 'Unknown' }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="min-h-[20px]"></div>
|
<div v-else class="min-h-[20px]"></div>
|
||||||
|
|
||||||
@ -101,37 +101,37 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ChevronRightIcon, LockIcon, SparklesIcon } from "@modrinth/assets";
|
import { ChevronRightIcon, LockIcon, SparklesIcon } from '@modrinth/assets'
|
||||||
import { Avatar, CopyCode } from "@modrinth/ui";
|
import { Avatar, CopyCode } from '@modrinth/ui'
|
||||||
import type { Project, Server } from "@modrinth/utils";
|
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
|
// Necessary only to get server icon
|
||||||
await useModrinthServers(props.server_id, ["general"]);
|
await useModrinthServers(props.server_id, ['general'])
|
||||||
}
|
}
|
||||||
|
|
||||||
const showGameLabel = computed(() => !!props.game);
|
const showGameLabel = computed(() => !!props.game)
|
||||||
const showLoaderLabel = computed(() => !!props.loader);
|
const showLoaderLabel = computed(() => !!props.loader)
|
||||||
|
|
||||||
let projectData: Ref<Project | null>;
|
let projectData: Ref<Project | null>
|
||||||
if (props.upstream) {
|
if (props.upstream) {
|
||||||
const { data } = await useAsyncData<Project>(
|
const { data } = await useAsyncData<Project>(
|
||||||
`server-project-${props.server_id}`,
|
`server-project-${props.server_id}`,
|
||||||
async (): Promise<Project> => {
|
async (): Promise<Project> => {
|
||||||
const result = await useBaseFetch(`project/${props.upstream?.project_id}`);
|
const result = await useBaseFetch(`project/${props.upstream?.project_id}`)
|
||||||
return result as Project;
|
return result as Project
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
projectData = data;
|
projectData = data
|
||||||
} else {
|
} else {
|
||||||
projectData = ref(null);
|
projectData = ref(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const image = useState<string | undefined>(`server-icon-${props.server_id}`, () => undefined);
|
const image = useState<string | undefined>(`server-icon-${props.server_id}`, () => undefined)
|
||||||
const iconUrl = computed(() => projectData.value?.icon_url || undefined);
|
const iconUrl = computed(() => projectData.value?.icon_url || undefined)
|
||||||
const isConfiguring = computed(() => props.flows?.intro);
|
const isConfiguring = computed(() => props.flows?.intro)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -35,12 +35,12 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps<{
|
defineProps<{
|
||||||
noSeparator?: boolean;
|
noSeparator?: boolean
|
||||||
loader?: "Fabric" | "Quilt" | "Forge" | "NeoForge" | "Paper" | "Spigot" | "Bukkit" | "Vanilla";
|
loader?: 'Fabric' | 'Quilt' | 'Forge' | 'NeoForge' | 'Paper' | 'Spigot' | 'Bukkit' | 'Vanilla'
|
||||||
loaderVersion?: string;
|
loaderVersion?: string
|
||||||
isLink?: boolean;
|
isLink?: boolean
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const route = useNativeRoute();
|
const route = useNativeRoute()
|
||||||
const serverId = route.params.id as string;
|
const serverId = route.params.id as string
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -15,5 +15,5 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
import { ButtonStyled } from '@modrinth/ui'
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -35,22 +35,22 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RightArrowIcon } from "@modrinth/assets";
|
import { RightArrowIcon } from '@modrinth/assets'
|
||||||
import type { RouteLocationNormalized } from "vue-router";
|
import type { RouteLocationNormalized } from 'vue-router'
|
||||||
|
|
||||||
import type { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
import type { BackupInProgressReason } from '~/pages/servers/manage/[id].vue'
|
||||||
|
|
||||||
const emit = defineEmits(["reinstall"]);
|
const emit = defineEmits(['reinstall'])
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
navLinks: { label: string; href: string; icon: Component; external?: boolean; shown?: boolean }[];
|
navLinks: { label: string; href: string; icon: Component; external?: boolean; shown?: boolean }[]
|
||||||
route: RouteLocationNormalized;
|
route: RouteLocationNormalized
|
||||||
server: ModrinthServer;
|
server: ModrinthServer
|
||||||
backupInProgress?: BackupInProgressReason;
|
backupInProgress?: BackupInProgressReason
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const onReinstall = (...args: any[]) => {
|
const onReinstall = (...args: any[]) => {
|
||||||
emit("reinstall", ...args);
|
emit('reinstall', ...args)
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -57,7 +57,7 @@
|
|||||||
>
|
>
|
||||||
<div class="flex flex-row items-center gap-2">
|
<div class="flex flex-row items-center gap-2">
|
||||||
<h2 class="m-0 -ml-0.5 mt-1 text-3xl font-extrabold text-contrast">
|
<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>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-base font-normal text-secondary">Storage usage</h3>
|
<h3 class="text-base font-normal text-secondary">Storage usage</h3>
|
||||||
@ -67,25 +67,25 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CpuIcon, DatabaseIcon, FolderOpenIcon, IssuesIcon } from "@modrinth/assets";
|
import { CpuIcon, DatabaseIcon, FolderOpenIcon, IssuesIcon } from '@modrinth/assets'
|
||||||
import type { Stats } from "@modrinth/utils";
|
import type { Stats } from '@modrinth/utils'
|
||||||
import { useStorage } from "@vueuse/core";
|
import { useStorage } from '@vueuse/core'
|
||||||
import { computed, ref, shallowRef } from "vue";
|
import { computed, ref, shallowRef } from 'vue'
|
||||||
|
|
||||||
const flags = useFeatureFlags();
|
const flags = useFeatureFlags()
|
||||||
const route = useNativeRoute();
|
const route = useNativeRoute()
|
||||||
const serverId = route.params.id;
|
const serverId = route.params.id
|
||||||
const VueApexCharts = defineAsyncComponent(() => import("vue3-apexcharts"));
|
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`, {
|
const userPreferences = useStorage(`pyro-server-${serverId}-preferences`, {
|
||||||
ramAsNumber: false,
|
ramAsNumber: false,
|
||||||
});
|
})
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{ data?: Stats; loading?: boolean }>(), {
|
const props = withDefaults(defineProps<{ data?: Stats; loading?: boolean }>(), {
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
})
|
||||||
|
|
||||||
const stats = shallowRef(
|
const stats = shallowRef(
|
||||||
props.data?.current || {
|
props.data?.current || {
|
||||||
@ -94,76 +94,76 @@ const stats = shallowRef(
|
|||||||
ram_total_bytes: 1, // Avoid division by zero
|
ram_total_bytes: 1, // Avoid division by zero
|
||||||
storage_usage_bytes: 0,
|
storage_usage_bytes: 0,
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
const onChartReady = (index: number) => {
|
const onChartReady = (index: number) => {
|
||||||
chartsReady.value.add(index);
|
chartsReady.value.add(index)
|
||||||
};
|
}
|
||||||
|
|
||||||
const formatBytes = (bytes: number) => {
|
const formatBytes = (bytes: number) => {
|
||||||
const units = ["B", "KB", "MB", "GB"];
|
const units = ['B', 'KB', 'MB', 'GB']
|
||||||
let value = bytes;
|
let value = bytes
|
||||||
let unit = 0;
|
let unit = 0
|
||||||
while (value >= 1024 && unit < units.length - 1) {
|
while (value >= 1024 && unit < units.length - 1) {
|
||||||
value /= 1024;
|
value /= 1024
|
||||||
unit++;
|
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 cpuData = ref<number[]>(Array(20).fill(0))
|
||||||
const ramData = ref<number[]>(Array(20).fill(0));
|
const ramData = ref<number[]>(Array(20).fill(0))
|
||||||
|
|
||||||
const updateGraphData = (arr: number[], newValue: number) => {
|
const updateGraphData = (arr: number[], newValue: number) => {
|
||||||
arr.push(newValue);
|
arr.push(newValue)
|
||||||
arr.shift();
|
arr.shift()
|
||||||
};
|
}
|
||||||
|
|
||||||
const metrics = computed(() => {
|
const metrics = computed(() => {
|
||||||
if (props.loading) {
|
if (props.loading) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
title: "CPU usage",
|
title: 'CPU usage',
|
||||||
value: "0.00%",
|
value: '0.00%',
|
||||||
max: "100%",
|
max: '100%',
|
||||||
icon: CpuIcon,
|
icon: CpuIcon,
|
||||||
data: cpuData.value,
|
data: cpuData.value,
|
||||||
showGraph: false,
|
showGraph: false,
|
||||||
warning: null,
|
warning: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Memory usage",
|
title: 'Memory usage',
|
||||||
value: "0.00%",
|
value: '0.00%',
|
||||||
max: "100%",
|
max: '100%',
|
||||||
icon: DatabaseIcon,
|
icon: DatabaseIcon,
|
||||||
data: ramData.value,
|
data: ramData.value,
|
||||||
showGraph: false,
|
showGraph: false,
|
||||||
warning: null,
|
warning: null,
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const ramPercent = Math.min(
|
const ramPercent = Math.min(
|
||||||
(stats.value.ram_usage_bytes / stats.value.ram_total_bytes) * 100,
|
(stats.value.ram_usage_bytes / stats.value.ram_total_bytes) * 100,
|
||||||
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(cpuData.value, cpuPercent)
|
||||||
updateGraphData(ramData.value, ramPercent);
|
updateGraphData(ramData.value, ramPercent)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
title: "CPU usage",
|
title: 'CPU usage',
|
||||||
value: `${cpuPercent.toFixed(2)}%`,
|
value: `${cpuPercent.toFixed(2)}%`,
|
||||||
max: "100%",
|
max: '100%',
|
||||||
icon: CpuIcon,
|
icon: CpuIcon,
|
||||||
data: cpuData.value,
|
data: cpuData.value,
|
||||||
showGraph: true,
|
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:
|
value:
|
||||||
userPreferences.value.ramAsNumber || flags.developerMode
|
userPreferences.value.ramAsNumber || flags.developerMode
|
||||||
? formatBytes(stats.value.ram_usage_bytes)
|
? formatBytes(stats.value.ram_usage_bytes)
|
||||||
@ -171,18 +171,18 @@ const metrics = computed(() => {
|
|||||||
max:
|
max:
|
||||||
userPreferences.value.ramAsNumber || flags.developerMode
|
userPreferences.value.ramAsNumber || flags.developerMode
|
||||||
? formatBytes(stats.value.ram_total_bytes)
|
? formatBytes(stats.value.ram_total_bytes)
|
||||||
: "100%",
|
: '100%',
|
||||||
icon: DatabaseIcon,
|
icon: DatabaseIcon,
|
||||||
data: ramData.value,
|
data: ramData.value,
|
||||||
showGraph: true,
|
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) => ({
|
const getChartOptions = (hasWarning: string | null, index: number) => ({
|
||||||
chart: {
|
chart: {
|
||||||
type: "area",
|
type: 'area',
|
||||||
animations: { enabled: false },
|
animations: { enabled: false },
|
||||||
sparkline: { enabled: true },
|
sparkline: { enabled: true },
|
||||||
toolbar: { show: false },
|
toolbar: { show: false },
|
||||||
@ -197,9 +197,9 @@ const getChartOptions = (hasWarning: string | null, index: number) => ({
|
|||||||
updated: () => onChartReady(index),
|
updated: () => onChartReady(index),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
stroke: { curve: "smooth", width: 3 },
|
stroke: { curve: 'smooth', width: 3 },
|
||||||
fill: {
|
fill: {
|
||||||
type: "gradient",
|
type: 'gradient',
|
||||||
gradient: {
|
gradient: {
|
||||||
shadeIntensity: 1,
|
shadeIntensity: 1,
|
||||||
opacityFrom: 0.25,
|
opacityFrom: 0.25,
|
||||||
@ -212,7 +212,7 @@ const getChartOptions = (hasWarning: string | null, index: number) => ({
|
|||||||
xaxis: {
|
xaxis: {
|
||||||
labels: { show: false },
|
labels: { show: false },
|
||||||
axisBorder: { show: false },
|
axisBorder: { show: false },
|
||||||
type: "numeric",
|
type: 'numeric',
|
||||||
tickAmount: 20,
|
tickAmount: 20,
|
||||||
range: 20,
|
range: 20,
|
||||||
},
|
},
|
||||||
@ -222,20 +222,20 @@ const getChartOptions = (hasWarning: string | null, index: number) => ({
|
|||||||
max: 100,
|
max: 100,
|
||||||
forceNiceScale: false,
|
forceNiceScale: false,
|
||||||
},
|
},
|
||||||
colors: [hasWarning ? "var(--color-orange)" : "var(--color-brand)"],
|
colors: [hasWarning ? 'var(--color-orange)' : 'var(--color-brand)'],
|
||||||
dataLabels: {
|
dataLabels: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.data?.current,
|
() => props.data?.current,
|
||||||
(newStats) => {
|
(newStats) => {
|
||||||
if (newStats) {
|
if (newStats) {
|
||||||
stats.value = newStats;
|
stats.value = newStats
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -19,30 +19,30 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { LinkIcon } from "@modrinth/assets";
|
import { LinkIcon } from '@modrinth/assets'
|
||||||
import { useStorage } from "@vueuse/core";
|
import { useStorage } from '@vueuse/core'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
subdomain: string;
|
subdomain: string
|
||||||
noSeparator?: boolean;
|
noSeparator?: boolean
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const copySubdomain = () => {
|
const copySubdomain = () => {
|
||||||
navigator.clipboard.writeText(props.subdomain + ".modrinth.gg");
|
navigator.clipboard.writeText(props.subdomain + '.modrinth.gg')
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "servers",
|
group: 'servers',
|
||||||
title: "Custom URL copied",
|
title: 'Custom URL copied',
|
||||||
text: "Your server's URL has been copied to your clipboard.",
|
text: "Your server's URL has been copied to your clipboard.",
|
||||||
type: "success",
|
type: 'success',
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const route = useNativeRoute();
|
const route = useNativeRoute()
|
||||||
const serverId = computed(() => route.params.id as string);
|
const serverId = computed(() => route.params.id as string)
|
||||||
|
|
||||||
const userPreferences = useStorage(`pyro-server-${serverId.value}-preferences`, {
|
const userPreferences = useStorage(`pyro-server-${serverId.value}-preferences`, {
|
||||||
hideSubdomainLabel: false,
|
hideSubdomainLabel: false,
|
||||||
});
|
})
|
||||||
|
|
||||||
const isHidden = computed(() => userPreferences.value.hideSubdomainLabel);
|
const isHidden = computed(() => userPreferences.value.hideSubdomainLabel)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -17,49 +17,49 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue";
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
uptimeSeconds: number;
|
uptimeSeconds: number
|
||||||
noSeparator?: boolean;
|
noSeparator?: boolean
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const formattedUptime = computed(() => {
|
const formattedUptime = computed(() => {
|
||||||
const days = Math.floor(props.uptimeSeconds / (24 * 3600));
|
const days = Math.floor(props.uptimeSeconds / (24 * 3600))
|
||||||
const hours = Math.floor((props.uptimeSeconds % (24 * 3600)) / 3600);
|
const hours = Math.floor((props.uptimeSeconds % (24 * 3600)) / 3600)
|
||||||
const minutes = Math.floor((props.uptimeSeconds % 3600) / 60);
|
const minutes = Math.floor((props.uptimeSeconds % 3600) / 60)
|
||||||
const seconds = props.uptimeSeconds % 60;
|
const seconds = props.uptimeSeconds % 60
|
||||||
|
|
||||||
let formatted = "";
|
let formatted = ''
|
||||||
if (days > 0) {
|
if (days > 0) {
|
||||||
formatted += `${days}d `;
|
formatted += `${days}d `
|
||||||
}
|
}
|
||||||
if (hours > 0 || days > 0) {
|
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 verboseUptime = computed(() => {
|
||||||
const days = Math.floor(props.uptimeSeconds / (24 * 3600));
|
const days = Math.floor(props.uptimeSeconds / (24 * 3600))
|
||||||
const hours = Math.floor((props.uptimeSeconds % (24 * 3600)) / 3600);
|
const hours = Math.floor((props.uptimeSeconds % (24 * 3600)) / 3600)
|
||||||
const minutes = Math.floor((props.uptimeSeconds % 3600) / 60);
|
const minutes = Math.floor((props.uptimeSeconds % 3600) / 60)
|
||||||
const seconds = props.uptimeSeconds % 60;
|
const seconds = props.uptimeSeconds % 60
|
||||||
|
|
||||||
let verbose = "";
|
let verbose = ''
|
||||||
if (days > 0) {
|
if (days > 0) {
|
||||||
verbose += `${days} day${days > 1 ? "s" : ""} `;
|
verbose += `${days} day${days > 1 ? 's' : ''} `
|
||||||
}
|
}
|
||||||
if (hours > 0) {
|
if (hours > 0) {
|
||||||
verbose += `${hours} hour${hours > 1 ? "s" : ""} `;
|
verbose += `${hours} hour${hours > 1 ? 's' : ''} `
|
||||||
}
|
}
|
||||||
if (minutes > 0) {
|
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>
|
</script>
|
||||||
|
|||||||
@ -42,7 +42,7 @@
|
|||||||
v-if="typeof option.action === 'function'"
|
v-if="typeof option.action === 'function'"
|
||||||
:ref="
|
:ref="
|
||||||
(el) => {
|
(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"
|
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('/')"
|
v-else-if="typeof option.action === 'string' && option.action.startsWith('/')"
|
||||||
:ref="
|
:ref="
|
||||||
(el) => {
|
(el) => {
|
||||||
if (el) menuItemsRef[index] = el as HTMLElement;
|
if (el) menuItemsRef[index] = el as HTMLElement
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
:to="option.action"
|
:to="option.action"
|
||||||
@ -75,7 +75,7 @@
|
|||||||
v-else-if="typeof option.action === 'string' && !option.action.startsWith('http')"
|
v-else-if="typeof option.action === 'string' && !option.action.startsWith('http')"
|
||||||
:ref="
|
:ref="
|
||||||
(el) => {
|
(el) => {
|
||||||
if (el) menuItemsRef[index] = el as HTMLElement;
|
if (el) menuItemsRef[index] = el as HTMLElement
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
:href="option.action"
|
:href="option.action"
|
||||||
@ -101,347 +101,347 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
import { ButtonStyled } from '@modrinth/ui'
|
||||||
import { onClickOutside, useElementHover } from "@vueuse/core";
|
import { onClickOutside, useElementHover } from '@vueuse/core'
|
||||||
import { computed,nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
interface Option {
|
interface Option {
|
||||||
id: string;
|
id: string
|
||||||
action?: (() => void) | string;
|
action?: (() => void) | string
|
||||||
shown?: boolean;
|
shown?: boolean
|
||||||
color?: "standard" | "brand" | "red" | "orange" | "green" | "blue" | "purple";
|
color?: 'standard' | 'brand' | 'red' | 'orange' | 'green' | 'blue' | 'purple'
|
||||||
}
|
}
|
||||||
|
|
||||||
type Divider = {
|
type Divider = {
|
||||||
divider: true;
|
divider: true
|
||||||
shown?: boolean;
|
shown?: boolean
|
||||||
};
|
}
|
||||||
|
|
||||||
type Item = Option | Divider;
|
type Item = Option | Divider
|
||||||
|
|
||||||
function isDivider(item: Item): item is Divider {
|
function isDivider(item: Item): item is Divider {
|
||||||
return (item as Divider).divider;
|
return (item as Divider).divider
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
options: Item[];
|
options: Item[]
|
||||||
hoverable?: boolean;
|
hoverable?: boolean
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
hoverable: false,
|
hoverable: false,
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "select", option: Option): void;
|
(e: 'select', option: Option): void
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const isOpen = ref(false);
|
const isOpen = ref(false)
|
||||||
const selectedIndex = ref(-1);
|
const selectedIndex = ref(-1)
|
||||||
const menuRef = ref<HTMLElement | null>(null);
|
const menuRef = ref<HTMLElement | null>(null)
|
||||||
const triggerRef = ref<HTMLElement | null>(null);
|
const triggerRef = ref<HTMLElement | null>(null)
|
||||||
const isMouseDown = ref(false);
|
const isMouseDown = ref(false)
|
||||||
const typeAheadBuffer = ref("");
|
const typeAheadBuffer = ref('')
|
||||||
const typeAheadTimeout = ref<number | null>(null);
|
const typeAheadTimeout = ref<number | null>(null)
|
||||||
const menuItemsRef = ref<HTMLElement[]>([]);
|
const menuItemsRef = ref<HTMLElement[]>([])
|
||||||
|
|
||||||
const hoveringTrigger = useElementHover(triggerRef);
|
const hoveringTrigger = useElementHover(triggerRef)
|
||||||
const hoveringMenu = useElementHover(menuRef);
|
const hoveringMenu = useElementHover(menuRef)
|
||||||
|
|
||||||
const hovering = computed(() => hoveringTrigger.value || hoveringMenu.value);
|
const hovering = computed(() => hoveringTrigger.value || hoveringMenu.value)
|
||||||
|
|
||||||
const menuStyle = ref({
|
const menuStyle = ref({
|
||||||
top: "0px",
|
top: '0px',
|
||||||
left: "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 = () => {
|
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 triggerRect = triggerRef.value.getBoundingClientRect()
|
||||||
const menuRect = menuRef.value.getBoundingClientRect();
|
const menuRect = menuRef.value.getBoundingClientRect()
|
||||||
const menuWidth = menuRect.width;
|
const menuWidth = menuRect.width
|
||||||
const menuHeight = menuRect.height;
|
const menuHeight = menuRect.height
|
||||||
const margin = 8;
|
const margin = 8
|
||||||
|
|
||||||
let top: number;
|
let top: number
|
||||||
let left: number;
|
let left: number
|
||||||
|
|
||||||
// okay gang lets calculate this shit
|
// okay gang lets calculate this shit
|
||||||
// from the top now yall
|
// from the top now yall
|
||||||
// y
|
// y
|
||||||
if (triggerRect.bottom + menuHeight + margin <= window.innerHeight) {
|
if (triggerRect.bottom + menuHeight + margin <= window.innerHeight) {
|
||||||
top = triggerRect.bottom + margin;
|
top = triggerRect.bottom + margin
|
||||||
} else if (triggerRect.top - menuHeight - margin >= 0) {
|
} else if (triggerRect.top - menuHeight - margin >= 0) {
|
||||||
top = triggerRect.top - menuHeight - margin;
|
top = triggerRect.top - menuHeight - margin
|
||||||
} else {
|
} else {
|
||||||
top = Math.max(margin, window.innerHeight - menuHeight - margin);
|
top = Math.max(margin, window.innerHeight - menuHeight - margin)
|
||||||
}
|
}
|
||||||
|
|
||||||
// x
|
// x
|
||||||
if (triggerRect.left + menuWidth + margin <= window.innerWidth) {
|
if (triggerRect.left + menuWidth + margin <= window.innerWidth) {
|
||||||
left = triggerRect.left;
|
left = triggerRect.left
|
||||||
} else if (triggerRect.right - menuWidth - margin >= 0) {
|
} else if (triggerRect.right - menuWidth - margin >= 0) {
|
||||||
left = triggerRect.right - menuWidth;
|
left = triggerRect.right - menuWidth
|
||||||
} else {
|
} else {
|
||||||
left = Math.max(margin, window.innerWidth - menuWidth - margin);
|
left = Math.max(margin, window.innerWidth - menuWidth - margin)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
top: `${top}px`,
|
top: `${top}px`,
|
||||||
left: `${left}px`,
|
left: `${left}px`,
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const toggleMenu = (event: MouseEvent) => {
|
const toggleMenu = (event: MouseEvent) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation()
|
||||||
if (!props.hoverable) {
|
if (!props.hoverable) {
|
||||||
if (isOpen.value) {
|
if (isOpen.value) {
|
||||||
closeMenu();
|
closeMenu()
|
||||||
} else {
|
} else {
|
||||||
openMenu();
|
openMenu()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const openMenu = () => {
|
const openMenu = () => {
|
||||||
isOpen.value = true;
|
isOpen.value = true
|
||||||
disableBodyScroll();
|
disableBodyScroll()
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
menuStyle.value = calculateMenuPosition();
|
menuStyle.value = calculateMenuPosition()
|
||||||
document.addEventListener("mousemove", handleMouseMove);
|
document.addEventListener('mousemove', handleMouseMove)
|
||||||
focusFirstMenuItem();
|
focusFirstMenuItem()
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const closeMenu = () => {
|
const closeMenu = () => {
|
||||||
isOpen.value = false;
|
isOpen.value = false
|
||||||
selectedIndex.value = -1;
|
selectedIndex.value = -1
|
||||||
enableBodyScroll();
|
enableBodyScroll()
|
||||||
document.removeEventListener("mousemove", handleMouseMove);
|
document.removeEventListener('mousemove', handleMouseMove)
|
||||||
};
|
}
|
||||||
|
|
||||||
const selectOption = (option: Option) => {
|
const selectOption = (option: Option) => {
|
||||||
emit("select", option);
|
emit('select', option)
|
||||||
if (typeof option.action === "function") {
|
if (typeof option.action === 'function') {
|
||||||
option.action();
|
option.action()
|
||||||
}
|
}
|
||||||
closeMenu();
|
closeMenu()
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleMouseDown = (event: MouseEvent) => {
|
const handleMouseDown = (event: MouseEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
isMouseDown.value = true;
|
isMouseDown.value = true
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
if (props.hoverable) {
|
if (props.hoverable) {
|
||||||
openMenu();
|
openMenu()
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
const handleMouseLeave = () => {
|
||||||
if (props.hoverable) {
|
if (props.hoverable) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!hovering.value) {
|
if (!hovering.value) {
|
||||||
closeMenu();
|
closeMenu()
|
||||||
}
|
}
|
||||||
}, 250);
|
}, 250)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleMouseMove = (event: MouseEvent) => {
|
const handleMouseMove = (event: MouseEvent) => {
|
||||||
if (!isOpen.value || !isMouseDown.value) return;
|
if (!isOpen.value || !isMouseDown.value) return
|
||||||
|
|
||||||
const menuRect = menuRef.value?.getBoundingClientRect();
|
const menuRect = menuRef.value?.getBoundingClientRect()
|
||||||
if (!menuRect) return;
|
if (!menuRect) return
|
||||||
|
|
||||||
const menuItems = menuRef.value?.querySelectorAll('[role="menuitem"]');
|
const menuItems = menuRef.value?.querySelectorAll('[role="menuitem"]')
|
||||||
if (!menuItems) return;
|
if (!menuItems) return
|
||||||
|
|
||||||
for (let i = 0; i < menuItems.length; i++) {
|
for (let i = 0; i < menuItems.length; i++) {
|
||||||
const itemRect = (menuItems[i] as HTMLElement).getBoundingClientRect();
|
const itemRect = (menuItems[i] as HTMLElement).getBoundingClientRect()
|
||||||
if (
|
if (
|
||||||
event.clientX >= itemRect.left &&
|
event.clientX >= itemRect.left &&
|
||||||
event.clientX <= itemRect.right &&
|
event.clientX <= itemRect.right &&
|
||||||
event.clientY >= itemRect.top &&
|
event.clientY >= itemRect.top &&
|
||||||
event.clientY <= itemRect.bottom
|
event.clientY <= itemRect.bottom
|
||||||
) {
|
) {
|
||||||
selectedIndex.value = i;
|
selectedIndex.value = i
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleItemClick = (option: Option, index: number) => {
|
const handleItemClick = (option: Option, index: number) => {
|
||||||
selectedIndex.value = index;
|
selectedIndex.value = index
|
||||||
selectOption(option);
|
selectOption(option)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleMouseOver = (index: number) => {
|
const handleMouseOver = (index: number) => {
|
||||||
selectedIndex.value = index;
|
selectedIndex.value = index
|
||||||
menuItemsRef.value[selectedIndex.value].focus?.();
|
menuItemsRef.value[selectedIndex.value].focus?.()
|
||||||
};
|
}
|
||||||
|
|
||||||
// Scrolling is disabled for keyboard navigation
|
// Scrolling is disabled for keyboard navigation
|
||||||
const disableBodyScroll = () => {
|
const disableBodyScroll = () => {
|
||||||
// Make opening not shift page when there's a vertical scrollbar
|
// 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) {
|
if (scrollBarWidth > 0) {
|
||||||
document.body.style.paddingRight = `${scrollBarWidth}px`;
|
document.body.style.paddingRight = `${scrollBarWidth}px`
|
||||||
} else {
|
} else {
|
||||||
document.body.style.paddingRight = "";
|
document.body.style.paddingRight = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
document.body.style.overflow = "hidden";
|
document.body.style.overflow = 'hidden'
|
||||||
};
|
}
|
||||||
|
|
||||||
const enableBodyScroll = () => {
|
const enableBodyScroll = () => {
|
||||||
document.body.style.paddingRight = "";
|
document.body.style.paddingRight = ''
|
||||||
document.body.style.overflow = "";
|
document.body.style.overflow = ''
|
||||||
};
|
}
|
||||||
|
|
||||||
const focusFirstMenuItem = () => {
|
const focusFirstMenuItem = () => {
|
||||||
if (menuItemsRef.value.length > 0) {
|
if (menuItemsRef.value.length > 0) {
|
||||||
menuItemsRef.value[0].focus?.();
|
menuItemsRef.value[0].focus?.()
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleKeydown = (event: KeyboardEvent) => {
|
const handleKeydown = (event: KeyboardEvent) => {
|
||||||
if (!isOpen.value) {
|
if (!isOpen.value) {
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
openMenu();
|
openMenu()
|
||||||
}
|
}
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case "ArrowDown":
|
case 'ArrowDown':
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
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
|
||||||
case "ArrowUp":
|
case 'ArrowUp':
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
selectedIndex.value =
|
selectedIndex.value =
|
||||||
(selectedIndex.value - 1 + filteredOptions.value.length) % filteredOptions.value.length;
|
(selectedIndex.value - 1 + filteredOptions.value.length) % filteredOptions.value.length
|
||||||
menuItemsRef.value[selectedIndex.value].focus?.();
|
menuItemsRef.value[selectedIndex.value].focus?.()
|
||||||
break;
|
break
|
||||||
case "Home":
|
case 'Home':
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
if (menuItemsRef.value.length > 0) {
|
if (menuItemsRef.value.length > 0) {
|
||||||
selectedIndex.value = 0;
|
selectedIndex.value = 0
|
||||||
menuItemsRef.value[selectedIndex.value].focus?.();
|
menuItemsRef.value[selectedIndex.value].focus?.()
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
case "End":
|
case 'End':
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
if (menuItemsRef.value.length > 0) {
|
if (menuItemsRef.value.length > 0) {
|
||||||
selectedIndex.value = filteredOptions.value.length - 1;
|
selectedIndex.value = filteredOptions.value.length - 1
|
||||||
menuItemsRef.value[selectedIndex.value].focus?.();
|
menuItemsRef.value[selectedIndex.value].focus?.()
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
case "Enter":
|
case 'Enter':
|
||||||
case " ":
|
case ' ':
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
if (selectedIndex.value >= 0) {
|
if (selectedIndex.value >= 0) {
|
||||||
const option = filteredOptions.value[selectedIndex.value];
|
const option = filteredOptions.value[selectedIndex.value]
|
||||||
if (isDivider(option)) break;
|
if (isDivider(option)) break
|
||||||
selectOption(option);
|
selectOption(option)
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
case "Escape":
|
case 'Escape':
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
closeMenu();
|
closeMenu()
|
||||||
triggerRef.value?.focus?.();
|
triggerRef.value?.focus?.()
|
||||||
break;
|
break
|
||||||
case "Tab":
|
case 'Tab':
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
if (menuItemsRef.value.length > 0) {
|
if (menuItemsRef.value.length > 0) {
|
||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
selectedIndex.value =
|
selectedIndex.value =
|
||||||
(selectedIndex.value - 1 + filteredOptions.value.length) % filteredOptions.value.length;
|
(selectedIndex.value - 1 + filteredOptions.value.length) % filteredOptions.value.length
|
||||||
} else {
|
} 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:
|
default:
|
||||||
if (event.key.length === 1) {
|
if (event.key.length === 1) {
|
||||||
typeAheadBuffer.value += event.key.toLowerCase();
|
typeAheadBuffer.value += event.key.toLowerCase()
|
||||||
const matchIndex = filteredOptions.value.findIndex(
|
const matchIndex = filteredOptions.value.findIndex(
|
||||||
(option) =>
|
(option) =>
|
||||||
!isDivider(option) && option.id.toLowerCase().startsWith(typeAheadBuffer.value),
|
!isDivider(option) && option.id.toLowerCase().startsWith(typeAheadBuffer.value),
|
||||||
);
|
)
|
||||||
if (matchIndex !== -1) {
|
if (matchIndex !== -1) {
|
||||||
selectedIndex.value = matchIndex;
|
selectedIndex.value = matchIndex
|
||||||
menuItemsRef.value[selectedIndex.value].focus?.();
|
menuItemsRef.value[selectedIndex.value].focus?.()
|
||||||
}
|
}
|
||||||
if (typeAheadTimeout.value) {
|
if (typeAheadTimeout.value) {
|
||||||
clearTimeout(typeAheadTimeout.value);
|
clearTimeout(typeAheadTimeout.value)
|
||||||
}
|
}
|
||||||
typeAheadTimeout.value = setTimeout(() => {
|
typeAheadTimeout.value = setTimeout(() => {
|
||||||
typeAheadBuffer.value = "";
|
typeAheadBuffer.value = ''
|
||||||
}, 1000) as unknown as number;
|
}, 1000) as unknown as number
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleResizeOrScroll = () => {
|
const handleResizeOrScroll = () => {
|
||||||
if (isOpen.value) {
|
if (isOpen.value) {
|
||||||
menuStyle.value = calculateMenuPosition();
|
menuStyle.value = calculateMenuPosition()
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const throttle = (func: (...args: any[]) => void, limit: number): ((...args: any[]) => void) => {
|
const throttle = (func: (...args: any[]) => void, limit: number): ((...args: any[]) => void) => {
|
||||||
let inThrottle: boolean;
|
let inThrottle: boolean
|
||||||
return function (...args: any[]) {
|
return function (...args: any[]) {
|
||||||
if (!inThrottle) {
|
if (!inThrottle) {
|
||||||
func(...args);
|
func(...args)
|
||||||
inThrottle = true;
|
inThrottle = true
|
||||||
setTimeout(() => (inThrottle = false), limit);
|
setTimeout(() => (inThrottle = false), limit)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const throttledHandleResizeOrScroll = throttle(handleResizeOrScroll, 100);
|
const throttledHandleResizeOrScroll = throttle(handleResizeOrScroll, 100)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
triggerRef.value?.addEventListener("keydown", handleKeydown);
|
triggerRef.value?.addEventListener('keydown', handleKeydown)
|
||||||
window.addEventListener("resize", throttledHandleResizeOrScroll);
|
window.addEventListener('resize', throttledHandleResizeOrScroll)
|
||||||
window.addEventListener("scroll", throttledHandleResizeOrScroll);
|
window.addEventListener('scroll', throttledHandleResizeOrScroll)
|
||||||
});
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
triggerRef.value?.removeEventListener("keydown", handleKeydown);
|
triggerRef.value?.removeEventListener('keydown', handleKeydown)
|
||||||
window.removeEventListener("resize", throttledHandleResizeOrScroll);
|
window.removeEventListener('resize', throttledHandleResizeOrScroll)
|
||||||
window.removeEventListener("scroll", throttledHandleResizeOrScroll);
|
window.removeEventListener('scroll', throttledHandleResizeOrScroll)
|
||||||
document.removeEventListener("mousemove", handleMouseMove);
|
document.removeEventListener('mousemove', handleMouseMove)
|
||||||
if (typeAheadTimeout.value) {
|
if (typeAheadTimeout.value) {
|
||||||
clearTimeout(typeAheadTimeout.value);
|
clearTimeout(typeAheadTimeout.value)
|
||||||
}
|
}
|
||||||
enableBodyScroll();
|
enableBodyScroll()
|
||||||
});
|
})
|
||||||
|
|
||||||
watch(isOpen, (newValue) => {
|
watch(isOpen, (newValue) => {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
menuRef.value?.addEventListener("keydown", handleKeydown);
|
menuRef.value?.addEventListener('keydown', handleKeydown)
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
menuRef.value?.removeEventListener("keydown", handleKeydown);
|
menuRef.value?.removeEventListener('keydown', handleKeydown)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
onClickOutside(menuRef, (event) => {
|
onClickOutside(menuRef, (event) => {
|
||||||
if (!triggerRef.value?.contains(event.target as Node)) {
|
if (!triggerRef.value?.contains(event.target as Node)) {
|
||||||
closeMenu();
|
closeMenu()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -223,10 +223,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { LoaderIcon } from "@modrinth/assets";
|
import { LoaderIcon } from '@modrinth/assets'
|
||||||
import type { Loaders } from "@modrinth/utils";
|
import type { Loaders } from '@modrinth/utils'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
loader: Loaders;
|
loader: Loaders
|
||||||
}>();
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,95 +1,95 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ButtonStyled, ServersSpecs } from "@modrinth/ui";
|
import { ButtonStyled, ServersSpecs } from '@modrinth/ui'
|
||||||
import { formatPrice } from "@modrinth/utils";
|
import { formatPrice } from '@modrinth/utils'
|
||||||
import type { MessageDescriptor } from "@vintl/vintl";
|
import type { MessageDescriptor } from '@vintl/vintl'
|
||||||
|
|
||||||
const { formatMessage, locale } = useVIntl();
|
const { formatMessage, locale } = useVIntl()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
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<
|
const plans: Record<
|
||||||
Plan,
|
Plan,
|
||||||
{
|
{
|
||||||
buttonColor: "blue" | "green" | "purple";
|
buttonColor: 'blue' | 'green' | 'purple'
|
||||||
accentText: string;
|
accentText: string
|
||||||
accentBg: string;
|
accentBg: string
|
||||||
name: MessageDescriptor;
|
name: MessageDescriptor
|
||||||
description: MessageDescriptor;
|
description: MessageDescriptor
|
||||||
mostPopular: boolean;
|
mostPopular: boolean
|
||||||
}
|
}
|
||||||
> = {
|
> = {
|
||||||
small: {
|
small: {
|
||||||
buttonColor: "blue",
|
buttonColor: 'blue',
|
||||||
accentText: "text-blue",
|
accentText: 'text-blue',
|
||||||
accentBg: "bg-bg-blue",
|
accentBg: 'bg-bg-blue',
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: "servers.plan.small.name",
|
id: 'servers.plan.small.name',
|
||||||
defaultMessage: "Small",
|
defaultMessage: 'Small',
|
||||||
}),
|
}),
|
||||||
description: defineMessage({
|
description: defineMessage({
|
||||||
id: "servers.plan.small.description",
|
id: 'servers.plan.small.description',
|
||||||
defaultMessage: "Perfect for 1–5 friends with a few light mods.",
|
defaultMessage: 'Perfect for 1–5 friends with a few light mods.',
|
||||||
}),
|
}),
|
||||||
mostPopular: false,
|
mostPopular: false,
|
||||||
},
|
},
|
||||||
medium: {
|
medium: {
|
||||||
buttonColor: "green",
|
buttonColor: 'green',
|
||||||
accentText: "text-green",
|
accentText: 'text-green',
|
||||||
accentBg: "bg-bg-green",
|
accentBg: 'bg-bg-green',
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: "servers.plan.medium.name",
|
id: 'servers.plan.medium.name',
|
||||||
defaultMessage: "Medium",
|
defaultMessage: 'Medium',
|
||||||
}),
|
}),
|
||||||
description: defineMessage({
|
description: defineMessage({
|
||||||
id: "servers.plan.medium.description",
|
id: 'servers.plan.medium.description',
|
||||||
defaultMessage: "Great for 6–15 players and multiple mods.",
|
defaultMessage: 'Great for 6–15 players and multiple mods.',
|
||||||
}),
|
}),
|
||||||
mostPopular: true,
|
mostPopular: true,
|
||||||
},
|
},
|
||||||
large: {
|
large: {
|
||||||
buttonColor: "purple",
|
buttonColor: 'purple',
|
||||||
accentText: "text-purple",
|
accentText: 'text-purple',
|
||||||
accentBg: "bg-bg-purple",
|
accentBg: 'bg-bg-purple',
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: "servers.plan.large.name",
|
id: 'servers.plan.large.name',
|
||||||
defaultMessage: "Large",
|
defaultMessage: 'Large',
|
||||||
}),
|
}),
|
||||||
description: defineMessage({
|
description: defineMessage({
|
||||||
id: "servers.plan.large.description",
|
id: 'servers.plan.large.description',
|
||||||
defaultMessage: "Ideal for 15–25 players, modpacks, or heavy modding.",
|
defaultMessage: 'Ideal for 15–25 players, modpacks, or heavy modding.',
|
||||||
}),
|
}),
|
||||||
mostPopular: false,
|
mostPopular: false,
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
capacity?: number;
|
capacity?: number
|
||||||
plan: Plan;
|
plan: Plan
|
||||||
ram: number;
|
ram: number
|
||||||
storage: number;
|
storage: number
|
||||||
cpus: number;
|
cpus: number
|
||||||
price: number;
|
price: number
|
||||||
interval: "monthly" | "quarterly" | "yearly";
|
interval: 'monthly' | 'quarterly' | 'yearly'
|
||||||
currency: string;
|
currency: string
|
||||||
isUsa: boolean;
|
isUsa: boolean
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const outOfStock = computed(() => {
|
const outOfStock = computed(() => {
|
||||||
return !props.capacity || props.capacity === 0;
|
return !props.capacity || props.capacity === 0
|
||||||
});
|
})
|
||||||
|
|
||||||
const billingMonths = computed(() => {
|
const billingMonths = computed(() => {
|
||||||
if (props.interval === "yearly") {
|
if (props.interval === 'yearly') {
|
||||||
return 12;
|
return 12
|
||||||
} else if (props.interval === "quarterly") {
|
} else if (props.interval === 'quarterly') {
|
||||||
return 3;
|
return 3
|
||||||
}
|
}
|
||||||
return 1;
|
return 1
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -122,7 +122,7 @@ const billingMonths = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
<span class="m-0 text-2xl font-bold text-contrast">
|
<span class="m-0 text-2xl font-bold text-contrast">
|
||||||
{{ formatPrice(locale, price / billingMonths, currency, true) }}
|
{{ formatPrice(locale, price / billingMonths, currency, true) }}
|
||||||
{{ isUsa ? "" : currency }}
|
{{ isUsa ? '' : currency }}
|
||||||
<span class="text-lg font-semibold text-secondary">
|
<span class="text-lg font-semibold text-secondary">
|
||||||
/ month<template v-if="interval !== 'monthly'">, billed {{ interval }}</template>
|
/ month<template v-if="interval !== 'monthly'">, billed {{ interval }}</template>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -1,112 +1,112 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { PlusIcon, XIcon } from "@modrinth/assets";
|
import { PlusIcon, XIcon } from '@modrinth/assets'
|
||||||
import { Accordion, ButtonStyled, NewModal, ServerNotice, TagItem } from "@modrinth/ui";
|
import { Accordion, ButtonStyled, NewModal, ServerNotice, TagItem } from '@modrinth/ui'
|
||||||
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
|
import type { ServerNotice as ServerNoticeType } from '@modrinth/utils'
|
||||||
import { ref } from "vue";
|
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<{
|
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 assignedServers = computed(() => assigned.value.filter((n) => n.kind === 'server') ?? [])
|
||||||
const assignedNodes = computed(() => assigned.value.filter((n) => n.kind === "node") ?? []);
|
const assignedNodes = computed(() => assigned.value.filter((n) => n.kind === 'node') ?? [])
|
||||||
|
|
||||||
const inputField = ref("");
|
const inputField = ref('')
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
await useServersFetch("notices").then((res) => {
|
await useServersFetch('notices').then((res) => {
|
||||||
const notices = res as ServerNoticeType[];
|
const notices = res as ServerNoticeType[]
|
||||||
assigned.value = notices.find((n) => n.id === notice.value?.id)?.assigned ?? [];
|
assigned.value = notices.find((n) => n.id === notice.value?.id)?.assigned ?? []
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function assign(server: boolean = true) {
|
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(
|
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) => {
|
).catch((err) => {
|
||||||
app.$notify({
|
app.$notify({
|
||||||
group: "main",
|
group: 'main',
|
||||||
title: "Error assigning notice",
|
title: 'Error assigning notice',
|
||||||
text: err,
|
text: err,
|
||||||
type: "error",
|
type: 'error',
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
app.$notify({
|
app.$notify({
|
||||||
group: "main",
|
group: 'main',
|
||||||
title: "Error assigning notice",
|
title: 'Error assigning notice',
|
||||||
text: "No server or node specified",
|
text: 'No server or node specified',
|
||||||
type: "error",
|
type: 'error',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
await refresh();
|
await refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function unassignDetect() {
|
async function unassignDetect() {
|
||||||
const input = inputField.value.trim();
|
const input = inputField.value.trim()
|
||||||
|
|
||||||
const server = assignedServers.value.some((assigned) => assigned.id === input);
|
const server = assignedServers.value.some((assigned) => assigned.id === input)
|
||||||
const node = assignedNodes.value.some((assigned) => assigned.id === input);
|
const node = assignedNodes.value.some((assigned) => assigned.id === input)
|
||||||
|
|
||||||
if (!server && !node) {
|
if (!server && !node) {
|
||||||
app.$notify({
|
app.$notify({
|
||||||
group: "main",
|
group: 'main',
|
||||||
title: "Error unassigning notice",
|
title: 'Error unassigning notice',
|
||||||
text: "ID is not an assigned server or node",
|
text: 'ID is not an assigned server or node',
|
||||||
type: "error",
|
type: 'error',
|
||||||
});
|
})
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await unassign(input, server);
|
await unassign(input, server)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function unassign(id: string, server: boolean = true) {
|
async function unassign(id: string, server: boolean = true) {
|
||||||
if (notice.value) {
|
if (notice.value) {
|
||||||
await useServersFetch(
|
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) => {
|
).catch((err) => {
|
||||||
app.$notify({
|
app.$notify({
|
||||||
group: "main",
|
group: 'main',
|
||||||
title: "Error unassigning notice",
|
title: 'Error unassigning notice',
|
||||||
text: err,
|
text: err,
|
||||||
type: "error",
|
type: 'error',
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
await refresh();
|
await refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
function show(currentNotice: ServerNoticeType) {
|
function show(currentNotice: ServerNoticeType) {
|
||||||
notice.value = currentNotice;
|
notice.value = currentNotice
|
||||||
assigned.value = currentNotice?.assigned ?? [];
|
assigned.value = currentNotice?.assigned ?? []
|
||||||
modal.value?.show();
|
modal.value?.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
modal.value?.hide();
|
modal.value?.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({ show, hide });
|
defineExpose({ show, hide })
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<NewModal ref="modal" :on-hide="() => emit('close')">
|
<NewModal ref="modal" :on-hide="() => emit('close')">
|
||||||
@ -165,7 +165,7 @@ defineExpose({ show, hide });
|
|||||||
:key="`node-${node.id}`"
|
:key="`node-${node.id}`"
|
||||||
:action="
|
:action="
|
||||||
() => {
|
() => {
|
||||||
unassign(node.id, false);
|
unassign(node.id, false)
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -102,11 +102,11 @@ import {
|
|||||||
MoreHorizontalIcon,
|
MoreHorizontalIcon,
|
||||||
ScaleIcon,
|
ScaleIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from "@modrinth/assets";
|
} from '@modrinth/assets'
|
||||||
import { AutoLink, Avatar, Badge, OverflowMenu, useRelativeTime } from "@modrinth/ui";
|
import { AutoLink, Avatar, Badge, OverflowMenu, useRelativeTime } from '@modrinth/ui'
|
||||||
import { renderString } from "@modrinth/utils";
|
import { renderString } from '@modrinth/utils'
|
||||||
|
|
||||||
import { isStaff } from "~/helpers/users.js";
|
import { isStaff } from '~/helpers/users.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
message: {
|
message: {
|
||||||
@ -137,34 +137,34 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const emit = defineEmits(["update-thread"]);
|
const emit = defineEmits(['update-thread'])
|
||||||
|
|
||||||
const formattedMessage = computed(() => {
|
const formattedMessage = computed(() => {
|
||||||
const body = renderString(props.message.body.body);
|
const body = renderString(props.message.body.body)
|
||||||
if (props.forceCompact) {
|
if (props.forceCompact) {
|
||||||
const hasImage = body.includes("<img");
|
const hasImage = body.includes('<img')
|
||||||
const noHtml = body.replace(/<\/?[^>]+(>|$)/g, "");
|
const noHtml = body.replace(/<\/?[^>]+(>|$)/g, '')
|
||||||
if (noHtml.trim()) {
|
if (noHtml.trim()) {
|
||||||
return noHtml;
|
return noHtml
|
||||||
} else if (hasImage) {
|
} else if (hasImage) {
|
||||||
return "sent an image.";
|
return 'sent an image.'
|
||||||
} else {
|
} else {
|
||||||
return "sent a message.";
|
return 'sent a message.'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return body;
|
return body
|
||||||
});
|
})
|
||||||
|
|
||||||
const formatRelativeTime = useRelativeTime();
|
const formatRelativeTime = useRelativeTime()
|
||||||
const timeSincePosted = ref(formatRelativeTime(props.message.created));
|
const timeSincePosted = ref(formatRelativeTime(props.message.created))
|
||||||
|
|
||||||
async function deleteMessage() {
|
async function deleteMessage() {
|
||||||
await useBaseFetch(`message/${props.message.id}`, {
|
await useBaseFetch(`message/${props.message.id}`, {
|
||||||
method: "DELETE",
|
method: 'DELETE',
|
||||||
});
|
})
|
||||||
emit("update-thread");
|
emit('update-thread')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -189,9 +189,9 @@ async function deleteMessage() {
|
|||||||
--gap-size: var(--spacing-card-sm);
|
--gap-size: var(--spacing-card-sm);
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template:
|
grid-template:
|
||||||
"icon author actions"
|
'icon author actions'
|
||||||
"icon body actions"
|
'icon body actions'
|
||||||
"date date date";
|
'date date date';
|
||||||
grid-template-columns: min-content auto 1fr;
|
grid-template-columns: min-content auto 1fr;
|
||||||
column-gap: var(--gap-size);
|
column-gap: var(--gap-size);
|
||||||
row-gap: var(--spacing-card-xs);
|
row-gap: var(--spacing-card-xs);
|
||||||
@ -307,9 +307,9 @@ a:active + .message__author a,
|
|||||||
|
|
||||||
&.has-body {
|
&.has-body {
|
||||||
grid-template:
|
grid-template:
|
||||||
"icon author actions"
|
'icon author actions'
|
||||||
"icon body actions"
|
'icon body actions'
|
||||||
"date date date";
|
'date date date';
|
||||||
grid-template-columns: min-content auto 1fr;
|
grid-template-columns: min-content auto 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -322,8 +322,8 @@ a:active + .message__author a,
|
|||||||
|
|
||||||
&.has-body {
|
&.has-body {
|
||||||
grid-template:
|
grid-template:
|
||||||
"icon author date actions"
|
'icon author date actions'
|
||||||
"icon body body actions";
|
'icon body body actions';
|
||||||
grid-template-columns: min-content auto 1fr;
|
grid-template-columns: min-content auto 1fr;
|
||||||
grid-template-rows: min-content 1fr auto;
|
grid-template-rows: min-content 1fr auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,9 +24,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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({
|
const props = defineProps({
|
||||||
thread: {
|
thread: {
|
||||||
@ -50,36 +50,36 @@ const props = defineProps({
|
|||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
default() {
|
default() {
|
||||||
return [];
|
return []
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const app = useNuxtApp();
|
const app = useNuxtApp()
|
||||||
|
|
||||||
const members = computed(() => {
|
const members = computed(() => {
|
||||||
const members = {};
|
const members = {}
|
||||||
for (const member of props.thread.members) {
|
for (const member of props.thread.members) {
|
||||||
members[member.id] = member;
|
members[member.id] = member
|
||||||
}
|
}
|
||||||
members[props.auth.user.id] = props.auth.user;
|
members[props.auth.user.id] = props.auth.user
|
||||||
return members;
|
return members
|
||||||
});
|
})
|
||||||
|
|
||||||
const displayMessages = computed(() => {
|
const displayMessages = computed(() => {
|
||||||
const sortedMessages = props.thread.messages
|
const sortedMessages = props.thread.messages
|
||||||
.slice()
|
.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) {
|
if (props.messages.length > 0) {
|
||||||
return sortedMessages.filter((msg) => props.messages.includes(msg.id));
|
return sortedMessages.filter((msg) => props.messages.includes(msg.id))
|
||||||
} else {
|
} else {
|
||||||
return sortedMessages.length > 0 ? [sortedMessages[sortedMessages.length - 1]] : [];
|
return sortedMessages.length > 0 ? [sortedMessages[sortedMessages.length - 1]] : []
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@ -1,71 +1,71 @@
|
|||||||
export const useAuth = async (oldToken = null) => {
|
export const useAuth = async (oldToken = null) => {
|
||||||
const auth = useState("auth", () => ({
|
const auth = useState('auth', () => ({
|
||||||
user: null,
|
user: null,
|
||||||
token: "",
|
token: '',
|
||||||
headers: {},
|
headers: {},
|
||||||
}));
|
}))
|
||||||
|
|
||||||
if (!auth.value.user || oldToken) {
|
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) => {
|
export const initAuth = async (oldToken = null) => {
|
||||||
const auth = {
|
const auth = {
|
||||||
user: null,
|
user: null,
|
||||||
token: "",
|
token: '',
|
||||||
};
|
|
||||||
|
|
||||||
if (oldToken === "none") {
|
|
||||||
return auth;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const route = useRoute();
|
if (oldToken === 'none') {
|
||||||
const authCookie = useCookie("auth-token", {
|
return auth
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const authCookie = useCookie('auth-token', {
|
||||||
maxAge: 60 * 60 * 24 * 365 * 10,
|
maxAge: 60 * 60 * 24 * 365 * 10,
|
||||||
sameSite: "lax",
|
sameSite: 'lax',
|
||||||
secure: true,
|
secure: true,
|
||||||
httpOnly: false,
|
httpOnly: false,
|
||||||
path: "/",
|
path: '/',
|
||||||
});
|
})
|
||||||
|
|
||||||
if (oldToken) {
|
if (oldToken) {
|
||||||
authCookie.value = oldToken;
|
authCookie.value = oldToken
|
||||||
}
|
}
|
||||||
|
|
||||||
if (route.query.code && !route.fullPath.includes("new_account=true")) {
|
if (route.query.code && !route.fullPath.includes('new_account=true')) {
|
||||||
authCookie.value = route.query.code;
|
authCookie.value = route.query.code
|
||||||
}
|
}
|
||||||
|
|
||||||
if (route.fullPath.includes("new_account=true") && route.path !== "/auth/welcome") {
|
if (route.fullPath.includes('new_account=true') && route.path !== '/auth/welcome') {
|
||||||
const redirect = route.path.startsWith("/auth/") ? null : route.fullPath;
|
const redirect = route.path.startsWith('/auth/') ? null : route.fullPath
|
||||||
|
|
||||||
await navigateTo(
|
await navigateTo(
|
||||||
`/auth/welcome?authToken=${route.query.code}${
|
`/auth/welcome?authToken=${route.query.code}${
|
||||||
redirect ? `&redirect=${encodeURIComponent(redirect)}` : ""
|
redirect ? `&redirect=${encodeURIComponent(redirect)}` : ''
|
||||||
}`,
|
}`,
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authCookie.value) {
|
if (authCookie.value) {
|
||||||
auth.token = authCookie.value;
|
auth.token = authCookie.value
|
||||||
|
|
||||||
if (!auth.token || !auth.token.startsWith("mra_")) {
|
if (!auth.token || !auth.token.startsWith('mra_')) {
|
||||||
return auth;
|
return auth
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
auth.user = await useBaseFetch(
|
auth.user = await useBaseFetch(
|
||||||
"user",
|
'user',
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: auth.token,
|
Authorization: auth.token,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
);
|
)
|
||||||
} catch {
|
} catch {
|
||||||
/* empty */
|
/* empty */
|
||||||
}
|
}
|
||||||
@ -74,67 +74,67 @@ export const initAuth = async (oldToken = null) => {
|
|||||||
if (!auth.user && auth.token) {
|
if (!auth.user && auth.token) {
|
||||||
try {
|
try {
|
||||||
const session = await useBaseFetch(
|
const session = await useBaseFetch(
|
||||||
"session/refresh",
|
'session/refresh',
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: auth.token,
|
Authorization: auth.token,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
);
|
)
|
||||||
|
|
||||||
auth.token = session.session;
|
auth.token = session.session
|
||||||
authCookie.value = auth.token;
|
authCookie.value = auth.token
|
||||||
|
|
||||||
auth.user = await useBaseFetch(
|
auth.user = await useBaseFetch(
|
||||||
"user",
|
'user',
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: auth.token,
|
Authorization: auth.token,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
);
|
)
|
||||||
} catch {
|
} catch {
|
||||||
authCookie.value = null;
|
authCookie.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return auth;
|
return auth
|
||||||
};
|
}
|
||||||
|
|
||||||
export const getAuthUrl = (provider, redirect = "/dashboard") => {
|
export const getAuthUrl = (provider, redirect = '/dashboard') => {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig()
|
||||||
const route = useNativeRoute();
|
const route = useNativeRoute()
|
||||||
|
|
||||||
const fullURL = route.query.launcher
|
const fullURL = route.query.launcher
|
||||||
? "https://launcher-files.modrinth.com"
|
? 'https://launcher-files.modrinth.com'
|
||||||
: `${config.public.siteUrl}/auth/sign-in?redirect=${redirect}`;
|
: `${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) => {
|
export const removeAuthProvider = async (provider) => {
|
||||||
startLoading();
|
startLoading()
|
||||||
try {
|
try {
|
||||||
const auth = await useAuth();
|
const auth = await useAuth()
|
||||||
|
|
||||||
await useBaseFetch("auth/provider", {
|
await useBaseFetch('auth/provider', {
|
||||||
method: "DELETE",
|
method: 'DELETE',
|
||||||
body: {
|
body: {
|
||||||
provider,
|
provider,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
await useAuth(auth.value.token);
|
await useAuth(auth.value.token)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const data = useNuxtApp();
|
const data = useNuxtApp()
|
||||||
data.$notify({
|
data.$notify({
|
||||||
group: "main",
|
group: 'main',
|
||||||
title: "An error occurred",
|
title: 'An error occurred',
|
||||||
text: err.data.description,
|
text: err.data.description,
|
||||||
type: "error",
|
type: 'error',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
stopLoading();
|
stopLoading()
|
||||||
};
|
}
|
||||||
|
|||||||
@ -1,576 +1,576 @@
|
|||||||
export const scopeMessages = defineMessages({
|
export const scopeMessages = defineMessages({
|
||||||
userReadEmailLabel: {
|
userReadEmailLabel: {
|
||||||
id: "scopes.userReadEmail.label",
|
id: 'scopes.userReadEmail.label',
|
||||||
defaultMessage: "Read user email",
|
defaultMessage: 'Read user email',
|
||||||
},
|
},
|
||||||
userReadEmailDescription: {
|
userReadEmailDescription: {
|
||||||
id: "scopes.userReadEmail.description",
|
id: 'scopes.userReadEmail.description',
|
||||||
defaultMessage: "Read your email",
|
defaultMessage: 'Read your email',
|
||||||
},
|
},
|
||||||
userReadLabel: {
|
userReadLabel: {
|
||||||
id: "scopes.userRead.label",
|
id: 'scopes.userRead.label',
|
||||||
defaultMessage: "Read user data",
|
defaultMessage: 'Read user data',
|
||||||
},
|
},
|
||||||
userReadDescription: {
|
userReadDescription: {
|
||||||
id: "scopes.userRead.description",
|
id: 'scopes.userRead.description',
|
||||||
defaultMessage: "Access your public profile information",
|
defaultMessage: 'Access your public profile information',
|
||||||
},
|
},
|
||||||
userWriteLabel: {
|
userWriteLabel: {
|
||||||
id: "scopes.userWrite.label",
|
id: 'scopes.userWrite.label',
|
||||||
defaultMessage: "Write user data",
|
defaultMessage: 'Write user data',
|
||||||
},
|
},
|
||||||
userWriteDescription: {
|
userWriteDescription: {
|
||||||
id: "scopes.userWrite.description",
|
id: 'scopes.userWrite.description',
|
||||||
defaultMessage: "Write to your profile",
|
defaultMessage: 'Write to your profile',
|
||||||
},
|
},
|
||||||
userDeleteLabel: {
|
userDeleteLabel: {
|
||||||
id: "scopes.userDelete.label",
|
id: 'scopes.userDelete.label',
|
||||||
defaultMessage: "Delete your account",
|
defaultMessage: 'Delete your account',
|
||||||
},
|
},
|
||||||
userDeleteDescription: {
|
userDeleteDescription: {
|
||||||
id: "scopes.userDelete.description",
|
id: 'scopes.userDelete.description',
|
||||||
defaultMessage: "Delete your account",
|
defaultMessage: 'Delete your account',
|
||||||
},
|
},
|
||||||
userAuthWriteLabel: {
|
userAuthWriteLabel: {
|
||||||
id: "scopes.userAuthWrite.label",
|
id: 'scopes.userAuthWrite.label',
|
||||||
defaultMessage: "Write auth data",
|
defaultMessage: 'Write auth data',
|
||||||
},
|
},
|
||||||
userAuthWriteDescription: {
|
userAuthWriteDescription: {
|
||||||
id: "scopes.userAuthWrite.description",
|
id: 'scopes.userAuthWrite.description',
|
||||||
defaultMessage: "Modify your authentication data",
|
defaultMessage: 'Modify your authentication data',
|
||||||
},
|
},
|
||||||
notificationReadLabel: {
|
notificationReadLabel: {
|
||||||
id: "scopes.notificationRead.label",
|
id: 'scopes.notificationRead.label',
|
||||||
defaultMessage: "Read notifications",
|
defaultMessage: 'Read notifications',
|
||||||
},
|
},
|
||||||
notificationReadDescription: {
|
notificationReadDescription: {
|
||||||
id: "scopes.notificationRead.description",
|
id: 'scopes.notificationRead.description',
|
||||||
defaultMessage: "Read your notifications",
|
defaultMessage: 'Read your notifications',
|
||||||
},
|
},
|
||||||
notificationWriteLabel: {
|
notificationWriteLabel: {
|
||||||
id: "scopes.notificationWrite.label",
|
id: 'scopes.notificationWrite.label',
|
||||||
defaultMessage: "Write notifications",
|
defaultMessage: 'Write notifications',
|
||||||
},
|
},
|
||||||
notificationWriteDescription: {
|
notificationWriteDescription: {
|
||||||
id: "scopes.notificationWrite.description",
|
id: 'scopes.notificationWrite.description',
|
||||||
defaultMessage: "Delete/View your notifications",
|
defaultMessage: 'Delete/View your notifications',
|
||||||
},
|
},
|
||||||
payoutsReadLabel: {
|
payoutsReadLabel: {
|
||||||
id: "scopes.payoutsRead.label",
|
id: 'scopes.payoutsRead.label',
|
||||||
defaultMessage: "Read payouts",
|
defaultMessage: 'Read payouts',
|
||||||
},
|
},
|
||||||
payoutsReadDescription: {
|
payoutsReadDescription: {
|
||||||
id: "scopes.payoutsRead.description",
|
id: 'scopes.payoutsRead.description',
|
||||||
defaultMessage: "Read your payouts data",
|
defaultMessage: 'Read your payouts data',
|
||||||
},
|
},
|
||||||
payoutsWriteLabel: {
|
payoutsWriteLabel: {
|
||||||
id: "scopes.payoutsWrite.label",
|
id: 'scopes.payoutsWrite.label',
|
||||||
defaultMessage: "Write payouts",
|
defaultMessage: 'Write payouts',
|
||||||
},
|
},
|
||||||
payoutsWriteDescription: {
|
payoutsWriteDescription: {
|
||||||
id: "scopes.payoutsWrite.description",
|
id: 'scopes.payoutsWrite.description',
|
||||||
defaultMessage: "Withdraw money",
|
defaultMessage: 'Withdraw money',
|
||||||
},
|
},
|
||||||
analyticsLabel: {
|
analyticsLabel: {
|
||||||
id: "scopes.analytics.label",
|
id: 'scopes.analytics.label',
|
||||||
defaultMessage: "Read analytics",
|
defaultMessage: 'Read analytics',
|
||||||
},
|
},
|
||||||
analyticsDescription: {
|
analyticsDescription: {
|
||||||
id: "scopes.analytics.description",
|
id: 'scopes.analytics.description',
|
||||||
defaultMessage: "Access your analytics data",
|
defaultMessage: 'Access your analytics data',
|
||||||
},
|
},
|
||||||
projectCreateLabel: {
|
projectCreateLabel: {
|
||||||
id: "scopes.projectCreate.label",
|
id: 'scopes.projectCreate.label',
|
||||||
defaultMessage: "Create projects",
|
defaultMessage: 'Create projects',
|
||||||
},
|
},
|
||||||
projectCreateDescription: {
|
projectCreateDescription: {
|
||||||
id: "scopes.projectCreate.description",
|
id: 'scopes.projectCreate.description',
|
||||||
defaultMessage: "Create new projects",
|
defaultMessage: 'Create new projects',
|
||||||
},
|
},
|
||||||
projectReadLabel: {
|
projectReadLabel: {
|
||||||
id: "scopes.projectRead.label",
|
id: 'scopes.projectRead.label',
|
||||||
defaultMessage: "Read projects",
|
defaultMessage: 'Read projects',
|
||||||
},
|
},
|
||||||
projectReadDescription: {
|
projectReadDescription: {
|
||||||
id: "scopes.projectRead.description",
|
id: 'scopes.projectRead.description',
|
||||||
defaultMessage: "Read all your projects",
|
defaultMessage: 'Read all your projects',
|
||||||
},
|
},
|
||||||
projectWriteLabel: {
|
projectWriteLabel: {
|
||||||
id: "scopes.projectWrite.label",
|
id: 'scopes.projectWrite.label',
|
||||||
defaultMessage: "Write projects",
|
defaultMessage: 'Write projects',
|
||||||
},
|
},
|
||||||
projectWriteDescription: {
|
projectWriteDescription: {
|
||||||
id: "scopes.projectWrite.description",
|
id: 'scopes.projectWrite.description',
|
||||||
defaultMessage: "Write to project data",
|
defaultMessage: 'Write to project data',
|
||||||
},
|
},
|
||||||
projectDeleteLabel: {
|
projectDeleteLabel: {
|
||||||
id: "scopes.projectDelete.label",
|
id: 'scopes.projectDelete.label',
|
||||||
defaultMessage: "Delete projects",
|
defaultMessage: 'Delete projects',
|
||||||
},
|
},
|
||||||
projectDeleteDescription: {
|
projectDeleteDescription: {
|
||||||
id: "scopes.projectDelete.description",
|
id: 'scopes.projectDelete.description',
|
||||||
defaultMessage: "Delete your projects",
|
defaultMessage: 'Delete your projects',
|
||||||
},
|
},
|
||||||
versionCreateLabel: {
|
versionCreateLabel: {
|
||||||
id: "scopes.versionCreate.label",
|
id: 'scopes.versionCreate.label',
|
||||||
defaultMessage: "Create versions",
|
defaultMessage: 'Create versions',
|
||||||
},
|
},
|
||||||
versionCreateDescription: {
|
versionCreateDescription: {
|
||||||
id: "scopes.versionCreate.description",
|
id: 'scopes.versionCreate.description',
|
||||||
defaultMessage: "Create new versions",
|
defaultMessage: 'Create new versions',
|
||||||
},
|
},
|
||||||
versionReadLabel: {
|
versionReadLabel: {
|
||||||
id: "scopes.versionRead.label",
|
id: 'scopes.versionRead.label',
|
||||||
defaultMessage: "Read versions",
|
defaultMessage: 'Read versions',
|
||||||
},
|
},
|
||||||
versionReadDescription: {
|
versionReadDescription: {
|
||||||
id: "scopes.versionRead.description",
|
id: 'scopes.versionRead.description',
|
||||||
defaultMessage: "Read all versions",
|
defaultMessage: 'Read all versions',
|
||||||
},
|
},
|
||||||
versionWriteLabel: {
|
versionWriteLabel: {
|
||||||
id: "scopes.versionWrite.label",
|
id: 'scopes.versionWrite.label',
|
||||||
defaultMessage: "Write versions",
|
defaultMessage: 'Write versions',
|
||||||
},
|
},
|
||||||
versionWriteDescription: {
|
versionWriteDescription: {
|
||||||
id: "scopes.versionWrite.description",
|
id: 'scopes.versionWrite.description',
|
||||||
defaultMessage: "Write to version data",
|
defaultMessage: 'Write to version data',
|
||||||
},
|
},
|
||||||
versionDeleteLabel: {
|
versionDeleteLabel: {
|
||||||
id: "scopes.versionDelete.label",
|
id: 'scopes.versionDelete.label',
|
||||||
defaultMessage: "Delete versions",
|
defaultMessage: 'Delete versions',
|
||||||
},
|
},
|
||||||
versionDeleteDescription: {
|
versionDeleteDescription: {
|
||||||
id: "scopes.versionDelete.description",
|
id: 'scopes.versionDelete.description',
|
||||||
defaultMessage: "Delete a version",
|
defaultMessage: 'Delete a version',
|
||||||
},
|
},
|
||||||
reportCreateLabel: {
|
reportCreateLabel: {
|
||||||
id: "scopes.reportCreate.label",
|
id: 'scopes.reportCreate.label',
|
||||||
defaultMessage: "Create reports",
|
defaultMessage: 'Create reports',
|
||||||
},
|
},
|
||||||
reportCreateDescription: {
|
reportCreateDescription: {
|
||||||
id: "scopes.reportCreate.description",
|
id: 'scopes.reportCreate.description',
|
||||||
defaultMessage: "Create reports",
|
defaultMessage: 'Create reports',
|
||||||
},
|
},
|
||||||
reportReadLabel: {
|
reportReadLabel: {
|
||||||
id: "scopes.reportRead.label",
|
id: 'scopes.reportRead.label',
|
||||||
defaultMessage: "Read reports",
|
defaultMessage: 'Read reports',
|
||||||
},
|
},
|
||||||
reportReadDescription: {
|
reportReadDescription: {
|
||||||
id: "scopes.reportRead.description",
|
id: 'scopes.reportRead.description',
|
||||||
defaultMessage: "Read reports",
|
defaultMessage: 'Read reports',
|
||||||
},
|
},
|
||||||
reportWriteLabel: {
|
reportWriteLabel: {
|
||||||
id: "scopes.reportWrite.label",
|
id: 'scopes.reportWrite.label',
|
||||||
defaultMessage: "Write reports",
|
defaultMessage: 'Write reports',
|
||||||
},
|
},
|
||||||
reportWriteDescription: {
|
reportWriteDescription: {
|
||||||
id: "scopes.reportWrite.description",
|
id: 'scopes.reportWrite.description',
|
||||||
defaultMessage: "Edit reports",
|
defaultMessage: 'Edit reports',
|
||||||
},
|
},
|
||||||
reportDeleteLabel: {
|
reportDeleteLabel: {
|
||||||
id: "scopes.reportDelete.label",
|
id: 'scopes.reportDelete.label',
|
||||||
defaultMessage: "Delete reports",
|
defaultMessage: 'Delete reports',
|
||||||
},
|
},
|
||||||
reportDeleteDescription: {
|
reportDeleteDescription: {
|
||||||
id: "scopes.reportDelete.description",
|
id: 'scopes.reportDelete.description',
|
||||||
defaultMessage: "Delete reports",
|
defaultMessage: 'Delete reports',
|
||||||
},
|
},
|
||||||
threadReadLabel: {
|
threadReadLabel: {
|
||||||
id: "scopes.threadRead.label",
|
id: 'scopes.threadRead.label',
|
||||||
defaultMessage: "Read threads",
|
defaultMessage: 'Read threads',
|
||||||
},
|
},
|
||||||
threadReadDescription: {
|
threadReadDescription: {
|
||||||
id: "scopes.threadRead.description",
|
id: 'scopes.threadRead.description',
|
||||||
defaultMessage: "Read threads",
|
defaultMessage: 'Read threads',
|
||||||
},
|
},
|
||||||
threadWriteLabel: {
|
threadWriteLabel: {
|
||||||
id: "scopes.threadWrite.label",
|
id: 'scopes.threadWrite.label',
|
||||||
defaultMessage: "Write threads",
|
defaultMessage: 'Write threads',
|
||||||
},
|
},
|
||||||
threadWriteDescription: {
|
threadWriteDescription: {
|
||||||
id: "scopes.threadWrite.description",
|
id: 'scopes.threadWrite.description',
|
||||||
defaultMessage: "Write to threads",
|
defaultMessage: 'Write to threads',
|
||||||
},
|
},
|
||||||
patCreateLabel: {
|
patCreateLabel: {
|
||||||
id: "scopes.patCreate.label",
|
id: 'scopes.patCreate.label',
|
||||||
defaultMessage: "Create PATs",
|
defaultMessage: 'Create PATs',
|
||||||
},
|
},
|
||||||
patCreateDescription: {
|
patCreateDescription: {
|
||||||
id: "scopes.patCreate.description",
|
id: 'scopes.patCreate.description',
|
||||||
defaultMessage: "Create personal API tokens",
|
defaultMessage: 'Create personal API tokens',
|
||||||
},
|
},
|
||||||
patReadLabel: {
|
patReadLabel: {
|
||||||
id: "scopes.patRead.label",
|
id: 'scopes.patRead.label',
|
||||||
defaultMessage: "Read PATs",
|
defaultMessage: 'Read PATs',
|
||||||
},
|
},
|
||||||
patReadDescription: {
|
patReadDescription: {
|
||||||
id: "scopes.patRead.description",
|
id: 'scopes.patRead.description',
|
||||||
defaultMessage: "View created API tokens",
|
defaultMessage: 'View created API tokens',
|
||||||
},
|
},
|
||||||
patWriteLabel: {
|
patWriteLabel: {
|
||||||
id: "scopes.patWrite.label",
|
id: 'scopes.patWrite.label',
|
||||||
defaultMessage: "Write PATs",
|
defaultMessage: 'Write PATs',
|
||||||
},
|
},
|
||||||
patWriteDescription: {
|
patWriteDescription: {
|
||||||
id: "scopes.patWrite.description",
|
id: 'scopes.patWrite.description',
|
||||||
defaultMessage: "Edit personal API tokens",
|
defaultMessage: 'Edit personal API tokens',
|
||||||
},
|
},
|
||||||
patDeleteLabel: {
|
patDeleteLabel: {
|
||||||
id: "scopes.patDelete.label",
|
id: 'scopes.patDelete.label',
|
||||||
defaultMessage: "Delete PATs",
|
defaultMessage: 'Delete PATs',
|
||||||
},
|
},
|
||||||
patDeleteDescription: {
|
patDeleteDescription: {
|
||||||
id: "scopes.patDelete.description",
|
id: 'scopes.patDelete.description',
|
||||||
defaultMessage: "Delete your personal API tokens",
|
defaultMessage: 'Delete your personal API tokens',
|
||||||
},
|
},
|
||||||
sessionReadLabel: {
|
sessionReadLabel: {
|
||||||
id: "scopes.sessionRead.label",
|
id: 'scopes.sessionRead.label',
|
||||||
defaultMessage: "Read sessions",
|
defaultMessage: 'Read sessions',
|
||||||
},
|
},
|
||||||
sessionReadDescription: {
|
sessionReadDescription: {
|
||||||
id: "scopes.sessionRead.description",
|
id: 'scopes.sessionRead.description',
|
||||||
defaultMessage: "Read active sessions",
|
defaultMessage: 'Read active sessions',
|
||||||
},
|
},
|
||||||
sessionDeleteLabel: {
|
sessionDeleteLabel: {
|
||||||
id: "scopes.sessionDelete.label",
|
id: 'scopes.sessionDelete.label',
|
||||||
defaultMessage: "Delete sessions",
|
defaultMessage: 'Delete sessions',
|
||||||
},
|
},
|
||||||
sessionDeleteDescription: {
|
sessionDeleteDescription: {
|
||||||
id: "scopes.sessionDelete.description",
|
id: 'scopes.sessionDelete.description',
|
||||||
defaultMessage: "Delete sessions",
|
defaultMessage: 'Delete sessions',
|
||||||
},
|
},
|
||||||
performAnalyticsLabel: {
|
performAnalyticsLabel: {
|
||||||
id: "scopes.performAnalytics.label",
|
id: 'scopes.performAnalytics.label',
|
||||||
defaultMessage: "Perform analytics",
|
defaultMessage: 'Perform analytics',
|
||||||
},
|
},
|
||||||
performAnalyticsDescription: {
|
performAnalyticsDescription: {
|
||||||
id: "scopes.performAnalytics.description",
|
id: 'scopes.performAnalytics.description',
|
||||||
defaultMessage: "Perform analytics actions",
|
defaultMessage: 'Perform analytics actions',
|
||||||
},
|
},
|
||||||
collectionCreateLabel: {
|
collectionCreateLabel: {
|
||||||
id: "scopes.collectionCreate.label",
|
id: 'scopes.collectionCreate.label',
|
||||||
defaultMessage: "Create collections",
|
defaultMessage: 'Create collections',
|
||||||
},
|
},
|
||||||
collectionCreateDescription: {
|
collectionCreateDescription: {
|
||||||
id: "scopes.collectionCreate.description",
|
id: 'scopes.collectionCreate.description',
|
||||||
defaultMessage: "Create collections",
|
defaultMessage: 'Create collections',
|
||||||
},
|
},
|
||||||
collectionReadLabel: {
|
collectionReadLabel: {
|
||||||
id: "scopes.collectionRead.label",
|
id: 'scopes.collectionRead.label',
|
||||||
defaultMessage: "Read collections",
|
defaultMessage: 'Read collections',
|
||||||
},
|
},
|
||||||
collectionReadDescription: {
|
collectionReadDescription: {
|
||||||
id: "scopes.collectionRead.description",
|
id: 'scopes.collectionRead.description',
|
||||||
defaultMessage: "Read collections",
|
defaultMessage: 'Read collections',
|
||||||
},
|
},
|
||||||
collectionWriteLabel: {
|
collectionWriteLabel: {
|
||||||
id: "scopes.collectionWrite.label",
|
id: 'scopes.collectionWrite.label',
|
||||||
defaultMessage: "Write collections",
|
defaultMessage: 'Write collections',
|
||||||
},
|
},
|
||||||
collectionWriteDescription: {
|
collectionWriteDescription: {
|
||||||
id: "scopes.collectionWrite.description",
|
id: 'scopes.collectionWrite.description',
|
||||||
defaultMessage: "Write to collections",
|
defaultMessage: 'Write to collections',
|
||||||
},
|
},
|
||||||
collectionDeleteLabel: {
|
collectionDeleteLabel: {
|
||||||
id: "scopes.collectionDelete.label",
|
id: 'scopes.collectionDelete.label',
|
||||||
defaultMessage: "Delete collections",
|
defaultMessage: 'Delete collections',
|
||||||
},
|
},
|
||||||
collectionDeleteDescription: {
|
collectionDeleteDescription: {
|
||||||
id: "scopes.collectionDelete.description",
|
id: 'scopes.collectionDelete.description',
|
||||||
defaultMessage: "Delete collections",
|
defaultMessage: 'Delete collections',
|
||||||
},
|
},
|
||||||
organizationCreateLabel: {
|
organizationCreateLabel: {
|
||||||
id: "scopes.organizationCreate.label",
|
id: 'scopes.organizationCreate.label',
|
||||||
defaultMessage: "Create organizations",
|
defaultMessage: 'Create organizations',
|
||||||
},
|
},
|
||||||
organizationCreateDescription: {
|
organizationCreateDescription: {
|
||||||
id: "scopes.organizationCreate.description",
|
id: 'scopes.organizationCreate.description',
|
||||||
defaultMessage: "Create organizations",
|
defaultMessage: 'Create organizations',
|
||||||
},
|
},
|
||||||
organizationReadLabel: {
|
organizationReadLabel: {
|
||||||
id: "scopes.organizationRead.label",
|
id: 'scopes.organizationRead.label',
|
||||||
defaultMessage: "Read organizations",
|
defaultMessage: 'Read organizations',
|
||||||
},
|
},
|
||||||
organizationReadDescription: {
|
organizationReadDescription: {
|
||||||
id: "scopes.organizationRead.description",
|
id: 'scopes.organizationRead.description',
|
||||||
defaultMessage: "Read organizations",
|
defaultMessage: 'Read organizations',
|
||||||
},
|
},
|
||||||
organizationWriteLabel: {
|
organizationWriteLabel: {
|
||||||
id: "scopes.organizationWrite.label",
|
id: 'scopes.organizationWrite.label',
|
||||||
defaultMessage: "Write organizations",
|
defaultMessage: 'Write organizations',
|
||||||
},
|
},
|
||||||
organizationWriteDescription: {
|
organizationWriteDescription: {
|
||||||
id: "scopes.organizationWrite.description",
|
id: 'scopes.organizationWrite.description',
|
||||||
defaultMessage: "Write to organizations",
|
defaultMessage: 'Write to organizations',
|
||||||
},
|
},
|
||||||
organizationDeleteLabel: {
|
organizationDeleteLabel: {
|
||||||
id: "scopes.organizationDelete.label",
|
id: 'scopes.organizationDelete.label',
|
||||||
defaultMessage: "Delete organizations",
|
defaultMessage: 'Delete organizations',
|
||||||
},
|
},
|
||||||
organizationDeleteDescription: {
|
organizationDeleteDescription: {
|
||||||
id: "scopes.organizationDelete.description",
|
id: 'scopes.organizationDelete.description',
|
||||||
defaultMessage: "Delete organizations",
|
defaultMessage: 'Delete organizations',
|
||||||
},
|
},
|
||||||
sessionAccessLabel: {
|
sessionAccessLabel: {
|
||||||
id: "scopes.sessionAccess.label",
|
id: 'scopes.sessionAccess.label',
|
||||||
defaultMessage: "Access sessions",
|
defaultMessage: 'Access sessions',
|
||||||
},
|
},
|
||||||
sessionAccessDescription: {
|
sessionAccessDescription: {
|
||||||
id: "scopes.sessionAccess.description",
|
id: 'scopes.sessionAccess.description',
|
||||||
defaultMessage: "Access modrinth-issued sessions",
|
defaultMessage: 'Access modrinth-issued sessions',
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const scopeDefinitions = [
|
const scopeDefinitions = [
|
||||||
{
|
{
|
||||||
id: "USER_READ_EMAIL",
|
id: 'USER_READ_EMAIL',
|
||||||
value: BigInt(1) << BigInt(0),
|
value: BigInt(1) << BigInt(0),
|
||||||
label: scopeMessages.userReadEmailLabel,
|
label: scopeMessages.userReadEmailLabel,
|
||||||
desc: scopeMessages.userReadEmailDescription,
|
desc: scopeMessages.userReadEmailDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "USER_READ",
|
id: 'USER_READ',
|
||||||
value: BigInt(1) << BigInt(1),
|
value: BigInt(1) << BigInt(1),
|
||||||
label: scopeMessages.userReadLabel,
|
label: scopeMessages.userReadLabel,
|
||||||
desc: scopeMessages.userReadDescription,
|
desc: scopeMessages.userReadDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "USER_WRITE",
|
id: 'USER_WRITE',
|
||||||
value: BigInt(1) << BigInt(2),
|
value: BigInt(1) << BigInt(2),
|
||||||
label: scopeMessages.userWriteLabel,
|
label: scopeMessages.userWriteLabel,
|
||||||
desc: scopeMessages.userWriteDescription,
|
desc: scopeMessages.userWriteDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "USER_DELETE",
|
id: 'USER_DELETE',
|
||||||
value: BigInt(1) << BigInt(3),
|
value: BigInt(1) << BigInt(3),
|
||||||
label: scopeMessages.userDeleteLabel,
|
label: scopeMessages.userDeleteLabel,
|
||||||
desc: scopeMessages.userDeleteDescription,
|
desc: scopeMessages.userDeleteDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "USER_AUTH_WRITE",
|
id: 'USER_AUTH_WRITE',
|
||||||
value: BigInt(1) << BigInt(4),
|
value: BigInt(1) << BigInt(4),
|
||||||
label: scopeMessages.userAuthWriteLabel,
|
label: scopeMessages.userAuthWriteLabel,
|
||||||
desc: scopeMessages.userAuthWriteDescription,
|
desc: scopeMessages.userAuthWriteDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "NOTIFICATION_READ",
|
id: 'NOTIFICATION_READ',
|
||||||
value: BigInt(1) << BigInt(5),
|
value: BigInt(1) << BigInt(5),
|
||||||
label: scopeMessages.notificationReadLabel,
|
label: scopeMessages.notificationReadLabel,
|
||||||
desc: scopeMessages.notificationReadDescription,
|
desc: scopeMessages.notificationReadDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "NOTIFICATION_WRITE",
|
id: 'NOTIFICATION_WRITE',
|
||||||
value: BigInt(1) << BigInt(6),
|
value: BigInt(1) << BigInt(6),
|
||||||
label: scopeMessages.notificationWriteLabel,
|
label: scopeMessages.notificationWriteLabel,
|
||||||
desc: scopeMessages.notificationWriteDescription,
|
desc: scopeMessages.notificationWriteDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "PAYOUTS_READ",
|
id: 'PAYOUTS_READ',
|
||||||
value: BigInt(1) << BigInt(7),
|
value: BigInt(1) << BigInt(7),
|
||||||
label: scopeMessages.payoutsReadLabel,
|
label: scopeMessages.payoutsReadLabel,
|
||||||
desc: scopeMessages.payoutsReadDescription,
|
desc: scopeMessages.payoutsReadDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "PAYOUTS_WRITE",
|
id: 'PAYOUTS_WRITE',
|
||||||
value: BigInt(1) << BigInt(8),
|
value: BigInt(1) << BigInt(8),
|
||||||
label: scopeMessages.payoutsWriteLabel,
|
label: scopeMessages.payoutsWriteLabel,
|
||||||
desc: scopeMessages.payoutsWriteDescription,
|
desc: scopeMessages.payoutsWriteDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "ANALYTICS",
|
id: 'ANALYTICS',
|
||||||
value: BigInt(1) << BigInt(9),
|
value: BigInt(1) << BigInt(9),
|
||||||
label: scopeMessages.analyticsLabel,
|
label: scopeMessages.analyticsLabel,
|
||||||
desc: scopeMessages.analyticsDescription,
|
desc: scopeMessages.analyticsDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "PROJECT_CREATE",
|
id: 'PROJECT_CREATE',
|
||||||
value: BigInt(1) << BigInt(10),
|
value: BigInt(1) << BigInt(10),
|
||||||
label: scopeMessages.projectCreateLabel,
|
label: scopeMessages.projectCreateLabel,
|
||||||
desc: scopeMessages.projectCreateDescription,
|
desc: scopeMessages.projectCreateDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "PROJECT_READ",
|
id: 'PROJECT_READ',
|
||||||
value: BigInt(1) << BigInt(11),
|
value: BigInt(1) << BigInt(11),
|
||||||
label: scopeMessages.projectReadLabel,
|
label: scopeMessages.projectReadLabel,
|
||||||
desc: scopeMessages.projectReadDescription,
|
desc: scopeMessages.projectReadDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "PROJECT_WRITE",
|
id: 'PROJECT_WRITE',
|
||||||
value: BigInt(1) << BigInt(12),
|
value: BigInt(1) << BigInt(12),
|
||||||
label: scopeMessages.projectWriteLabel,
|
label: scopeMessages.projectWriteLabel,
|
||||||
desc: scopeMessages.projectWriteDescription,
|
desc: scopeMessages.projectWriteDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "PROJECT_DELETE",
|
id: 'PROJECT_DELETE',
|
||||||
value: BigInt(1) << BigInt(13),
|
value: BigInt(1) << BigInt(13),
|
||||||
label: scopeMessages.projectDeleteLabel,
|
label: scopeMessages.projectDeleteLabel,
|
||||||
desc: scopeMessages.projectDeleteDescription,
|
desc: scopeMessages.projectDeleteDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "VERSION_CREATE",
|
id: 'VERSION_CREATE',
|
||||||
value: BigInt(1) << BigInt(14),
|
value: BigInt(1) << BigInt(14),
|
||||||
label: scopeMessages.versionCreateLabel,
|
label: scopeMessages.versionCreateLabel,
|
||||||
desc: scopeMessages.versionCreateDescription,
|
desc: scopeMessages.versionCreateDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "VERSION_READ",
|
id: 'VERSION_READ',
|
||||||
value: BigInt(1) << BigInt(15),
|
value: BigInt(1) << BigInt(15),
|
||||||
label: scopeMessages.versionReadLabel,
|
label: scopeMessages.versionReadLabel,
|
||||||
desc: scopeMessages.versionReadDescription,
|
desc: scopeMessages.versionReadDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "VERSION_WRITE",
|
id: 'VERSION_WRITE',
|
||||||
value: BigInt(1) << BigInt(16),
|
value: BigInt(1) << BigInt(16),
|
||||||
label: scopeMessages.versionWriteLabel,
|
label: scopeMessages.versionWriteLabel,
|
||||||
desc: scopeMessages.versionWriteDescription,
|
desc: scopeMessages.versionWriteDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "VERSION_DELETE",
|
id: 'VERSION_DELETE',
|
||||||
value: BigInt(1) << BigInt(17),
|
value: BigInt(1) << BigInt(17),
|
||||||
label: scopeMessages.versionDeleteLabel,
|
label: scopeMessages.versionDeleteLabel,
|
||||||
desc: scopeMessages.versionDeleteDescription,
|
desc: scopeMessages.versionDeleteDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "REPORT_CREATE",
|
id: 'REPORT_CREATE',
|
||||||
value: BigInt(1) << BigInt(18),
|
value: BigInt(1) << BigInt(18),
|
||||||
label: scopeMessages.reportCreateLabel,
|
label: scopeMessages.reportCreateLabel,
|
||||||
desc: scopeMessages.reportCreateDescription,
|
desc: scopeMessages.reportCreateDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "REPORT_READ",
|
id: 'REPORT_READ',
|
||||||
value: BigInt(1) << BigInt(19),
|
value: BigInt(1) << BigInt(19),
|
||||||
label: scopeMessages.reportReadLabel,
|
label: scopeMessages.reportReadLabel,
|
||||||
desc: scopeMessages.reportReadDescription,
|
desc: scopeMessages.reportReadDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "REPORT_WRITE",
|
id: 'REPORT_WRITE',
|
||||||
value: BigInt(1) << BigInt(20),
|
value: BigInt(1) << BigInt(20),
|
||||||
label: scopeMessages.reportWriteLabel,
|
label: scopeMessages.reportWriteLabel,
|
||||||
desc: scopeMessages.reportWriteDescription,
|
desc: scopeMessages.reportWriteDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "REPORT_DELETE",
|
id: 'REPORT_DELETE',
|
||||||
value: BigInt(1) << BigInt(21),
|
value: BigInt(1) << BigInt(21),
|
||||||
label: scopeMessages.reportDeleteLabel,
|
label: scopeMessages.reportDeleteLabel,
|
||||||
desc: scopeMessages.reportDeleteDescription,
|
desc: scopeMessages.reportDeleteDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "THREAD_READ",
|
id: 'THREAD_READ',
|
||||||
value: BigInt(1) << BigInt(22),
|
value: BigInt(1) << BigInt(22),
|
||||||
label: scopeMessages.threadReadLabel,
|
label: scopeMessages.threadReadLabel,
|
||||||
desc: scopeMessages.threadReadDescription,
|
desc: scopeMessages.threadReadDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "THREAD_WRITE",
|
id: 'THREAD_WRITE',
|
||||||
value: BigInt(1) << BigInt(23),
|
value: BigInt(1) << BigInt(23),
|
||||||
label: scopeMessages.threadWriteLabel,
|
label: scopeMessages.threadWriteLabel,
|
||||||
desc: scopeMessages.threadWriteDescription,
|
desc: scopeMessages.threadWriteDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "PAT_CREATE",
|
id: 'PAT_CREATE',
|
||||||
value: BigInt(1) << BigInt(24),
|
value: BigInt(1) << BigInt(24),
|
||||||
label: scopeMessages.patCreateLabel,
|
label: scopeMessages.patCreateLabel,
|
||||||
desc: scopeMessages.patCreateDescription,
|
desc: scopeMessages.patCreateDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "PAT_READ",
|
id: 'PAT_READ',
|
||||||
value: BigInt(1) << BigInt(25),
|
value: BigInt(1) << BigInt(25),
|
||||||
label: scopeMessages.patReadLabel,
|
label: scopeMessages.patReadLabel,
|
||||||
desc: scopeMessages.patReadDescription,
|
desc: scopeMessages.patReadDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "PAT_WRITE",
|
id: 'PAT_WRITE',
|
||||||
value: BigInt(1) << BigInt(26),
|
value: BigInt(1) << BigInt(26),
|
||||||
label: scopeMessages.patWriteLabel,
|
label: scopeMessages.patWriteLabel,
|
||||||
desc: scopeMessages.patWriteDescription,
|
desc: scopeMessages.patWriteDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "PAT_DELETE",
|
id: 'PAT_DELETE',
|
||||||
value: BigInt(1) << BigInt(27),
|
value: BigInt(1) << BigInt(27),
|
||||||
label: scopeMessages.patDeleteLabel,
|
label: scopeMessages.patDeleteLabel,
|
||||||
desc: scopeMessages.patDeleteDescription,
|
desc: scopeMessages.patDeleteDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "SESSION_READ",
|
id: 'SESSION_READ',
|
||||||
value: BigInt(1) << BigInt(28),
|
value: BigInt(1) << BigInt(28),
|
||||||
label: scopeMessages.sessionReadLabel,
|
label: scopeMessages.sessionReadLabel,
|
||||||
desc: scopeMessages.sessionReadDescription,
|
desc: scopeMessages.sessionReadDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "SESSION_DELETE",
|
id: 'SESSION_DELETE',
|
||||||
value: BigInt(1) << BigInt(29),
|
value: BigInt(1) << BigInt(29),
|
||||||
label: scopeMessages.sessionDeleteLabel,
|
label: scopeMessages.sessionDeleteLabel,
|
||||||
desc: scopeMessages.sessionDeleteDescription,
|
desc: scopeMessages.sessionDeleteDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "PERFORM_ANALYTICS",
|
id: 'PERFORM_ANALYTICS',
|
||||||
value: BigInt(1) << BigInt(30),
|
value: BigInt(1) << BigInt(30),
|
||||||
label: scopeMessages.performAnalyticsLabel,
|
label: scopeMessages.performAnalyticsLabel,
|
||||||
desc: scopeMessages.performAnalyticsDescription,
|
desc: scopeMessages.performAnalyticsDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "COLLECTION_CREATE",
|
id: 'COLLECTION_CREATE',
|
||||||
value: BigInt(1) << BigInt(31),
|
value: BigInt(1) << BigInt(31),
|
||||||
label: scopeMessages.collectionCreateLabel,
|
label: scopeMessages.collectionCreateLabel,
|
||||||
desc: scopeMessages.collectionCreateDescription,
|
desc: scopeMessages.collectionCreateDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "COLLECTION_READ",
|
id: 'COLLECTION_READ',
|
||||||
value: BigInt(1) << BigInt(32),
|
value: BigInt(1) << BigInt(32),
|
||||||
label: scopeMessages.collectionReadLabel,
|
label: scopeMessages.collectionReadLabel,
|
||||||
desc: scopeMessages.collectionReadDescription,
|
desc: scopeMessages.collectionReadDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "COLLECTION_WRITE",
|
id: 'COLLECTION_WRITE',
|
||||||
value: BigInt(1) << BigInt(33),
|
value: BigInt(1) << BigInt(33),
|
||||||
label: scopeMessages.collectionWriteLabel,
|
label: scopeMessages.collectionWriteLabel,
|
||||||
desc: scopeMessages.collectionWriteDescription,
|
desc: scopeMessages.collectionWriteDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "COLLECTION_DELETE",
|
id: 'COLLECTION_DELETE',
|
||||||
value: BigInt(1) << BigInt(34),
|
value: BigInt(1) << BigInt(34),
|
||||||
label: scopeMessages.collectionDeleteLabel,
|
label: scopeMessages.collectionDeleteLabel,
|
||||||
desc: scopeMessages.collectionDeleteDescription,
|
desc: scopeMessages.collectionDeleteDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "ORGANIZATION_CREATE",
|
id: 'ORGANIZATION_CREATE',
|
||||||
value: BigInt(1) << BigInt(35),
|
value: BigInt(1) << BigInt(35),
|
||||||
label: scopeMessages.organizationCreateLabel,
|
label: scopeMessages.organizationCreateLabel,
|
||||||
desc: scopeMessages.organizationCreateDescription,
|
desc: scopeMessages.organizationCreateDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "ORGANIZATION_READ",
|
id: 'ORGANIZATION_READ',
|
||||||
value: BigInt(1) << BigInt(36),
|
value: BigInt(1) << BigInt(36),
|
||||||
label: scopeMessages.organizationReadLabel,
|
label: scopeMessages.organizationReadLabel,
|
||||||
desc: scopeMessages.organizationReadDescription,
|
desc: scopeMessages.organizationReadDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "ORGANIZATION_WRITE",
|
id: 'ORGANIZATION_WRITE',
|
||||||
value: BigInt(1) << BigInt(37),
|
value: BigInt(1) << BigInt(37),
|
||||||
label: scopeMessages.organizationWriteLabel,
|
label: scopeMessages.organizationWriteLabel,
|
||||||
desc: scopeMessages.organizationWriteDescription,
|
desc: scopeMessages.organizationWriteDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "ORGANIZATION_DELETE",
|
id: 'ORGANIZATION_DELETE',
|
||||||
value: BigInt(1) << BigInt(38),
|
value: BigInt(1) << BigInt(38),
|
||||||
label: scopeMessages.organizationDeleteLabel,
|
label: scopeMessages.organizationDeleteLabel,
|
||||||
desc: scopeMessages.organizationDeleteDescription,
|
desc: scopeMessages.organizationDeleteDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "SESSION_ACCESS",
|
id: 'SESSION_ACCESS',
|
||||||
value: BigInt(1) << BigInt(39),
|
value: BigInt(1) << BigInt(39),
|
||||||
label: scopeMessages.sessionAccessLabel,
|
label: scopeMessages.sessionAccessLabel,
|
||||||
desc: scopeMessages.sessionAccessDescription,
|
desc: scopeMessages.sessionAccessDescription,
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
|
|
||||||
const Scopes = scopeDefinitions.reduce(
|
const Scopes = scopeDefinitions.reduce(
|
||||||
(acc, scope) => {
|
(acc, scope) => {
|
||||||
acc[scope.id] = scope.value;
|
acc[scope.id] = scope.value
|
||||||
return acc;
|
return acc
|
||||||
},
|
},
|
||||||
{} as Record<string, bigint>,
|
{} as Record<string, bigint>,
|
||||||
);
|
)
|
||||||
|
|
||||||
export const restrictedScopes = [
|
export const restrictedScopes = [
|
||||||
Scopes.PAT_READ,
|
Scopes.PAT_READ,
|
||||||
@ -583,18 +583,18 @@ export const restrictedScopes = [
|
|||||||
Scopes.USER_AUTH_WRITE,
|
Scopes.USER_AUTH_WRITE,
|
||||||
Scopes.USER_DELETE,
|
Scopes.USER_DELETE,
|
||||||
Scopes.PERFORM_ANALYTICS,
|
Scopes.PERFORM_ANALYTICS,
|
||||||
];
|
]
|
||||||
|
|
||||||
export const scopeList = Object.entries(Scopes)
|
export const scopeList = Object.entries(Scopes)
|
||||||
.filter(([_, value]) => !restrictedScopes.includes(value))
|
.filter(([_, value]) => !restrictedScopes.includes(value))
|
||||||
.map(([key, _]) => key);
|
.map(([key, _]) => key)
|
||||||
|
|
||||||
export const getScopeValue = (scope: string) => {
|
export const getScopeValue = (scope: string) => {
|
||||||
return Scopes[scope];
|
return Scopes[scope]
|
||||||
};
|
}
|
||||||
|
|
||||||
export const encodeScopes = (scopes: string[]) => {
|
export const encodeScopes = (scopes: string[]) => {
|
||||||
let scopeFlag = BigInt(0);
|
let scopeFlag = BigInt(0)
|
||||||
|
|
||||||
// We iterate over the provided scopes
|
// We iterate over the provided scopes
|
||||||
for (const scope of scopes) {
|
for (const scope of scopes) {
|
||||||
@ -602,77 +602,77 @@ export const encodeScopes = (scopes: string[]) => {
|
|||||||
for (const [scopeName, scopeFlagValue] of Object.entries(Scopes)) {
|
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 the scope name is the same as the provided scope, add the scope flag to the scopeFlag variable
|
||||||
if (scopeName === scope) {
|
if (scopeName === scope) {
|
||||||
scopeFlag = scopeFlag | scopeFlagValue;
|
scopeFlag = scopeFlag | scopeFlagValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return scopeFlag;
|
return scopeFlag
|
||||||
};
|
}
|
||||||
|
|
||||||
export const decodeScopes = (scopes: bigint | number) => {
|
export const decodeScopes = (scopes: bigint | number) => {
|
||||||
if (typeof scopes === "number") {
|
if (typeof scopes === 'number') {
|
||||||
scopes = BigInt(scopes);
|
scopes = BigInt(scopes)
|
||||||
}
|
}
|
||||||
|
|
||||||
const authorizedScopes = [];
|
const authorizedScopes = []
|
||||||
|
|
||||||
// We iterate over the entries of the Scopes object
|
// We iterate over the entries of the Scopes object
|
||||||
for (const [scopeName, scopeFlag] of Object.entries(Scopes)) {
|
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 the scope flag is present in the provided number, add the scope name to the list
|
||||||
if ((scopes & scopeFlag) === scopeFlag) {
|
if ((scopes & scopeFlag) === scopeFlag) {
|
||||||
authorizedScopes.push(scopeName);
|
authorizedScopes.push(scopeName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return authorizedScopes;
|
return authorizedScopes
|
||||||
};
|
}
|
||||||
|
|
||||||
export const hasScope = (scopes: bigint, scope: string) => {
|
export const hasScope = (scopes: bigint, scope: string) => {
|
||||||
const authorizedScopes = decodeScopes(scopes);
|
const authorizedScopes = decodeScopes(scopes)
|
||||||
return authorizedScopes.includes(scope);
|
return authorizedScopes.includes(scope)
|
||||||
};
|
}
|
||||||
|
|
||||||
export const toggleScope = (scopes: bigint, scope: string) => {
|
export const toggleScope = (scopes: bigint, scope: string) => {
|
||||||
const authorizedScopes = decodeScopes(scopes);
|
const authorizedScopes = decodeScopes(scopes)
|
||||||
if (authorizedScopes.includes(scope)) {
|
if (authorizedScopes.includes(scope)) {
|
||||||
return encodeScopes(authorizedScopes.filter((authorizedScope) => authorizedScope !== scope));
|
return encodeScopes(authorizedScopes.filter((authorizedScope) => authorizedScope !== scope))
|
||||||
} else {
|
} else {
|
||||||
return encodeScopes([...authorizedScopes, scope]);
|
return encodeScopes([...authorizedScopes, scope])
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export const useScopes = () => {
|
export const useScopes = () => {
|
||||||
const { formatMessage } = useVIntl();
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const scopesToDefinitions = (scopes: bigint) => {
|
const scopesToDefinitions = (scopes: bigint) => {
|
||||||
const authorizedScopes = decodeScopes(scopes);
|
const authorizedScopes = decodeScopes(scopes)
|
||||||
return authorizedScopes.map((scope) => {
|
return authorizedScopes.map((scope) => {
|
||||||
const scopeDefinition = scopeDefinitions.find(
|
const scopeDefinition = scopeDefinitions.find(
|
||||||
(scopeDefinition) => scopeDefinition.id === scope,
|
(scopeDefinition) => scopeDefinition.id === scope,
|
||||||
);
|
)
|
||||||
if (!scopeDefinition) {
|
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 scopesToLabels = (scopes: bigint) => {
|
||||||
const authorizedScopes = decodeScopes(scopes);
|
const authorizedScopes = decodeScopes(scopes)
|
||||||
return authorizedScopes.map((scope) => {
|
return authorizedScopes.map((scope) => {
|
||||||
const scopeDefinition = scopeDefinitions.find(
|
const scopeDefinition = scopeDefinitions.find(
|
||||||
(scopeDefinition) => scopeDefinition.id === scope,
|
(scopeDefinition) => scopeDefinition.id === scope,
|
||||||
);
|
)
|
||||||
if (!scopeDefinition) {
|
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 {
|
return {
|
||||||
scopesToDefinitions,
|
scopesToDefinitions,
|
||||||
scopesToLabels,
|
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) {
|
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) {
|
if (!formatter) {
|
||||||
formatter = new Intl.NumberFormat(locale, {
|
formatter = new Intl.NumberFormat(locale, {
|
||||||
notation: "compact",
|
notation: 'compact',
|
||||||
maximumFractionDigits: fractionDigits,
|
maximumFractionDigits: fractionDigits,
|
||||||
});
|
})
|
||||||
formatters.set(context, formatter);
|
formatters.set(context, formatter)
|
||||||
}
|
}
|
||||||
|
|
||||||
function format(value: number): string {
|
function format(value: number): string {
|
||||||
let formattedValue = value;
|
let formattedValue = value
|
||||||
if (truncate) {
|
if (truncate) {
|
||||||
const scale = Math.pow(10, fractionDigits);
|
const scale = Math.pow(10, fractionDigits)
|
||||||
formattedValue = Math.floor(value * scale) / scale;
|
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 = () => {
|
export const useUserCountry = () => {
|
||||||
const country = useState<string>("userCountry", () => "US");
|
const country = useState<string>('userCountry', () => 'US')
|
||||||
const fromServer = useState<boolean>("userCountryFromServer", () => false);
|
const fromServer = useState<boolean>('userCountryFromServer', () => false)
|
||||||
|
|
||||||
if (import.meta.server) {
|
if (import.meta.server) {
|
||||||
const headers = useRequestHeaders(["cf-ipcountry", "accept-language"]);
|
const headers = useRequestHeaders(['cf-ipcountry', 'accept-language'])
|
||||||
const cf = headers["cf-ipcountry"];
|
const cf = headers['cf-ipcountry']
|
||||||
if (cf) {
|
if (cf) {
|
||||||
country.value = cf.toUpperCase();
|
country.value = cf.toUpperCase()
|
||||||
fromServer.value = true;
|
fromServer.value = true
|
||||||
} else {
|
} else {
|
||||||
const al = headers["accept-language"] || "";
|
const al = headers['accept-language'] || ''
|
||||||
const tag = al.split(",")[0];
|
const tag = al.split(',')[0]
|
||||||
const val = tag.split("-")[1]?.toLowerCase();
|
const val = tag.split('-')[1]?.toLowerCase()
|
||||||
if (val) {
|
if (val) {
|
||||||
country.value = val;
|
country.value = val
|
||||||
fromServer.value = true;
|
fromServer.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.client) {
|
if (import.meta.client) {
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (fromServer.value) return;
|
if (fromServer.value) return
|
||||||
// @ts-expect-error - ignore TS not knowing about navigator.userLanguage
|
// @ts-expect-error - ignore TS not knowing about navigator.userLanguage
|
||||||
const lang = navigator.language || navigator.userLanguage || "";
|
const lang = navigator.language || navigator.userLanguage || ''
|
||||||
const region = lang.split("-")[1];
|
const region = lang.split('-')[1]
|
||||||
if (region) {
|
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) {
|
function safeTagFor(locale: string) {
|
||||||
let safeTag = safeTags.get(locale);
|
let safeTag = safeTags.get(locale)
|
||||||
if (safeTag == null) {
|
if (safeTag == null) {
|
||||||
safeTag = new Intl.Locale(locale).baseName;
|
safeTag = new Intl.Locale(locale).baseName
|
||||||
safeTags.set(locale, safeTag);
|
safeTags.set(locale, safeTag)
|
||||||
}
|
}
|
||||||
return safeTag;
|
return safeTag
|
||||||
}
|
}
|
||||||
|
|
||||||
type DisplayNamesWrapper = Intl.DisplayNames & {
|
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) {
|
function getWrapperKey(locale: string, options: Intl.DisplayNamesOptions) {
|
||||||
return JSON.stringify({ ...options, locale });
|
return JSON.stringify({ ...options, locale })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createDisplayNames(
|
export function createDisplayNames(
|
||||||
locale: string,
|
locale: string,
|
||||||
options: Intl.DisplayNamesOptions = { type: "language" },
|
options: Intl.DisplayNamesOptions = { type: 'language' },
|
||||||
) {
|
) {
|
||||||
const wrapperKey = getWrapperKey(locale, options);
|
const wrapperKey = getWrapperKey(locale, options)
|
||||||
let wrapper = displayNamesDicts.get(wrapperKey);
|
let wrapper = displayNamesDicts.get(wrapperKey)
|
||||||
|
|
||||||
if (wrapper == null) {
|
if (wrapper == null) {
|
||||||
const dict = new Intl.DisplayNames(locale, options);
|
const dict = new Intl.DisplayNames(locale, options)
|
||||||
|
|
||||||
const badTags: string[] = [];
|
const badTags: string[] = []
|
||||||
|
|
||||||
wrapper = {
|
wrapper = {
|
||||||
resolvedOptions() {
|
resolvedOptions() {
|
||||||
return dict.resolvedOptions();
|
return dict.resolvedOptions()
|
||||||
},
|
},
|
||||||
of(tag: string) {
|
of(tag: string) {
|
||||||
let attempt = 0;
|
let attempt = 0
|
||||||
|
|
||||||
|
|
||||||
lookupLoop: do {
|
lookupLoop: do {
|
||||||
let lookup: string;
|
let lookup: string
|
||||||
switch (attempt) {
|
switch (attempt) {
|
||||||
case 0:
|
case 0:
|
||||||
lookup = tag;
|
lookup = tag
|
||||||
break;
|
break
|
||||||
case 1:
|
case 1:
|
||||||
lookup = safeTagFor(tag);
|
lookup = safeTagFor(tag)
|
||||||
break;
|
break
|
||||||
default:
|
default:
|
||||||
|
break lookupLoop
|
||||||
break lookupLoop;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (badTags.includes(lookup)) continue;
|
if (badTags.includes(lookup)) continue
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return dict.of(lookup);
|
return dict.of(lookup)
|
||||||
} catch {
|
} catch {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Failed to get display name for ${lookup} using dictionary for ${
|
`Failed to get display name for ${lookup} using dictionary for ${
|
||||||
this.resolvedOptions().locale
|
this.resolvedOptions().locale
|
||||||
}`,
|
}`,
|
||||||
);
|
)
|
||||||
badTags.push(lookup);
|
badTags.push(lookup)
|
||||||
continue;
|
continue
|
||||||
}
|
}
|
||||||
} while (++attempt < 5);
|
} while (++attempt < 5)
|
||||||
|
|
||||||
return undefined;
|
return undefined
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
displayNamesDicts.set(wrapperKey, wrapper);
|
displayNamesDicts.set(wrapperKey, wrapper)
|
||||||
}
|
}
|
||||||
|
|
||||||
return wrapper;
|
return wrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDisplayNames(
|
export function useDisplayNames(
|
||||||
@ -85,8 +83,8 @@ export function useDisplayNames(
|
|||||||
| (() => Intl.DisplayNamesOptions | undefined)
|
| (() => Intl.DisplayNamesOptions | undefined)
|
||||||
| Ref<Intl.DisplayNamesOptions | undefined>,
|
| Ref<Intl.DisplayNamesOptions | undefined>,
|
||||||
) {
|
) {
|
||||||
const $locale = toRef(locale);
|
const $locale = toRef(locale)
|
||||||
const $options = toRef(options);
|
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