refactor: TabbedModal to use NewModal and DI (#5612)
* refactor: tabbed modal to use NewModal * refactor: use DI for instance settings modal instead of passing down props * pnpm prepr
This commit is contained in:
@@ -18,8 +18,9 @@ import { useRouter } from 'vue-router'
|
|||||||
import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue'
|
import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import { duplicate, edit, edit_icon, list, remove } from '@/helpers/profile'
|
import { duplicate, edit, edit_icon, list, remove } from '@/helpers/profile'
|
||||||
|
import { injectInstanceSettings } from '@/providers/instance-settings'
|
||||||
|
|
||||||
import type { GameInstance, InstanceSettingsTabProps } from '../../../helpers/types'
|
import type { GameInstance } from '../../../helpers/types'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
@@ -27,21 +28,21 @@ const router = useRouter()
|
|||||||
|
|
||||||
const deleteConfirmModal = ref()
|
const deleteConfirmModal = ref()
|
||||||
|
|
||||||
const props = defineProps<InstanceSettingsTabProps>()
|
const { instance } = injectInstanceSettings()
|
||||||
|
|
||||||
const title = ref(props.instance.name)
|
const title = ref(instance.name)
|
||||||
const icon: Ref<string | undefined> = ref(props.instance.icon_path)
|
const icon: Ref<string | undefined> = ref(instance.icon_path)
|
||||||
const groups = ref(props.instance.groups)
|
const groups = ref(instance.groups)
|
||||||
|
|
||||||
const newCategoryInput = ref('')
|
const newCategoryInput = ref('')
|
||||||
|
|
||||||
const installing = computed(() => props.instance.install_stage !== 'installed')
|
const installing = computed(() => instance.install_stage !== 'installed')
|
||||||
|
|
||||||
async function duplicateProfile() {
|
async function duplicateProfile() {
|
||||||
await duplicate(props.instance.path).catch(handleError)
|
await duplicate(instance.path).catch(handleError)
|
||||||
trackEvent('InstanceDuplicate', {
|
trackEvent('InstanceDuplicate', {
|
||||||
loader: props.instance.loader,
|
loader: instance.loader,
|
||||||
game_version: props.instance.game_version,
|
game_version: instance.game_version,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +53,7 @@ const availableGroups = computed(() => [
|
|||||||
|
|
||||||
async function resetIcon() {
|
async function resetIcon() {
|
||||||
icon.value = undefined
|
icon.value = undefined
|
||||||
await edit_icon(props.instance.path, null).catch(handleError)
|
await edit_icon(instance.path, null).catch(handleError)
|
||||||
trackEvent('InstanceRemoveIcon')
|
trackEvent('InstanceRemoveIcon')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +71,7 @@ async function setIcon() {
|
|||||||
if (!value) return
|
if (!value) return
|
||||||
|
|
||||||
icon.value = value
|
icon.value = value
|
||||||
await edit_icon(props.instance.path, icon.value).catch(handleError)
|
await edit_icon(instance.path, icon.value).catch(handleError)
|
||||||
|
|
||||||
trackEvent('InstanceSetIcon')
|
trackEvent('InstanceSetIcon')
|
||||||
}
|
}
|
||||||
@@ -101,7 +102,7 @@ watch(
|
|||||||
[title, groups, groups],
|
[title, groups, groups],
|
||||||
async () => {
|
async () => {
|
||||||
if (removing.value) return
|
if (removing.value) return
|
||||||
await edit(props.instance.path, editProfileObject.value).catch(handleError)
|
await edit(instance.path, editProfileObject.value).catch(handleError)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
@@ -109,11 +110,11 @@ watch(
|
|||||||
const removing = ref(false)
|
const removing = ref(false)
|
||||||
async function removeProfile() {
|
async function removeProfile() {
|
||||||
removing.value = true
|
removing.value = true
|
||||||
const path = props.instance.path
|
const path = instance.path
|
||||||
|
|
||||||
trackEvent('InstanceRemove', {
|
trackEvent('InstanceRemove', {
|
||||||
loader: props.instance.loader,
|
loader: instance.loader,
|
||||||
game_version: props.instance.game_version,
|
game_version: instance.game_version,
|
||||||
})
|
})
|
||||||
|
|
||||||
await router.push({ path: '/' })
|
await router.push({ path: '/' })
|
||||||
@@ -218,7 +219,7 @@ const messages = defineMessages({
|
|||||||
:src="icon ? convertFileSrc(icon) : icon"
|
:src="icon ? convertFileSrc(icon) : icon"
|
||||||
size="108px"
|
size="108px"
|
||||||
class="!border-4 group-hover:brightness-75"
|
class="!border-4 group-hover:brightness-75"
|
||||||
:tint-by="props.instance.path"
|
:tint-by="instance.path"
|
||||||
no-shadow
|
no-shadow
|
||||||
/>
|
/>
|
||||||
<div class="absolute top-0 right-0 m-2">
|
<div class="absolute top-0 right-0 m-2">
|
||||||
|
|||||||
@@ -10,22 +10,21 @@ import { computed, ref, watch } from 'vue'
|
|||||||
|
|
||||||
import { edit } from '@/helpers/profile'
|
import { edit } from '@/helpers/profile'
|
||||||
import { get } from '@/helpers/settings.ts'
|
import { get } from '@/helpers/settings.ts'
|
||||||
|
import { injectInstanceSettings } from '@/providers/instance-settings'
|
||||||
|
|
||||||
import type { AppSettings, Hooks, InstanceSettingsTabProps } from '../../../helpers/types'
|
import type { AppSettings, Hooks } from '../../../helpers/types'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const props = defineProps<InstanceSettingsTabProps>()
|
const { instance } = injectInstanceSettings()
|
||||||
|
|
||||||
const globalSettings = (await get().catch(handleError)) as AppSettings
|
const globalSettings = (await get().catch(handleError)) as AppSettings
|
||||||
|
|
||||||
const overrideHooks = ref(
|
const overrideHooks = ref(
|
||||||
!!props.instance.hooks.pre_launch ||
|
!!instance.hooks.pre_launch || !!instance.hooks.wrapper || !!instance.hooks.post_exit,
|
||||||
!!props.instance.hooks.wrapper ||
|
|
||||||
!!props.instance.hooks.post_exit,
|
|
||||||
)
|
)
|
||||||
const hooks = ref(props.instance.hooks ?? globalSettings.hooks)
|
const hooks = ref(instance.hooks ?? globalSettings.hooks)
|
||||||
|
|
||||||
const editProfileObject = computed(() => {
|
const editProfileObject = computed(() => {
|
||||||
const editProfile: {
|
const editProfile: {
|
||||||
@@ -41,7 +40,7 @@ const editProfileObject = computed(() => {
|
|||||||
watch(
|
watch(
|
||||||
[overrideHooks, hooks],
|
[overrideHooks, hooks],
|
||||||
async () => {
|
async () => {
|
||||||
await edit(props.instance.path, editProfileObject.value)
|
await edit(instance.path, editProfileObject.value)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -27,17 +27,15 @@ import {
|
|||||||
update_repair_modrinth,
|
update_repair_modrinth,
|
||||||
} from '@/helpers/profile'
|
} from '@/helpers/profile'
|
||||||
import { get_game_versions, get_loaders } from '@/helpers/tags'
|
import { get_game_versions, get_loaders } from '@/helpers/tags'
|
||||||
|
import { injectInstanceSettings } from '@/providers/instance-settings'
|
||||||
|
|
||||||
import type { InstanceSettingsTabProps, Manifest } from '../../../helpers/types'
|
import type { Manifest } from '../../../helpers/types'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
const props = defineProps<InstanceSettingsTabProps>()
|
const { instance, offline, isMinecraftServer, onUnlinked } = injectInstanceSettings()
|
||||||
const emit = defineEmits<{
|
|
||||||
unlinked: []
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const [
|
const [
|
||||||
fabric_versions,
|
fabric_versions,
|
||||||
@@ -75,9 +73,9 @@ const [
|
|||||||
])
|
])
|
||||||
|
|
||||||
const { data: modpackInfo } = useQuery({
|
const { data: modpackInfo } = useQuery({
|
||||||
queryKey: computed(() => ['linkedModpackInfo', props.instance.path]),
|
queryKey: computed(() => ['linkedModpackInfo', instance.path]),
|
||||||
queryFn: () => get_linked_modpack_info(props.instance.path, 'must_revalidate'),
|
queryFn: () => get_linked_modpack_info(instance.path, 'must_revalidate'),
|
||||||
enabled: computed(() => !!props.instance.linked_data?.project_id && !props.offline),
|
enabled: computed(() => !!instance.linked_data?.project_id && !offline),
|
||||||
})
|
})
|
||||||
|
|
||||||
const repairing = ref(false)
|
const repairing = ref(false)
|
||||||
@@ -103,13 +101,13 @@ function getManifest(loader: string) {
|
|||||||
provideAppBackup({
|
provideAppBackup({
|
||||||
async createBackup() {
|
async createBackup() {
|
||||||
const allProfiles = await list()
|
const allProfiles = await list()
|
||||||
const prefix = `${props.instance.name} - Backup #`
|
const prefix = `${instance.name} - Backup #`
|
||||||
const existingNums = allProfiles
|
const existingNums = allProfiles
|
||||||
.filter((p) => p.name.startsWith(prefix))
|
.filter((p) => p.name.startsWith(prefix))
|
||||||
.map((p) => parseInt(p.name.slice(prefix.length), 10))
|
.map((p) => parseInt(p.name.slice(prefix.length), 10))
|
||||||
.filter((n) => !isNaN(n))
|
.filter((n) => !isNaN(n))
|
||||||
const nextNum = existingNums.length > 0 ? Math.max(...existingNums) + 1 : 1
|
const nextNum = existingNums.length > 0 ? Math.max(...existingNums) + 1 : 1
|
||||||
const newPath = await duplicate(props.instance.path)
|
const newPath = await duplicate(instance.path)
|
||||||
await edit(newPath, { name: `${prefix}${nextNum}` })
|
await edit(newPath, { name: `${prefix}${nextNum}` })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -120,30 +118,27 @@ provideInstallationSettings({
|
|||||||
const rows = [
|
const rows = [
|
||||||
{
|
{
|
||||||
label: formatMessage(commonMessages.platformLabel),
|
label: formatMessage(commonMessages.platformLabel),
|
||||||
value: formatLoaderLabel(props.instance.loader),
|
value: formatLoaderLabel(instance.loader),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: formatMessage(commonMessages.gameVersionLabel),
|
label: formatMessage(commonMessages.gameVersionLabel),
|
||||||
value: props.instance.game_version,
|
value: instance.game_version,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
if (props.instance.loader !== 'vanilla' && props.instance.loader_version) {
|
if (instance.loader !== 'vanilla' && instance.loader_version) {
|
||||||
rows.push({
|
rows.push({
|
||||||
label: formatMessage(messages.loaderVersion, {
|
label: formatMessage(messages.loaderVersion, {
|
||||||
loader: formatLoaderLabel(props.instance.loader),
|
loader: formatLoaderLabel(instance.loader),
|
||||||
}),
|
}),
|
||||||
value: props.instance.loader_version,
|
value: instance.loader_version,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return rows
|
return rows
|
||||||
}),
|
}),
|
||||||
isLinked: computed(() => !!props.instance.linked_data?.locked),
|
isLinked: computed(() => !!instance.linked_data?.locked),
|
||||||
isBusy: computed(
|
isBusy: computed(
|
||||||
() =>
|
() =>
|
||||||
props.instance.install_stage !== 'installed' ||
|
instance.install_stage !== 'installed' || repairing.value || reinstalling.value || !!offline,
|
||||||
repairing.value ||
|
|
||||||
reinstalling.value ||
|
|
||||||
!!props.offline,
|
|
||||||
),
|
),
|
||||||
modpack: computed(() => {
|
modpack: computed(() => {
|
||||||
if (!modpackInfo.value) return null
|
if (!modpackInfo.value) return null
|
||||||
@@ -154,9 +149,9 @@ provideInstallationSettings({
|
|||||||
versionNumber: modpackInfo.value.version?.version_number,
|
versionNumber: modpackInfo.value.version?.version_number,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
currentPlatform: computed(() => props.instance.loader),
|
currentPlatform: computed(() => instance.loader),
|
||||||
currentGameVersion: computed(() => props.instance.game_version),
|
currentGameVersion: computed(() => instance.game_version),
|
||||||
currentLoaderVersion: computed(() => props.instance.loader_version ?? ''),
|
currentLoaderVersion: computed(() => instance.loader_version ?? ''),
|
||||||
availablePlatforms: loaders?.value?.map((x) => x.name) ?? [],
|
availablePlatforms: loaders?.value?.map((x) => x.name) ?? [],
|
||||||
|
|
||||||
resolveGameVersions(loader, showSnapshots) {
|
resolveGameVersions(loader, showSnapshots) {
|
||||||
@@ -199,50 +194,50 @@ provideInstallationSettings({
|
|||||||
if (platform !== 'vanilla' && loaderVersionId) {
|
if (platform !== 'vanilla' && loaderVersionId) {
|
||||||
editProfile.loader_version = loaderVersionId
|
editProfile.loader_version = loaderVersionId
|
||||||
}
|
}
|
||||||
await edit(props.instance.path, editProfile).catch(handleError)
|
await edit(instance.path, editProfile).catch(handleError)
|
||||||
},
|
},
|
||||||
|
|
||||||
afterSave: async () => {
|
afterSave: async () => {
|
||||||
await install(props.instance.path, false).catch(handleError)
|
await install(instance.path, false).catch(handleError)
|
||||||
trackEvent('InstanceRepair', {
|
trackEvent('InstanceRepair', {
|
||||||
loader: props.instance.loader,
|
loader: instance.loader,
|
||||||
game_version: props.instance.game_version,
|
game_version: instance.game_version,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
async repair() {
|
async repair() {
|
||||||
repairing.value = true
|
repairing.value = true
|
||||||
await install(props.instance.path, true).catch(handleError)
|
await install(instance.path, true).catch(handleError)
|
||||||
repairing.value = false
|
repairing.value = false
|
||||||
trackEvent('InstanceRepair', {
|
trackEvent('InstanceRepair', {
|
||||||
loader: props.instance.loader,
|
loader: instance.loader,
|
||||||
game_version: props.instance.game_version,
|
game_version: instance.game_version,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
async reinstallModpack() {
|
async reinstallModpack() {
|
||||||
reinstalling.value = true
|
reinstalling.value = true
|
||||||
await update_repair_modrinth(props.instance.path).catch(handleError)
|
await update_repair_modrinth(instance.path).catch(handleError)
|
||||||
reinstalling.value = false
|
reinstalling.value = false
|
||||||
trackEvent('InstanceRepair', {
|
trackEvent('InstanceRepair', {
|
||||||
loader: props.instance.loader,
|
loader: instance.loader,
|
||||||
game_version: props.instance.game_version,
|
game_version: instance.game_version,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
async unlinkModpack() {
|
async unlinkModpack() {
|
||||||
await edit(props.instance.path, {
|
await edit(instance.path, {
|
||||||
linked_data: null as unknown as undefined,
|
linked_data: null as unknown as undefined,
|
||||||
})
|
})
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: ['linkedModpackInfo', props.instance.path],
|
queryKey: ['linkedModpackInfo', instance.path],
|
||||||
})
|
})
|
||||||
emit('unlinked')
|
onUnlinked()
|
||||||
},
|
},
|
||||||
|
|
||||||
getCachedModpackVersions: () => null,
|
getCachedModpackVersions: () => null,
|
||||||
async fetchModpackVersions() {
|
async fetchModpackVersions() {
|
||||||
const versions = await get_project_versions(props.instance.linked_data!.project_id!).catch(
|
const versions = await get_project_versions(instance.linked_data!.project_id!).catch(
|
||||||
handleError,
|
handleError,
|
||||||
)
|
)
|
||||||
return (versions ?? []) as Labrinth.Versions.v2.Version[]
|
return (versions ?? []) as Labrinth.Versions.v2.Version[]
|
||||||
@@ -255,25 +250,25 @@ provideInstallationSettings({
|
|||||||
},
|
},
|
||||||
|
|
||||||
async onModpackVersionConfirm(version) {
|
async onModpackVersionConfirm(version) {
|
||||||
await update_managed_modrinth_version(props.instance.path, version.id)
|
await update_managed_modrinth_version(instance.path, version.id)
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: ['linkedModpackInfo', props.instance.path],
|
queryKey: ['linkedModpackInfo', instance.path],
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
updaterModalProps: computed(() => ({
|
updaterModalProps: computed(() => ({
|
||||||
isApp: true,
|
isApp: true,
|
||||||
currentVersionId:
|
currentVersionId:
|
||||||
modpackInfo.value?.update_version_id ?? props.instance.linked_data?.version_id ?? '',
|
modpackInfo.value?.update_version_id ?? instance.linked_data?.version_id ?? '',
|
||||||
projectIconUrl: modpackInfo.value?.project?.icon_url,
|
projectIconUrl: modpackInfo.value?.project?.icon_url,
|
||||||
projectName: modpackInfo.value?.project?.title ?? 'Modpack',
|
projectName: modpackInfo.value?.project?.title ?? 'Modpack',
|
||||||
currentGameVersion: props.instance.game_version,
|
currentGameVersion: instance.game_version,
|
||||||
currentLoader: props.instance.loader,
|
currentLoader: instance.loader,
|
||||||
})),
|
})),
|
||||||
|
|
||||||
isServer: false,
|
isServer: false,
|
||||||
isApp: true,
|
isApp: true,
|
||||||
showModpackVersionActions: !props.isMinecraftServer,
|
showModpackVersionActions: !isMinecraftServer.value,
|
||||||
repairing,
|
repairing,
|
||||||
reinstalling,
|
reinstalling,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,34 +14,31 @@ import JavaSelector from '@/components/ui/JavaSelector.vue'
|
|||||||
import useMemorySlider from '@/composables/useMemorySlider'
|
import useMemorySlider from '@/composables/useMemorySlider'
|
||||||
import { edit, get_optimal_jre_key } from '@/helpers/profile'
|
import { edit, get_optimal_jre_key } from '@/helpers/profile'
|
||||||
import { get } from '@/helpers/settings.ts'
|
import { get } from '@/helpers/settings.ts'
|
||||||
|
import { injectInstanceSettings } from '@/providers/instance-settings'
|
||||||
|
|
||||||
import type { AppSettings, InstanceSettingsTabProps } from '../../../helpers/types'
|
import type { AppSettings } from '../../../helpers/types'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const props = defineProps<InstanceSettingsTabProps>()
|
const { instance } = injectInstanceSettings()
|
||||||
|
|
||||||
const globalSettings = (await get().catch(handleError)) as unknown as AppSettings
|
const globalSettings = (await get().catch(handleError)) as unknown as AppSettings
|
||||||
|
|
||||||
const overrideJavaInstall = ref(!!props.instance.java_path)
|
const overrideJavaInstall = ref(!!instance.java_path)
|
||||||
const optimalJava = readonly(await get_optimal_jre_key(props.instance.path).catch(handleError))
|
const optimalJava = readonly(await get_optimal_jre_key(instance.path).catch(handleError))
|
||||||
const javaInstall = ref({ path: optimalJava.path ?? props.instance.java_path })
|
const javaInstall = ref({ path: optimalJava.path ?? instance.java_path })
|
||||||
|
|
||||||
const overrideJavaArgs = ref((props.instance.extra_launch_args?.length ?? 0) > 0)
|
const overrideJavaArgs = ref((instance.extra_launch_args?.length ?? 0) > 0)
|
||||||
const javaArgs = ref(
|
const javaArgs = ref((instance.extra_launch_args ?? globalSettings.extra_launch_args).join(' '))
|
||||||
(props.instance.extra_launch_args ?? globalSettings.extra_launch_args).join(' '),
|
|
||||||
)
|
|
||||||
|
|
||||||
const overrideEnvVars = ref((props.instance.custom_env_vars?.length ?? 0) > 0)
|
const overrideEnvVars = ref((instance.custom_env_vars?.length ?? 0) > 0)
|
||||||
const envVars = ref(
|
const envVars = ref(
|
||||||
(props.instance.custom_env_vars ?? globalSettings.custom_env_vars)
|
(instance.custom_env_vars ?? globalSettings.custom_env_vars).map((x) => x.join('=')).join(' '),
|
||||||
.map((x) => x.join('='))
|
|
||||||
.join(' '),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const overrideMemorySettings = ref(!!props.instance.memory)
|
const overrideMemorySettings = ref(!!instance.memory)
|
||||||
const memory = ref(props.instance.memory ?? globalSettings.memory)
|
const memory = ref(instance.memory ?? globalSettings.memory)
|
||||||
const { maxMemory, snapPoints } = (await useMemorySlider().catch(handleError)) as unknown as {
|
const { maxMemory, snapPoints } = (await useMemorySlider().catch(handleError)) as unknown as {
|
||||||
maxMemory: number
|
maxMemory: number
|
||||||
snapPoints: number[]
|
snapPoints: number[]
|
||||||
@@ -79,7 +76,7 @@ watch(
|
|||||||
memory,
|
memory,
|
||||||
],
|
],
|
||||||
async () => {
|
async () => {
|
||||||
await edit(props.instance.path, editProfileObject.value)
|
await edit(instance.path, editProfileObject.value)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,24 +11,23 @@ import { computed, type Ref, ref, watch } from 'vue'
|
|||||||
|
|
||||||
import { edit } from '@/helpers/profile'
|
import { edit } from '@/helpers/profile'
|
||||||
import { get } from '@/helpers/settings.ts'
|
import { get } from '@/helpers/settings.ts'
|
||||||
|
import { injectInstanceSettings } from '@/providers/instance-settings'
|
||||||
|
|
||||||
import type { AppSettings, InstanceSettingsTabProps } from '../../../helpers/types'
|
import type { AppSettings } from '../../../helpers/types'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const props = defineProps<InstanceSettingsTabProps>()
|
const { instance } = injectInstanceSettings()
|
||||||
|
|
||||||
const globalSettings = (await get().catch(handleError)) as AppSettings
|
const globalSettings = (await get().catch(handleError)) as AppSettings
|
||||||
|
|
||||||
const overrideWindowSettings = ref(
|
const overrideWindowSettings = ref(!!instance.game_resolution || !!instance.force_fullscreen)
|
||||||
!!props.instance.game_resolution || !!props.instance.force_fullscreen,
|
|
||||||
)
|
|
||||||
const resolution: Ref<[number, number]> = ref(
|
const resolution: Ref<[number, number]> = ref(
|
||||||
props.instance.game_resolution ?? (globalSettings.game_resolution.slice() as [number, number]),
|
instance.game_resolution ?? (globalSettings.game_resolution.slice() as [number, number]),
|
||||||
)
|
)
|
||||||
const fullscreenSetting: Ref<boolean> = ref(
|
const fullscreenSetting: Ref<boolean> = ref(
|
||||||
props.instance.force_fullscreen ?? globalSettings.force_fullscreen,
|
instance.force_fullscreen ?? globalSettings.force_fullscreen,
|
||||||
)
|
)
|
||||||
|
|
||||||
const editProfileObject = computed(() => {
|
const editProfileObject = computed(() => {
|
||||||
@@ -47,7 +46,7 @@ const editProfileObject = computed(() => {
|
|||||||
watch(
|
watch(
|
||||||
[overrideWindowSettings, resolution, fullscreenSetting],
|
[overrideWindowSettings, resolution, fullscreenSetting],
|
||||||
async () => {
|
async () => {
|
||||||
await edit(props.instance.path, editProfileObject.value)
|
await edit(instance.path, editProfileObject.value)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,9 +20,8 @@ import {
|
|||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { getVersion } from '@tauri-apps/api/app'
|
import { getVersion } from '@tauri-apps/api/app'
|
||||||
import { platform as getOsPlatform, version as getOsVersion } from '@tauri-apps/plugin-os'
|
import { platform as getOsPlatform, version as getOsVersion } from '@tauri-apps/plugin-os'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import AppearanceSettings from '@/components/ui/settings/AppearanceSettings.vue'
|
import AppearanceSettings from '@/components/ui/settings/AppearanceSettings.vue'
|
||||||
import DefaultInstanceSettings from '@/components/ui/settings/DefaultInstanceSettings.vue'
|
import DefaultInstanceSettings from '@/components/ui/settings/DefaultInstanceSettings.vue'
|
||||||
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
|
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
|
||||||
@@ -106,15 +105,13 @@ const tabs = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const modal = ref()
|
const modal = ref<InstanceType<typeof TabbedModal> | null>(null)
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
modal.value.show()
|
modal.value?.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOpen = computed(() => modal.value?.isOpen)
|
defineExpose({ show })
|
||||||
|
|
||||||
defineExpose({ show, isOpen })
|
|
||||||
|
|
||||||
const { progress, version: downloadingVersion } = injectAppUpdateDownloadProgress()
|
const { progress, version: downloadingVersion } = injectAppUpdateDownloadProgress()
|
||||||
|
|
||||||
@@ -138,8 +135,8 @@ function devModeCount() {
|
|||||||
settings.value.developer_mode = !!themeStore.devMode
|
settings.value.developer_mode = !!themeStore.devMode
|
||||||
devModeCounter.value = 0
|
devModeCounter.value = 0
|
||||||
|
|
||||||
if (!themeStore.devMode && tabs[modal.value.selectedTab].developerOnly) {
|
if (!themeStore.devMode && tabs[modal.value!.selectedTab].developerOnly) {
|
||||||
modal.value.setTab(0)
|
modal.value!.setTab(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,49 +149,46 @@ const messages = defineMessages({
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="modal">
|
<TabbedModal ref="modal" :tabs="tabs.filter((t) => !t.developerOnly || themeStore.devMode)">
|
||||||
<template #title>
|
<template #title>
|
||||||
<span class="flex items-center gap-2 text-lg font-extrabold text-contrast">
|
<span class="flex items-center gap-2 text-lg font-extrabold text-contrast">
|
||||||
<SettingsIcon /> Settings
|
<SettingsIcon /> Settings
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
<template #footer>
|
||||||
<TabbedModal :tabs="tabs.filter((t) => !t.developerOnly || themeStore.devMode)">
|
<div class="mt-auto text-secondary text-sm">
|
||||||
<template #footer>
|
<div class="mb-3">
|
||||||
<div class="mt-auto text-secondary text-sm">
|
<template v-if="progress > 0 && progress < 1">
|
||||||
<div class="mb-3">
|
<p class="m-0 mb-2">
|
||||||
<template v-if="progress > 0 && progress < 1">
|
{{ formatMessage(messages.downloading, { version: downloadingVersion }) }}
|
||||||
<p class="m-0 mb-2">
|
</p>
|
||||||
{{ formatMessage(messages.downloading, { version: downloadingVersion }) }}
|
<ProgressBar :progress="progress" />
|
||||||
</p>
|
</template>
|
||||||
<ProgressBar :progress="progress" />
|
</div>
|
||||||
</template>
|
<p v-if="themeStore.devMode" class="text-brand font-semibold m-0 mb-2">
|
||||||
</div>
|
{{ formatMessage(developerModeEnabled) }}
|
||||||
<p v-if="themeStore.devMode" class="text-brand font-semibold m-0 mb-2">
|
</p>
|
||||||
{{ formatMessage(developerModeEnabled) }}
|
<div class="flex items-center gap-3">
|
||||||
</p>
|
<button
|
||||||
<div class="flex items-center gap-3">
|
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
|
||||||
<button
|
:class="{
|
||||||
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
|
'text-brand': themeStore.devMode,
|
||||||
:class="{
|
'text-secondary': !themeStore.devMode,
|
||||||
'text-brand': themeStore.devMode,
|
}"
|
||||||
'text-secondary': !themeStore.devMode,
|
@click="devModeCount"
|
||||||
}"
|
>
|
||||||
@click="devModeCount"
|
<ModrinthIcon class="w-6 h-6" />
|
||||||
>
|
</button>
|
||||||
<ModrinthIcon class="w-6 h-6" />
|
<div>
|
||||||
</button>
|
<p class="m-0">Modrinth App {{ version }}</p>
|
||||||
<div>
|
<p class="m-0">
|
||||||
<p class="m-0">Modrinth App {{ version }}</p>
|
<span v-if="osPlatform === 'macos'">macOS</span>
|
||||||
<p class="m-0">
|
<span v-else class="capitalize">{{ osPlatform }}</span>
|
||||||
<span v-if="osPlatform === 'macos'">macOS</span>
|
{{ osVersion }}
|
||||||
<span v-else class="capitalize">{{ osPlatform }}</span>
|
</p>
|
||||||
{{ osVersion }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</TabbedModal>
|
</template>
|
||||||
</ModalWrapper>
|
</TabbedModal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -12,14 +12,13 @@ import {
|
|||||||
Avatar,
|
Avatar,
|
||||||
commonMessages,
|
commonMessages,
|
||||||
defineMessage,
|
defineMessage,
|
||||||
NewModal,
|
|
||||||
TabbedModal,
|
TabbedModal,
|
||||||
type TabbedModalTab,
|
type TabbedModalTab,
|
||||||
useVIntl,
|
useVIntl,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue'
|
import { computed, nextTick, ref, watch } from 'vue'
|
||||||
|
|
||||||
import GeneralSettings from '@/components/ui/instance_settings/GeneralSettings.vue'
|
import GeneralSettings from '@/components/ui/instance_settings/GeneralSettings.vue'
|
||||||
import HooksSettings from '@/components/ui/instance_settings/HooksSettings.vue'
|
import HooksSettings from '@/components/ui/instance_settings/HooksSettings.vue'
|
||||||
@@ -28,12 +27,16 @@ import JavaSettings from '@/components/ui/instance_settings/JavaSettings.vue'
|
|||||||
import WindowSettings from '@/components/ui/instance_settings/WindowSettings.vue'
|
import WindowSettings from '@/components/ui/instance_settings/WindowSettings.vue'
|
||||||
import { get_project_v3 } from '@/helpers/cache'
|
import { get_project_v3 } from '@/helpers/cache'
|
||||||
import { get_linked_modpack_info } from '@/helpers/profile'
|
import { get_linked_modpack_info } from '@/helpers/profile'
|
||||||
|
import { provideInstanceSettings } from '@/providers/instance-settings'
|
||||||
|
|
||||||
import type { InstanceSettingsTabProps } from '../../../helpers/types'
|
import type { GameInstance } from '../../../helpers/types'
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const props = defineProps<InstanceSettingsTabProps>()
|
const props = defineProps<{
|
||||||
|
instance: GameInstance
|
||||||
|
offline?: boolean
|
||||||
|
}>()
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
unlinked: []
|
unlinked: []
|
||||||
}>()
|
}>()
|
||||||
@@ -41,6 +44,13 @@ const emit = defineEmits<{
|
|||||||
const isMinecraftServer = ref(false)
|
const isMinecraftServer = ref(false)
|
||||||
const handleUnlinked = () => emit('unlinked')
|
const handleUnlinked = () => emit('unlinked')
|
||||||
|
|
||||||
|
provideInstanceSettings({
|
||||||
|
instance: props.instance,
|
||||||
|
offline: props.offline,
|
||||||
|
isMinecraftServer,
|
||||||
|
onUnlinked: handleUnlinked,
|
||||||
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.instance,
|
() => props.instance,
|
||||||
(instance) => {
|
(instance) => {
|
||||||
@@ -58,7 +68,7 @@ watch(
|
|||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
const tabs = computed<TabbedModalTab<InstanceSettingsTabProps>[]>(() => [
|
const tabs = computed<TabbedModalTab[]>(() => [
|
||||||
{
|
{
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: 'instance.settings.tabs.general',
|
id: 'instance.settings.tabs.general',
|
||||||
@@ -102,8 +112,7 @@ const tabs = computed<TabbedModalTab<InstanceSettingsTabProps>[]>(() => [
|
|||||||
])
|
])
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const modal = ref()
|
const tabbedModal = ref<InstanceType<typeof TabbedModal> | null>(null)
|
||||||
const tabbedModal = useTemplateRef('tabbedModal')
|
|
||||||
|
|
||||||
function show(tabIndex?: number) {
|
function show(tabIndex?: number) {
|
||||||
if (props.instance.linked_data?.project_id) {
|
if (props.instance.linked_data?.project_id) {
|
||||||
@@ -112,7 +121,7 @@ function show(tabIndex?: number) {
|
|||||||
queryFn: () => get_linked_modpack_info(props.instance.path, 'stale_while_revalidate'),
|
queryFn: () => get_linked_modpack_info(props.instance.path, 'stale_while_revalidate'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
modal.value.show()
|
tabbedModal.value?.show()
|
||||||
if (tabIndex !== undefined) {
|
if (tabIndex !== undefined) {
|
||||||
nextTick(() => tabbedModal.value?.setTab(tabIndex))
|
nextTick(() => tabbedModal.value?.setTab(tabIndex))
|
||||||
}
|
}
|
||||||
@@ -121,8 +130,9 @@ function show(tabIndex?: number) {
|
|||||||
defineExpose({ show })
|
defineExpose({ show })
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<NewModal
|
<TabbedModal
|
||||||
ref="modal"
|
ref="tabbedModal"
|
||||||
|
:tabs="tabs"
|
||||||
:max-width="'min(928px, calc(95vw - 10rem))'"
|
:max-width="'min(928px, calc(95vw - 10rem))'"
|
||||||
:width="'min(928px, calc(95vw - 10rem))'"
|
:width="'min(928px, calc(95vw - 10rem))'"
|
||||||
>
|
>
|
||||||
@@ -139,19 +149,5 @@ defineExpose({ show })
|
|||||||
}}</span>
|
}}</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
</TabbedModal>
|
||||||
<TabbedModal
|
|
||||||
ref="tabbedModal"
|
|
||||||
:tabs="
|
|
||||||
tabs.map((tab) => ({
|
|
||||||
...tab,
|
|
||||||
props: {
|
|
||||||
...props,
|
|
||||||
isMinecraftServer,
|
|
||||||
onUnlinked: handleUnlinked,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</NewModal>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
6
apps/app-frontend/src/helpers/types.d.ts
vendored
6
apps/app-frontend/src/helpers/types.d.ts
vendored
@@ -128,9 +128,3 @@ type AppSettings = {
|
|||||||
prev_custom_dir?: string
|
prev_custom_dir?: string
|
||||||
migrated: boolean
|
migrated: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InstanceSettingsTabProps = {
|
|
||||||
instance: GameInstance
|
|
||||||
offline?: boolean
|
|
||||||
isMinecraftServer?: boolean
|
|
||||||
}
|
|
||||||
|
|||||||
14
apps/app-frontend/src/providers/instance-settings.ts
Normal file
14
apps/app-frontend/src/providers/instance-settings.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { createContext } from '@modrinth/ui'
|
||||||
|
import type { Ref } from 'vue'
|
||||||
|
|
||||||
|
import type { GameInstance } from '@/helpers/types'
|
||||||
|
|
||||||
|
export interface InstanceSettingsContext {
|
||||||
|
instance: GameInstance
|
||||||
|
offline?: boolean
|
||||||
|
isMinecraftServer: Ref<boolean>
|
||||||
|
onUnlinked: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const [injectInstanceSettings, provideInstanceSettings] =
|
||||||
|
createContext<InstanceSettingsContext>('InstanceSettingsModal', 'instanceSettings')
|
||||||
@@ -1,24 +1,40 @@
|
|||||||
|
<script lang="ts"></script>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type Component, computed, nextTick, ref } from 'vue'
|
import { type Component, computed, nextTick, ref } from 'vue'
|
||||||
|
|
||||||
import { type MessageDescriptor, useVIntl } from '../../composables/i18n'
|
import { type MessageDescriptor, useVIntl } from '../../composables/i18n'
|
||||||
import { useScrollIndicator } from '../../composables/scroll-indicator'
|
import { useScrollIndicator } from '../../composables/scroll-indicator'
|
||||||
|
import NewModal from './NewModal.vue'
|
||||||
const { formatMessage } = useVIntl()
|
export interface Tab {
|
||||||
|
|
||||||
export type Tab<Props> = {
|
|
||||||
name: MessageDescriptor
|
name: MessageDescriptor
|
||||||
icon: Component
|
icon: Component
|
||||||
content: Component<Props>
|
content: Component
|
||||||
props?: Props
|
|
||||||
badge?: MessageDescriptor
|
badge?: MessageDescriptor
|
||||||
shown?: boolean
|
shown?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const { formatMessage } = useVIntl()
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
tabs: Tab<any>[]
|
const props = withDefaults(
|
||||||
}>()
|
defineProps<{
|
||||||
|
tabs: Tab[]
|
||||||
|
header?: string
|
||||||
|
maxWidth?: string
|
||||||
|
width?: string
|
||||||
|
closable?: boolean
|
||||||
|
onHide?: () => void
|
||||||
|
onShow?: () => void
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
header: undefined,
|
||||||
|
maxWidth: undefined,
|
||||||
|
width: undefined,
|
||||||
|
closable: true,
|
||||||
|
onHide: undefined,
|
||||||
|
onShow: undefined,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const visibleTabs = computed(() => props.tabs.filter((tab) => tab.shown !== false))
|
const visibleTabs = computed(() => props.tabs.filter((tab) => tab.shown !== false))
|
||||||
|
|
||||||
@@ -28,77 +44,98 @@ const scrollContainer = ref<HTMLElement | null>(null)
|
|||||||
const { showTopFade, showBottomFade, checkScrollState, forceCheck } =
|
const { showTopFade, showBottomFade, checkScrollState, forceCheck } =
|
||||||
useScrollIndicator(scrollContainer)
|
useScrollIndicator(scrollContainer)
|
||||||
|
|
||||||
|
const modal = ref<InstanceType<typeof NewModal> | null>(null)
|
||||||
|
|
||||||
function setTab(index: number) {
|
function setTab(index: number) {
|
||||||
selectedTab.value = index
|
selectedTab.value = index
|
||||||
nextTick(() => forceCheck())
|
nextTick(() => forceCheck())
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({ selectedTab, setTab })
|
function show(event?: MouseEvent) {
|
||||||
|
modal.value?.show(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
modal.value?.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ show, hide, selectedTab, setTab })
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="grid grid-cols-[auto_1fr]">
|
<NewModal
|
||||||
<div
|
ref="modal"
|
||||||
class="flex flex-col gap-1 border-solid pr-4 border-0 border-r-[1px] border-divider min-w-[200px]"
|
:header="header"
|
||||||
>
|
:max-width="maxWidth"
|
||||||
<button
|
:width="width"
|
||||||
v-for="(tab, index) in visibleTabs"
|
:closable="closable"
|
||||||
:key="index"
|
:on-hide="onHide"
|
||||||
:class="`flex gap-2 items-center text-left rounded-xl px-4 py-2 border-none text-nowrap font-semibold cursor-pointer active:scale-[0.97] transition-all ${selectedTab === index ? 'bg-button-bgSelected text-button-textSelected' : 'bg-transparent text-button-text hover:bg-button-bg hover:text-contrast'}`"
|
:on-show="onShow"
|
||||||
@click="() => setTab(index)"
|
no-padding
|
||||||
>
|
>
|
||||||
<component :is="tab.icon" class="w-4 h-4 flex-shrink-0" />
|
<template v-if="$slots.title" #title>
|
||||||
<span>{{ formatMessage(tab.name) }}</span>
|
<slot name="title" />
|
||||||
<span
|
</template>
|
||||||
v-if="tab.badge"
|
<div class="grid grid-cols-[auto_1fr] p-4">
|
||||||
class="rounded-full px-1.5 py-0.5 text-xs font-bold bg-brand-highlight text-brand-green"
|
|
||||||
>
|
|
||||||
{{ formatMessage(tab.badge) }}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<slot name="footer" />
|
|
||||||
</div>
|
|
||||||
<div class="relative">
|
|
||||||
<Transition
|
|
||||||
enter-active-class="transition-all duration-200 ease-out"
|
|
||||||
enter-from-class="opacity-0 max-h-0"
|
|
||||||
enter-to-class="opacity-100 max-h-24"
|
|
||||||
leave-active-class="transition-all duration-200 ease-in"
|
|
||||||
leave-from-class="opacity-100 max-h-24"
|
|
||||||
leave-to-class="opacity-0 max-h-0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="showTopFade"
|
|
||||||
class="pointer-events-none absolute left-0 right-0 top-0 z-10 h-24 bg-gradient-to-b from-bg-raised to-transparent"
|
|
||||||
/>
|
|
||||||
</Transition>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref="scrollContainer"
|
class="flex flex-col gap-1 border-solid pr-4 border-0 border-r-[1px] border-divider min-w-[200px]"
|
||||||
class="min-w-[400px] h-[500px] overflow-y-auto px-4"
|
|
||||||
@scroll="checkScrollState"
|
|
||||||
>
|
>
|
||||||
<Suspense>
|
<button
|
||||||
<component
|
v-for="(tab, index) in visibleTabs"
|
||||||
:is="visibleTabs[selectedTab].content"
|
:key="index"
|
||||||
v-bind="visibleTabs[selectedTab].props ?? {}"
|
:class="`flex gap-2 items-center text-left rounded-xl px-4 py-2 border-none text-nowrap font-semibold cursor-pointer active:scale-[0.97] transition-all ${selectedTab === index ? 'bg-button-bgSelected text-button-textSelected' : 'bg-transparent text-button-text hover:bg-button-bg hover:text-contrast'}`"
|
||||||
/>
|
@click="() => setTab(index)"
|
||||||
</Suspense>
|
>
|
||||||
</div>
|
<component :is="tab.icon" class="w-4 h-4 flex-shrink-0" />
|
||||||
|
<span>{{ formatMessage(tab.name) }}</span>
|
||||||
|
<span
|
||||||
|
v-if="tab.badge"
|
||||||
|
class="rounded-full px-1.5 py-0.5 text-xs font-bold bg-brand-highlight text-brand-green"
|
||||||
|
>
|
||||||
|
{{ formatMessage(tab.badge) }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<slot name="footer" />
|
||||||
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition-all duration-200 ease-out"
|
||||||
|
enter-from-class="opacity-0 max-h-0"
|
||||||
|
enter-to-class="opacity-100 max-h-10"
|
||||||
|
leave-active-class="transition-all duration-200 ease-in"
|
||||||
|
leave-from-class="opacity-100 max-h-10"
|
||||||
|
leave-to-class="opacity-0 max-h-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="showTopFade"
|
||||||
|
class="pointer-events-none absolute left-0 right-0 top-0 z-10 h-10 bg-gradient-to-b from-bg-raised to-transparent"
|
||||||
|
/>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
<Transition
|
|
||||||
enter-active-class="transition-all duration-200 ease-out"
|
|
||||||
enter-from-class="opacity-0 max-h-0"
|
|
||||||
enter-to-class="opacity-100 max-h-24"
|
|
||||||
leave-active-class="transition-all duration-200 ease-in"
|
|
||||||
leave-from-class="opacity-100 max-h-24"
|
|
||||||
leave-to-class="opacity-0 max-h-0"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
v-if="showBottomFade"
|
ref="scrollContainer"
|
||||||
class="pointer-events-none absolute bottom-0 left-0 right-0 z-10 h-24 bg-gradient-to-t from-bg-raised to-transparent"
|
class="min-w-[400px] h-[500px] overflow-y-auto px-4"
|
||||||
/>
|
@scroll="checkScrollState"
|
||||||
</Transition>
|
>
|
||||||
|
<Suspense>
|
||||||
|
<component :is="visibleTabs[selectedTab].content" />
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition-all duration-200 ease-out"
|
||||||
|
enter-from-class="opacity-0 max-h-0"
|
||||||
|
enter-to-class="opacity-100 max-h-10"
|
||||||
|
leave-active-class="transition-all duration-200 ease-in"
|
||||||
|
leave-from-class="opacity-100 max-h-10"
|
||||||
|
leave-to-class="opacity-0 max-h-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="showBottomFade"
|
||||||
|
class="pointer-events-none absolute bottom-0 left-0 right-0 z-10 h-10 bg-gradient-to-t from-bg-raised to-transparent"
|
||||||
|
/>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</NewModal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
320
packages/ui/src/stories/modal/TabbedModal.stories.ts
Normal file
320
packages/ui/src/stories/modal/TabbedModal.stories.ts
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
import {
|
||||||
|
CoffeeIcon,
|
||||||
|
GameIcon,
|
||||||
|
GaugeIcon,
|
||||||
|
InfoIcon,
|
||||||
|
LanguagesIcon,
|
||||||
|
MonitorIcon,
|
||||||
|
PaintbrushIcon,
|
||||||
|
ReportIcon,
|
||||||
|
SettingsIcon,
|
||||||
|
ShieldIcon,
|
||||||
|
WrenchIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import type { StoryObj } from '@storybook/vue3-vite'
|
||||||
|
import { defineComponent, h, ref } from 'vue'
|
||||||
|
|
||||||
|
import ButtonStyled from '../../components/base/ButtonStyled.vue'
|
||||||
|
import TabbedModal from '../../components/modal/TabbedModal.vue'
|
||||||
|
|
||||||
|
function makeTabContent(label: string, lines = 3) {
|
||||||
|
return defineComponent({
|
||||||
|
name: `${label}Tab`,
|
||||||
|
render() {
|
||||||
|
return h('div', { class: 'space-y-4 py-2' }, [
|
||||||
|
h('h2', { class: 'text-xl font-bold text-contrast m-0' }, label),
|
||||||
|
...Array.from({ length: lines }, (_, i) =>
|
||||||
|
h('p', { class: 'text-secondary m-0' }, `${label} content paragraph ${i + 1}.`),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Modal/TabbedModal',
|
||||||
|
// @ts-ignore
|
||||||
|
component: TabbedModal,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
|
||||||
|
export const Default: StoryObj = {
|
||||||
|
render: () => ({
|
||||||
|
components: { TabbedModal, ButtonStyled },
|
||||||
|
setup() {
|
||||||
|
const modalRef = ref<InstanceType<typeof TabbedModal> | null>(null)
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
name: { id: 'general', defaultMessage: 'General' },
|
||||||
|
icon: InfoIcon,
|
||||||
|
content: makeTabContent('General'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: { id: 'appearance', defaultMessage: 'Appearance' },
|
||||||
|
icon: PaintbrushIcon,
|
||||||
|
content: makeTabContent('Appearance'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: { id: 'privacy', defaultMessage: 'Privacy' },
|
||||||
|
icon: ShieldIcon,
|
||||||
|
content: makeTabContent('Privacy'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
return { modalRef, tabs }
|
||||||
|
},
|
||||||
|
template: /* html */ `
|
||||||
|
<div>
|
||||||
|
<ButtonStyled color="brand">
|
||||||
|
<button @click="modalRef?.show()">Open Tabbed Modal</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<TabbedModal ref="modalRef" header="Settings" :tabs="tabs" />
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithTitleSlot: StoryObj = {
|
||||||
|
render: () => ({
|
||||||
|
components: { TabbedModal, ButtonStyled, SettingsIcon },
|
||||||
|
setup() {
|
||||||
|
const modalRef = ref<InstanceType<typeof TabbedModal> | null>(null)
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
name: { id: 'general', defaultMessage: 'General' },
|
||||||
|
icon: InfoIcon,
|
||||||
|
content: makeTabContent('General'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: { id: 'appearance', defaultMessage: 'Appearance' },
|
||||||
|
icon: PaintbrushIcon,
|
||||||
|
content: makeTabContent('Appearance'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
return { modalRef, tabs }
|
||||||
|
},
|
||||||
|
template: /* html */ `
|
||||||
|
<div>
|
||||||
|
<ButtonStyled color="brand">
|
||||||
|
<button @click="modalRef?.show()">Open with Title Slot</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<TabbedModal ref="modalRef" :tabs="tabs">
|
||||||
|
<template #title>
|
||||||
|
<span class="flex items-center gap-2 text-lg font-extrabold text-contrast">
|
||||||
|
<SettingsIcon /> Custom Title
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</TabbedModal>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithFooter: StoryObj = {
|
||||||
|
render: () => ({
|
||||||
|
components: { TabbedModal, ButtonStyled },
|
||||||
|
setup() {
|
||||||
|
const modalRef = ref<InstanceType<typeof TabbedModal> | null>(null)
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
name: { id: 'general', defaultMessage: 'General' },
|
||||||
|
icon: InfoIcon,
|
||||||
|
content: makeTabContent('General'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: { id: 'appearance', defaultMessage: 'Appearance' },
|
||||||
|
icon: PaintbrushIcon,
|
||||||
|
content: makeTabContent('Appearance'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: { id: 'privacy', defaultMessage: 'Privacy' },
|
||||||
|
icon: ShieldIcon,
|
||||||
|
content: makeTabContent('Privacy'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
return { modalRef, tabs }
|
||||||
|
},
|
||||||
|
template: /* html */ `
|
||||||
|
<div>
|
||||||
|
<ButtonStyled color="brand">
|
||||||
|
<button @click="modalRef?.show()">Open with Footer</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<TabbedModal ref="modalRef" header="Settings" :tabs="tabs">
|
||||||
|
<template #footer>
|
||||||
|
<div class="mt-auto text-secondary text-sm">
|
||||||
|
<p class="m-0">App v1.0.0</p>
|
||||||
|
<p class="m-0">macOS 15.0</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TabbedModal>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithBadge: StoryObj = {
|
||||||
|
render: () => ({
|
||||||
|
components: { TabbedModal, ButtonStyled },
|
||||||
|
setup() {
|
||||||
|
const modalRef = ref<InstanceType<typeof TabbedModal> | null>(null)
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
name: { id: 'general', defaultMessage: 'General' },
|
||||||
|
icon: InfoIcon,
|
||||||
|
content: makeTabContent('General'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: { id: 'language', defaultMessage: 'Language' },
|
||||||
|
icon: LanguagesIcon,
|
||||||
|
content: makeTabContent('Language'),
|
||||||
|
badge: { id: 'beta', defaultMessage: 'Beta' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: { id: 'privacy', defaultMessage: 'Privacy' },
|
||||||
|
icon: ShieldIcon,
|
||||||
|
content: makeTabContent('Privacy'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
return { modalRef, tabs }
|
||||||
|
},
|
||||||
|
template: /* html */ `
|
||||||
|
<div>
|
||||||
|
<ButtonStyled color="brand">
|
||||||
|
<button @click="modalRef?.show()">Open with Badge</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<TabbedModal ref="modalRef" header="Settings" :tabs="tabs" />
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HiddenTabs: StoryObj = {
|
||||||
|
render: () => ({
|
||||||
|
components: { TabbedModal, ButtonStyled },
|
||||||
|
setup() {
|
||||||
|
const modalRef = ref<InstanceType<typeof TabbedModal> | null>(null)
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
name: { id: 'general', defaultMessage: 'General' },
|
||||||
|
icon: InfoIcon,
|
||||||
|
content: makeTabContent('General'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: { id: 'hidden', defaultMessage: 'Hidden Tab' },
|
||||||
|
icon: ReportIcon,
|
||||||
|
content: makeTabContent('Hidden'),
|
||||||
|
shown: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: { id: 'appearance', defaultMessage: 'Appearance' },
|
||||||
|
icon: PaintbrushIcon,
|
||||||
|
content: makeTabContent('Appearance'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
return { modalRef, tabs }
|
||||||
|
},
|
||||||
|
template: /* html */ `
|
||||||
|
<div>
|
||||||
|
<ButtonStyled color="brand">
|
||||||
|
<button @click="modalRef?.show()">Open with Hidden Tab</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<TabbedModal ref="modalRef" header="Settings" :tabs="tabs" />
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ManyTabs: StoryObj = {
|
||||||
|
render: () => ({
|
||||||
|
components: { TabbedModal, ButtonStyled },
|
||||||
|
setup() {
|
||||||
|
const modalRef = ref<InstanceType<typeof TabbedModal> | null>(null)
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
name: { id: 'general', defaultMessage: 'General' },
|
||||||
|
icon: InfoIcon,
|
||||||
|
content: makeTabContent('General'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: { id: 'appearance', defaultMessage: 'Appearance' },
|
||||||
|
icon: PaintbrushIcon,
|
||||||
|
content: makeTabContent('Appearance'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: { id: 'language', defaultMessage: 'Language' },
|
||||||
|
icon: LanguagesIcon,
|
||||||
|
content: makeTabContent('Language'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: { id: 'privacy', defaultMessage: 'Privacy' },
|
||||||
|
icon: ShieldIcon,
|
||||||
|
content: makeTabContent('Privacy'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: { id: 'java', defaultMessage: 'Java and memory' },
|
||||||
|
icon: CoffeeIcon,
|
||||||
|
content: makeTabContent('Java and memory'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: { id: 'instances', defaultMessage: 'Default instance options' },
|
||||||
|
icon: GameIcon,
|
||||||
|
content: makeTabContent('Default instance options'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: { id: 'resources', defaultMessage: 'Resource management' },
|
||||||
|
icon: GaugeIcon,
|
||||||
|
content: makeTabContent('Resource management'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: { id: 'window', defaultMessage: 'Window' },
|
||||||
|
icon: MonitorIcon,
|
||||||
|
content: makeTabContent('Window'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: { id: 'hooks', defaultMessage: 'Launch hooks' },
|
||||||
|
icon: WrenchIcon,
|
||||||
|
content: makeTabContent('Launch hooks'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
return { modalRef, tabs }
|
||||||
|
},
|
||||||
|
template: /* html */ `
|
||||||
|
<div>
|
||||||
|
<ButtonStyled color="brand">
|
||||||
|
<button @click="modalRef?.show()">Open with Many Tabs</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<TabbedModal ref="modalRef" header="Settings" :tabs="tabs" />
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ScrollableContent: StoryObj = {
|
||||||
|
render: () => ({
|
||||||
|
components: { TabbedModal, ButtonStyled },
|
||||||
|
setup() {
|
||||||
|
const modalRef = ref<InstanceType<typeof TabbedModal> | null>(null)
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
name: { id: 'long', defaultMessage: 'Long content' },
|
||||||
|
icon: InfoIcon,
|
||||||
|
content: makeTabContent('Long content', 30),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: { id: 'short', defaultMessage: 'Short content' },
|
||||||
|
icon: PaintbrushIcon,
|
||||||
|
content: makeTabContent('Short content', 2),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
return { modalRef, tabs }
|
||||||
|
},
|
||||||
|
template: /* html */ `
|
||||||
|
<div>
|
||||||
|
<ButtonStyled color="brand">
|
||||||
|
<button @click="modalRef?.show()">Open with Scrollable Content</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<TabbedModal ref="modalRef" header="Scrollable Demo" :tabs="tabs" />
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user