mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 03:55:59 +00:00
* pnpm prepr * feat(instance-sync): screenshots sync * fix: prepr + fmt * fix: qa * feat: screenshit editor * feat: screenshot* editor qa * fix: qa * fix: lint * fix: aecsocket rev * qa: a11y tab navigation focus is cut off * qa: Change “Edited” badge to be bg-highlight-green * qa: friends list input style wrong * qa: toolbar width + labels * qa: multiselect changes * qa: anim changes * qa: card hover colors * qa: copy feedback * qa: hide date * qa: instance icon in overflow/contextmenu * qa: spacing * qa: group empty state text * qa: drag preview badge * qa: group deletion skip warning if no screenshots * qa: redesign editor * qa: remove screenshot parent/edited badge stuff * qa: control font size consistency * qa: viewer copy control * qa: editor fixes * qa: redirect on disable screenshot sync * qa: margin police * fix: crop * fix: qa * feat: initial start on basic instance file syncing (#7220) * qa: final * qa: final 2 * fix: lint + prepr * fix: copy * fix: screenshot editing outside of app causing ghost files in db + sync fail rollback impl * chore: split up * fix: fmt --------- Co-authored-by: tdgao <mr.trumgao@gmail.com>
362 lines
9.8 KiB
Vue
362 lines
9.8 KiB
Vue
<script setup lang="ts">
|
|
import {
|
|
CoffeeIcon,
|
|
GaugeIcon,
|
|
HeartHandshakeIcon,
|
|
LanguagesIcon,
|
|
ModrinthIcon,
|
|
PaintbrushIcon,
|
|
RefreshCwIcon,
|
|
Settings2Icon,
|
|
ShieldIcon,
|
|
ToggleRightIcon,
|
|
UserIcon,
|
|
} from '@modrinth/assets'
|
|
import {
|
|
commonMessages,
|
|
commonSettingsMessages,
|
|
defineMessage,
|
|
defineMessages,
|
|
ProgressBar,
|
|
TabbedModal,
|
|
UnsavedChangesPopup,
|
|
useVIntl,
|
|
} from '@modrinth/ui'
|
|
import { getVersion } from '@tauri-apps/api/app'
|
|
import { platform as getOsPlatform, version as getOsVersion } from '@tauri-apps/plugin-os'
|
|
import { computed, provide, ref, watch } from 'vue'
|
|
|
|
import PrivacySettings from '@/components/ui/settings/account/PrivacySettings.vue'
|
|
import ProfileSettings from '@/components/ui/settings/account/ProfileSettings.vue'
|
|
import SocialSettings from '@/components/ui/settings/account/SocialSettings.vue'
|
|
import AppearanceSettings from '@/components/ui/settings/display/AppearanceSettings.vue'
|
|
import BehaviorSettings from '@/components/ui/settings/display/BehaviorSettings.vue'
|
|
import FeatureFlagSettings from '@/components/ui/settings/display/FeatureFlagSettings.vue'
|
|
import LanguageSettings from '@/components/ui/settings/display/LanguageSettings.vue'
|
|
import InstancesSyncedSettings from '@/components/ui/settings/instances/InstancesSyncedSettings.vue'
|
|
import JavaSettings from '@/components/ui/settings/instances/JavaSettings.vue'
|
|
import ResourceManagementSettings from '@/components/ui/settings/instances/ResourceManagementSettings.vue'
|
|
import { useAppSettings } from '@/composables/use-app-settings.ts'
|
|
import { get, set } from '@/helpers/settings.ts'
|
|
import {
|
|
appSettingsModalContextKey,
|
|
type UnsavedChangesController,
|
|
} from '@/providers/app-settings-modal'
|
|
import { injectAppUpdateDownloadProgress } from '@/providers/download-progress.ts'
|
|
|
|
// TODO: Apply COMPONENT_STRUCTURE.md here and extract out common setting option components
|
|
const appSettings = useAppSettings()
|
|
|
|
const { formatMessage } = useVIntl()
|
|
|
|
const devModeCounter = ref(0)
|
|
|
|
const developerModeEnabled = defineMessage({
|
|
id: 'app.settings.developer-mode-enabled',
|
|
defaultMessage: 'Developer mode enabled.',
|
|
})
|
|
|
|
const tabCategories = defineMessages({
|
|
display: {
|
|
id: 'settings.sidebar.label.display',
|
|
defaultMessage: 'Display',
|
|
},
|
|
account: {
|
|
id: 'settings.sidebar.label.account',
|
|
defaultMessage: 'Account',
|
|
},
|
|
instances: {
|
|
id: 'app.settings.sidebar.label.instances',
|
|
defaultMessage: 'Instances',
|
|
},
|
|
})
|
|
|
|
const tabs = [
|
|
{
|
|
name: defineMessage({
|
|
id: 'app.settings.tabs.appearance',
|
|
defaultMessage: 'Appearance',
|
|
}),
|
|
category: tabCategories.display,
|
|
icon: PaintbrushIcon,
|
|
content: AppearanceSettings,
|
|
},
|
|
{
|
|
name: defineMessage({
|
|
id: 'app.settings.tabs.behavior',
|
|
defaultMessage: 'Behavior',
|
|
}),
|
|
category: tabCategories.display,
|
|
icon: Settings2Icon,
|
|
content: BehaviorSettings,
|
|
},
|
|
{
|
|
name: defineMessage({
|
|
id: 'app.settings.tabs.language',
|
|
defaultMessage: 'Language',
|
|
}),
|
|
category: tabCategories.display,
|
|
icon: LanguagesIcon,
|
|
content: LanguageSettings,
|
|
badge: commonMessages.beta,
|
|
},
|
|
{
|
|
name: commonSettingsMessages.featureFlags,
|
|
category: tabCategories.display,
|
|
icon: ToggleRightIcon,
|
|
content: FeatureFlagSettings,
|
|
developerOnly: true,
|
|
},
|
|
{
|
|
name: commonSettingsMessages.profile,
|
|
category: tabCategories.account,
|
|
icon: UserIcon,
|
|
content: ProfileSettings,
|
|
},
|
|
{
|
|
name: commonSettingsMessages.social,
|
|
category: tabCategories.account,
|
|
icon: HeartHandshakeIcon,
|
|
content: SocialSettings,
|
|
},
|
|
{
|
|
name: defineMessage({
|
|
id: 'app.settings.tabs.privacy',
|
|
defaultMessage: 'Privacy',
|
|
}),
|
|
category: tabCategories.account,
|
|
icon: ShieldIcon,
|
|
content: PrivacySettings,
|
|
},
|
|
{
|
|
name: defineMessage({
|
|
id: 'app.settings.tabs.synced-options',
|
|
defaultMessage: 'Synced settings',
|
|
}),
|
|
category: tabCategories.instances,
|
|
icon: RefreshCwIcon,
|
|
content: InstancesSyncedSettings,
|
|
},
|
|
{
|
|
name: defineMessage({
|
|
id: 'app.settings.tabs.java-installations',
|
|
defaultMessage: 'Java installations',
|
|
}),
|
|
category: tabCategories.instances,
|
|
icon: CoffeeIcon,
|
|
content: JavaSettings,
|
|
},
|
|
{
|
|
name: defineMessage({
|
|
id: 'app.settings.tabs.resource-management',
|
|
defaultMessage: 'Resource management',
|
|
}),
|
|
category: tabCategories.instances,
|
|
icon: GaugeIcon,
|
|
content: ResourceManagementSettings,
|
|
},
|
|
]
|
|
|
|
const availableTabs = computed(() =>
|
|
tabs.filter((tab) => !tab.developerOnly || appSettings.devMode),
|
|
)
|
|
|
|
const modal = ref<InstanceType<typeof TabbedModal> | null>(null)
|
|
const unsavedChangesPopup = ref<{ nudge: () => void } | null>(null)
|
|
const unsavedChangesController = ref<UnsavedChangesController | null>(null)
|
|
const emptyUnsavedChangesState: Record<string, unknown> = {}
|
|
const originalUnsavedChangesState = computed(
|
|
() => unsavedChangesController.value?.getOriginal() ?? emptyUnsavedChangesState,
|
|
)
|
|
const modifiedUnsavedChangesState = computed(
|
|
() => unsavedChangesController.value?.getModified() ?? emptyUnsavedChangesState,
|
|
)
|
|
const savingUnsavedChanges = computed(() => unsavedChangesController.value?.isSaving() ?? false)
|
|
const hasUnsavedChanges = computed(
|
|
() =>
|
|
(unsavedChangesController.value?.hasChanges() ?? false) ||
|
|
(unsavedChangesController.value?.isSaving() ?? false),
|
|
)
|
|
|
|
function canLeaveCurrentTab(): boolean {
|
|
if (
|
|
!unsavedChangesController.value?.hasChanges() &&
|
|
!unsavedChangesController.value?.isSaving()
|
|
) {
|
|
return true
|
|
}
|
|
unsavedChangesPopup.value?.nudge()
|
|
return false
|
|
}
|
|
|
|
function close(): boolean {
|
|
return modal.value?.hide() ?? false
|
|
}
|
|
|
|
function registerUnsavedChangesController(controller: UnsavedChangesController | null): void {
|
|
unsavedChangesController.value = controller
|
|
}
|
|
|
|
provide(appSettingsModalContextKey, {
|
|
close,
|
|
registerUnsavedChangesController,
|
|
})
|
|
|
|
function resetUnsavedChanges(): void {
|
|
unsavedChangesController.value?.reset()
|
|
}
|
|
|
|
function saveUnsavedChanges(): void {
|
|
void unsavedChangesController.value?.save()
|
|
}
|
|
|
|
function show() {
|
|
modal.value?.show()
|
|
}
|
|
|
|
function showProfile(): void {
|
|
const profileTabIndex = availableTabs.value.findIndex((tab) => tab.content === ProfileSettings)
|
|
if (profileTabIndex >= 0) {
|
|
modal.value?.setTab(profileTabIndex)
|
|
}
|
|
modal.value?.show()
|
|
}
|
|
|
|
function showFeatureFlags(): void {
|
|
const featureFlagsTabIndex = availableTabs.value.findIndex(
|
|
(tab) => tab.content === FeatureFlagSettings,
|
|
)
|
|
if (featureFlagsTabIndex >= 0) {
|
|
modal.value?.setTab(featureFlagsTabIndex)
|
|
}
|
|
modal.value?.show()
|
|
}
|
|
|
|
function showSyncedOptions(): void {
|
|
const syncedOptionsTabIndex = availableTabs.value.findIndex(
|
|
(tab) => tab.content === InstancesSyncedSettings,
|
|
)
|
|
if (syncedOptionsTabIndex >= 0) {
|
|
modal.value?.setTab(syncedOptionsTabIndex)
|
|
}
|
|
modal.value?.show()
|
|
}
|
|
|
|
defineExpose({ show, showProfile, showFeatureFlags, showSyncedOptions })
|
|
|
|
const { progress, version: downloadingVersion } = injectAppUpdateDownloadProgress()
|
|
|
|
const version = await getVersion()
|
|
const osPlatform = getOsPlatform()
|
|
const osVersion = getOsVersion()
|
|
const settings = ref(await get())
|
|
|
|
watch(
|
|
settings,
|
|
async () => {
|
|
await set(settings.value)
|
|
},
|
|
{ deep: true },
|
|
)
|
|
|
|
function devModeCount() {
|
|
devModeCounter.value++
|
|
if (devModeCounter.value > 5) {
|
|
const selectedTab = modal.value ? availableTabs.value[modal.value.selectedTab] : undefined
|
|
|
|
appSettings.devMode = !appSettings.devMode
|
|
settings.value.developer_mode = !!appSettings.devMode
|
|
devModeCounter.value = 0
|
|
|
|
if (modal.value) {
|
|
const selectedTabIndex = selectedTab ? availableTabs.value.indexOf(selectedTab) : -1
|
|
modal.value.setTab(selectedTabIndex >= 0 ? selectedTabIndex : 0)
|
|
}
|
|
}
|
|
}
|
|
|
|
const messages = defineMessages({
|
|
downloading: {
|
|
id: 'app.settings.downloading',
|
|
defaultMessage: 'Downloading v{version}',
|
|
},
|
|
appVersion: {
|
|
id: 'app.settings.app-version',
|
|
defaultMessage: 'Modrinth App {version}',
|
|
},
|
|
macos: {
|
|
id: 'app.settings.operating-system.macos',
|
|
defaultMessage: 'macOS',
|
|
},
|
|
developerModeButtonLabel: {
|
|
id: 'app.settings.developer-mode-button.label',
|
|
defaultMessage: 'Toggle developer mode',
|
|
},
|
|
})
|
|
</script>
|
|
<template>
|
|
<TabbedModal
|
|
ref="modal"
|
|
:tabs="availableTabs"
|
|
:width="'min(928px, calc(95vw - 10rem))'"
|
|
:before-hide="canLeaveCurrentTab"
|
|
:before-tab-change="canLeaveCurrentTab"
|
|
:floating-action-bar-shown="hasUnsavedChanges"
|
|
>
|
|
<template #title>
|
|
<span class="text-2xl font-semibold text-contrast">
|
|
{{ formatMessage(commonMessages.settingsLabel) }}
|
|
</span>
|
|
</template>
|
|
<template #floating-action-bar>
|
|
<UnsavedChangesPopup
|
|
ref="unsavedChangesPopup"
|
|
:original="originalUnsavedChangesState"
|
|
:modified="modifiedUnsavedChangesState"
|
|
:saving="savingUnsavedChanges"
|
|
inline
|
|
@reset="resetUnsavedChanges"
|
|
@save="saveUnsavedChanges"
|
|
/>
|
|
</template>
|
|
<template #footer>
|
|
<div class="mt-auto text-secondary text-sm">
|
|
<div class="mb-3">
|
|
<template v-if="progress > 0 && progress < 1">
|
|
<p class="m-0 mb-2">
|
|
{{ formatMessage(messages.downloading, { version: downloadingVersion }) }}
|
|
</p>
|
|
<ProgressBar :progress="progress" />
|
|
</template>
|
|
</div>
|
|
<p v-if="appSettings.devMode" class="text-brand font-semibold m-0 mb-2">
|
|
{{ formatMessage(developerModeEnabled) }}
|
|
</p>
|
|
<div class="flex items-center gap-3">
|
|
<button
|
|
:aria-label="formatMessage(messages.developerModeButtonLabel)"
|
|
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
|
|
:class="{
|
|
'text-brand': appSettings.devMode,
|
|
'text-secondary': !appSettings.devMode,
|
|
}"
|
|
@click="devModeCount"
|
|
>
|
|
<ModrinthIcon aria-hidden="true" class="w-6 h-6" />
|
|
</button>
|
|
<div class="max-w-[200px]">
|
|
<p class="m-0">
|
|
{{ formatMessage(messages.appVersion, { version }) }}
|
|
</p>
|
|
<p class="m-0">
|
|
<span v-if="osPlatform === 'macos'">{{ formatMessage(messages.macos) }}</span>
|
|
<span v-else class="capitalize">{{ osPlatform }}</span>
|
|
{{ osVersion }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</TabbedModal>
|
|
</template>
|