* Translate and sort server filters * Set team_members to unknown[] * Additional fixes after merge * Additional translations * Replace "IP" with "server address" * Prioritize English and user language
329 lines
8.2 KiB
Vue
329 lines
8.2 KiB
Vue
<template>
|
|
<div v-if="open" class="open-in-app-modal">
|
|
<div :class="{ shown: visible }" class="fullscreen-overlay" @click="hide" />
|
|
<div class="modal-content" :class="{ shown: visible }">
|
|
<div class="flex flex-col items-center gap-6">
|
|
<div class="flex flex-col gap-6">
|
|
<div v-if="countdown > 0" class="countdown-container">
|
|
<svg class="countdown-svg" viewBox="0 0 100 100">
|
|
<circle
|
|
class="stroke-surface-4"
|
|
cx="50"
|
|
cy="50"
|
|
r="45"
|
|
fill="none"
|
|
stroke-width="6"
|
|
/>
|
|
<circle
|
|
class="countdown-progress"
|
|
cx="50"
|
|
cy="50"
|
|
r="45"
|
|
fill="none"
|
|
stroke-width="6"
|
|
:stroke-dasharray="circumference"
|
|
:stroke-dashoffset="strokeDashoffset"
|
|
stroke-linecap="round"
|
|
/>
|
|
</svg>
|
|
<span class="countdown-number">{{ countdown }}</span>
|
|
</div>
|
|
|
|
<h2 class="m-0 text-3xl font-bold text-contrast text-center">
|
|
{{ formatMessage(messages.openingApp) }}
|
|
</h2>
|
|
|
|
<div
|
|
class="flex flex-col items-center gap-4 bg-surface-3 rounded-3xl border border-solid border-surface-5 p-6"
|
|
>
|
|
<div class="flex items-center gap-3 rounded-xl bg-surface-2 p-3 w-full">
|
|
<Avatar :src="serverProject.icon" :alt="serverProject.name" size="48px" />
|
|
<div class="flex flex-col gap-1">
|
|
<span class="font-semibold text-contrast">{{ serverProject.name }}</span>
|
|
<div class="flex items-center gap-2 text-secondary">
|
|
<ServerOnlinePlayers
|
|
:online="serverProject.numPlayers ?? 0"
|
|
:status-online="serverProject.statusOnline"
|
|
/>
|
|
<ServerRegion v-if="serverProject.region" :region="serverProject.region" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-col text-left gap-3">
|
|
<span class="font-semibold text-contrast">{{
|
|
formatMessage(messages.whyUseApp)
|
|
}}</span>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<div class="flex text-base gap-2 items-center">
|
|
<div
|
|
class="w-5 h-5 border border-solid rounded-full flex items-center justify-center border-brand bg-brand-highlight text-brand"
|
|
>
|
|
<CheckIcon />
|
|
</div>
|
|
<span>{{ formatMessage(messages.benefitLaunch) }}</span>
|
|
</div>
|
|
<div class="flex text-base gap-2 items-center">
|
|
<div
|
|
class="w-5 h-5 border border-solid rounded-full flex items-center justify-center border-brand bg-brand-highlight text-brand"
|
|
>
|
|
<CheckIcon />
|
|
</div>
|
|
<span>{{ formatMessage(messages.benefitInstall) }}</span>
|
|
</div>
|
|
<div class="flex text-base gap-2 items-center">
|
|
<div
|
|
class="w-5 h-5 border border-solid rounded-full flex items-center justify-center border-brand bg-brand-highlight text-brand"
|
|
>
|
|
<CheckIcon />
|
|
</div>
|
|
<span>{{ formatMessage(messages.benefitUpdate) }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<span v-if="countdown > 0" class="text-secondary">{{
|
|
formatMessage(messages.openingAutomatically)
|
|
}}</span>
|
|
<div v-else class="grid grid-cols-2 gap-2 w-full">
|
|
<ButtonStyled class="flex-1">
|
|
<button @click="hide">
|
|
<XIcon />
|
|
{{ formatMessage(commonMessages.closeButton) }}
|
|
</button>
|
|
</ButtonStyled>
|
|
<ButtonStyled color="green" class="flex-1">
|
|
<a href="https://modrinth.com/app" target="_blank" rel="noopener noreferrer">
|
|
<DownloadIcon />
|
|
{{ formatMessage(messages.getApp) }}
|
|
</a>
|
|
</ButtonStyled>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { CheckIcon, DownloadIcon, XIcon } from '@modrinth/assets'
|
|
import { commonMessages } from '@modrinth/ui'
|
|
import { computed, nextTick, onUnmounted, ref } from 'vue'
|
|
|
|
import { defineMessages, useVIntl } from '../../composables/i18n'
|
|
import { Avatar, ButtonStyled } from '../base'
|
|
import ServerOnlinePlayers from '../project/server/ServerOnlinePlayers.vue'
|
|
import ServerRegion from '../project/server/ServerRegion.vue'
|
|
|
|
const { formatMessage } = useVIntl()
|
|
|
|
const messages = defineMessages({
|
|
openingApp: {
|
|
id: 'modal.open-in-app.title',
|
|
defaultMessage: 'Opening Modrinth App',
|
|
},
|
|
whyUseApp: {
|
|
id: 'modal.open-in-app.why-use',
|
|
defaultMessage: 'Why use the Modrinth App',
|
|
},
|
|
benefitLaunch: {
|
|
id: 'modal.open-in-app.benefit.launch',
|
|
defaultMessage: 'Launch the game straight into the server',
|
|
},
|
|
benefitInstall: {
|
|
id: 'modal.open-in-app.benefit.install',
|
|
defaultMessage: 'Automatically install required content',
|
|
},
|
|
benefitUpdate: {
|
|
id: 'modal.open-in-app.benefit.update',
|
|
defaultMessage: 'Keep files updated when the server changes',
|
|
},
|
|
openingAutomatically: {
|
|
id: 'modal.open-in-app.opening-automatically',
|
|
defaultMessage: 'The Modrinth App will open automatically...',
|
|
},
|
|
getApp: {
|
|
id: 'modal.open-in-app.get-app',
|
|
defaultMessage: 'Get Modrinth App',
|
|
},
|
|
})
|
|
|
|
export interface ServerProject {
|
|
name: string
|
|
slug?: string
|
|
numPlayers?: number
|
|
icon?: string
|
|
statusOnline?: boolean
|
|
region?: string
|
|
}
|
|
|
|
const open = ref(false)
|
|
const visible = ref(false)
|
|
const countdown = ref(3)
|
|
const countdownProgress = ref(1)
|
|
let countdownInterval: ReturnType<typeof setInterval> | null = null
|
|
let progressInterval: ReturnType<typeof setInterval> | null = null
|
|
|
|
const circumference = 2 * Math.PI * 45
|
|
const strokeDashoffset = computed(() => {
|
|
return circumference * (1 - countdownProgress.value)
|
|
})
|
|
|
|
const serverProject = ref<ServerProject>({
|
|
name: '',
|
|
slug: '',
|
|
numPlayers: 0,
|
|
icon: undefined,
|
|
statusOnline: false,
|
|
region: '',
|
|
})
|
|
const appLink = computed(() => {
|
|
return `modrinth://server/${serverProject.value.slug}`
|
|
})
|
|
|
|
function startCountdown() {
|
|
countdown.value = 3
|
|
countdownProgress.value = 1
|
|
|
|
const totalDuration = 3000
|
|
const progressUpdateInterval = 16
|
|
const progressDecrement = progressUpdateInterval / totalDuration
|
|
|
|
progressInterval = setInterval(() => {
|
|
countdownProgress.value = Math.max(0, countdownProgress.value - progressDecrement)
|
|
}, progressUpdateInterval)
|
|
|
|
countdownInterval = setInterval(() => {
|
|
countdown.value--
|
|
if (countdown.value <= 0) {
|
|
stopCountdown()
|
|
}
|
|
}, 1000)
|
|
}
|
|
|
|
function stopCountdown() {
|
|
if (countdownInterval) {
|
|
clearInterval(countdownInterval)
|
|
countdownInterval = null
|
|
}
|
|
if (progressInterval) {
|
|
clearInterval(progressInterval)
|
|
progressInterval = null
|
|
}
|
|
}
|
|
|
|
interface ShowOpenInAppOptions {
|
|
serverProject: ServerProject
|
|
}
|
|
|
|
async function show(options: ShowOpenInAppOptions) {
|
|
serverProject.value = options.serverProject
|
|
|
|
await nextTick()
|
|
|
|
window.open(appLink.value, '_self')
|
|
open.value = true
|
|
document.body.style.overflow = 'hidden'
|
|
window.addEventListener('keydown', handleKeyDown)
|
|
setTimeout(() => {
|
|
visible.value = true
|
|
startCountdown()
|
|
}, 50)
|
|
}
|
|
|
|
function hide() {
|
|
visible.value = false
|
|
document.body.style.overflow = ''
|
|
window.removeEventListener('keydown', handleKeyDown)
|
|
stopCountdown()
|
|
setTimeout(() => {
|
|
open.value = false
|
|
}, 300)
|
|
}
|
|
|
|
function handleKeyDown(event: KeyboardEvent) {
|
|
if (event.key === 'Escape') {
|
|
hide()
|
|
}
|
|
}
|
|
|
|
onUnmounted(() => {
|
|
stopCountdown()
|
|
})
|
|
|
|
defineExpose({ show, hide, open })
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.open-in-app-modal {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 100;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.fullscreen-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: linear-gradient(to bottom, rgba(66, 131, 92, 0.23) 0%, rgba(17, 35, 43, 0.4) 97%);
|
|
backdrop-filter: blur(12px);
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease-out;
|
|
cursor: pointer;
|
|
|
|
&.shown {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
.modal-content {
|
|
position: relative;
|
|
z-index: 1;
|
|
padding: 2.5rem;
|
|
opacity: 0;
|
|
transform: scale(0.95);
|
|
transition: all 0.3s ease-out;
|
|
|
|
&.shown {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
}
|
|
|
|
.countdown-container {
|
|
position: relative;
|
|
width: 120px;
|
|
height: 120px;
|
|
margin: 0 auto;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.countdown-svg {
|
|
position: absolute;
|
|
width: 100%;
|
|
height: 100%;
|
|
transform: rotate(-90deg);
|
|
}
|
|
|
|
.countdown-bg {
|
|
stroke: var(--surface-4);
|
|
}
|
|
|
|
.countdown-progress {
|
|
stroke: var(--color-green);
|
|
transition: stroke-dashoffset 0.05s linear;
|
|
}
|
|
|
|
.countdown-number {
|
|
font-size: 3rem;
|
|
font-weight: 700;
|
|
color: var(--color-contrast);
|
|
z-index: 1;
|
|
}
|
|
</style>
|