diff --git a/.github/workflows/changelog-comment.yml b/.github/workflows/changelog-comment.yml index e9e440e..33dd202 100644 --- a/.github/workflows/changelog-comment.yml +++ b/.github/workflows/changelog-comment.yml @@ -19,6 +19,11 @@ jobs: name: Post changelog comment runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: read + contents: read + steps: - name: Post or update changelog comment uses: actions/github-script@d746ffe35508b1917358783b479e04febd2b8f71 # v9.0.0 diff --git a/Cargo.lock b/Cargo.lock index 5354349..4d43cbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1889,6 +1889,28 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree", +] + +[[package]] +name = "fontdb" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2660c5e9157bf76d2db1294e4a9feba604ef610819a3b591088d0d8392a3290f" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -3477,6 +3499,15 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.9.1" @@ -5102,6 +5133,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + [[package]] name = "rsa" version = "0.9.10" @@ -5758,6 +5795,15 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.15.2" @@ -6912,6 +6958,7 @@ dependencies = [ "either", "enumset", "flate2", + "fontdb", "futures", "hyper", "hyper-util", @@ -6926,6 +6973,7 @@ dependencies = [ "path-util", "quartz_nbt", "regex", + "semver", "serde", "serde_json", "serde_with", diff --git a/Cargo.toml b/Cargo.toml index 2293561..6a7a81b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ encoding_rs = "0.8.35" enumset = "1.1.10" eyre = "0.6.12" flate2 = "1.1.4" +fontdb = "0.24.0" fs4 = { version = "0.13.1", default-features = false } futures = "0.3.31" futures-lite = "2.6.1" @@ -83,6 +84,7 @@ rgb = "0.8.52" rustls = { version = "0.23.42", default-features = false, features = ["ring", "std", "tls12"] } rustls-native-certs = "0.8.4" rustls-pki-types = "1.15.0" +semver = "1.0.28" serde = "1.0.228" serde_bytes = "0.11.19" serde_cbor = "0.11.2" diff --git a/apps/app-frontend/src/components/ui/instance_settings/BackupExclusionSelector.vue b/apps/app-frontend/src/components/ui/instance_settings/BackupExclusionSelector.vue new file mode 100644 index 0000000..7544988 --- /dev/null +++ b/apps/app-frontend/src/components/ui/instance_settings/BackupExclusionSelector.vue @@ -0,0 +1,132 @@ + + + diff --git a/apps/app-frontend/src/components/ui/instance_settings/BackupSettings.vue b/apps/app-frontend/src/components/ui/instance_settings/BackupSettings.vue new file mode 100644 index 0000000..5299c11 --- /dev/null +++ b/apps/app-frontend/src/components/ui/instance_settings/BackupSettings.vue @@ -0,0 +1,675 @@ + + + diff --git a/apps/app-frontend/src/components/ui/modal/InstanceSettingsModal.vue b/apps/app-frontend/src/components/ui/modal/InstanceSettingsModal.vue index b5869c2..d7fa727 100644 --- a/apps/app-frontend/src/components/ui/modal/InstanceSettingsModal.vue +++ b/apps/app-frontend/src/components/ui/modal/InstanceSettingsModal.vue @@ -20,6 +20,7 @@ import type { PlatformTag } from '@modrinth/utils' import { useQuery, useQueryClient } from '@tanstack/vue-query' import { computed, nextTick, ref, watch } from 'vue' +import BackupSettings from '@/components/ui/instance_settings/BackupSettings.vue' import CoreComponentsSettings from '@/components/ui/instance_settings/CoreComponentsSettings.vue' import GeneralSettings from '@/components/ui/instance_settings/GeneralSettings.vue' import HooksSettings from '@/components/ui/instance_settings/HooksSettings.vue' @@ -105,6 +106,14 @@ const tabs = computed(() => [ icon: WrenchIcon, content: InstallationSettings, }, + { + name: defineMessage({ + id: 'instance.settings.tabs.backups', + defaultMessage: 'Backups', + }), + icon: FileArchiveIcon, + content: BackupSettings, + }, { // Core component editing is instance-specific and advanced, so it is intentionally excluded from first-run onboarding. name: defineMessage({ diff --git a/apps/app-frontend/src/components/ui/settings/AppearanceSettings.vue b/apps/app-frontend/src/components/ui/settings/AppearanceSettings.vue index 0d4089e..279ed0e 100644 --- a/apps/app-frontend/src/components/ui/settings/AppearanceSettings.vue +++ b/apps/app-frontend/src/components/ui/settings/AppearanceSettings.vue @@ -22,7 +22,7 @@ import { convertFileSrc } from '@tauri-apps/api/core' import { appDataDir, join } from '@tauri-apps/api/path' import { open } from '@tauri-apps/plugin-dialog' import { exists, mkdir, readFile, remove, writeFile } from '@tauri-apps/plugin-fs' -import { computed, ref, watch } from 'vue' +import { computed, onMounted, ref, watch } from 'vue' import { get, set } from '@/helpers/settings.ts' import { getOS } from '@/helpers/utils' @@ -41,6 +41,19 @@ import { parseCustomAccentColor, } from '@/store/theme.ts' +import { + buildFontFamilyOptions, + canonicalFontFamily, + DEFAULT_MONO_FONT_STACK, + DEFAULT_UI_FONT_STACK, + type FontFamilyOption, + type FontFamilyOptionOrDivider, + optionValueToFontSetting, + resolveFontFamily, + type SystemFontFamily, +} from '@/helpers/font-family.ts' +import { getSystemFontFamilies } from '@/helpers/system-fonts.ts' + import SettingsRow from './SettingsRow.vue' import SettingsSection from './SettingsSection.vue' @@ -363,6 +376,63 @@ const messages = defineMessages({ defaultMessage: 'Hide the downloads button in the sidebar when there are no active download tasks.', }, + fontsTitle: { id: 'app.appearance-settings.fonts.title', defaultMessage: 'Fonts' }, + fontsDescription: { + id: 'app.appearance-settings.fonts.description', + defaultMessage: 'Choose the fonts the launcher interface and its logs are rendered in.', + }, + uiFontTitle: { id: 'app.appearance-settings.fonts.ui-font', defaultMessage: 'Interface font' }, + uiFontDescription: { + id: 'app.appearance-settings.fonts.ui-font-description', + defaultMessage: + 'Used across the launcher interface. Only fonts installed on this system are listed.', + }, + monoFontTitle: { + id: 'app.appearance-settings.fonts.mono-font', + defaultMessage: 'Monospace font', + }, + monoFontDescription: { + id: 'app.appearance-settings.fonts.mono-font-description', + defaultMessage: 'Used for logs, the server console, and other monospace content.', + }, + fontDefault: { + id: 'app.appearance-settings.fonts.default', + defaultMessage: 'Launcher default', + }, + fontMonospace: { + id: 'app.appearance-settings.fonts.monospace', + defaultMessage: 'Monospace', + }, + fontMissing: { + id: 'app.appearance-settings.fonts.missing', + defaultMessage: 'Not installed', + }, + fontSearchPlaceholder: { + id: 'app.appearance-settings.fonts.search-placeholder', + defaultMessage: 'Search installed fonts', + }, + fontNoResults: { + id: 'app.appearance-settings.fonts.no-results', + defaultMessage: 'No matching fonts', + }, + fontLoadFailed: { + id: 'app.appearance-settings.fonts.load-failed', + defaultMessage: "Couldn't read the list of installed fonts.", + }, + fontRetry: { id: 'app.appearance-settings.fonts.retry', defaultMessage: 'Retry' }, + fontReset: { + id: 'app.appearance-settings.fonts.reset', + defaultMessage: 'Reset', + }, + fontPreviewLabel: { id: 'app.appearance-settings.fonts.preview', defaultMessage: 'Preview' }, + fontUiPreviewSample: { + id: 'app.appearance-settings.fonts.ui-preview-sample', + defaultMessage: 'The quick brown fox jumps over the lazy dog.', + }, + fontMonoPreviewSample: { + id: 'app.appearance-settings.fonts.mono-preview-sample', + defaultMessage: 'const x = () => 0; // monospace', + }, }) const os = ref(await getOS()) @@ -373,6 +443,78 @@ const customBackgroundPreview = computed(() => : null, ) +const systemFonts = ref([]) +const systemFontsLoading = ref(true) +const systemFontsFailed = ref(false) + +async function loadSystemFonts() { + systemFontsLoading.value = true + systemFontsFailed.value = false + + try { + systemFonts.value = await getSystemFontFamilies() + } catch (error) { + systemFontsFailed.value = true + console.warn('Failed to read the list of installed fonts', error) + } finally { + systemFontsLoading.value = false + } +} + +function selectedOptionLabel(options: FontFamilyOptionOrDivider[], value: string): string { + const option = options.find( + (entry): entry is FontFamilyOption => 'value' in entry && entry.value === value, + ) + + return option?.label ?? '' +} + +const uiFontOptions = computed(() => + buildFontFamilyOptions(systemFonts.value, { + defaultLabel: formatMessage(messages.fontDefault), + missingLabel: formatMessage(messages.fontMissing), + selected: settings.value.ui_font, + }), +) + +const monoFontOptions = computed(() => + buildFontFamilyOptions(systemFonts.value, { + defaultLabel: formatMessage(messages.fontDefault), + missingLabel: formatMessage(messages.fontMissing), + monospaceLabel: formatMessage(messages.fontMonospace), + selected: settings.value.mono_font, + groupMonospaced: true, + }), +) + +const uiFontSelection = computed({ + get: () => canonicalFontFamily(systemFonts.value, settings.value.ui_font), + set: (value: string) => { + settings.value.ui_font = optionValueToFontSetting(value) + }, +}) + +const monoFontSelection = computed({ + get: () => canonicalFontFamily(systemFonts.value, settings.value.mono_font), + set: (value: string) => { + settings.value.mono_font = optionValueToFontSetting(value) + }, +}) + +const uiFontSearchValue = computed(() => + selectedOptionLabel(uiFontOptions.value, uiFontSelection.value), +) +const monoFontSearchValue = computed(() => + selectedOptionLabel(monoFontOptions.value, monoFontSelection.value), +) + +const uiFontPreview = computed(() => + resolveFontFamily(settings.value.ui_font, DEFAULT_UI_FONT_STACK), +) +const monoFontPreview = computed(() => + resolveFontFamily(settings.value.mono_font, DEFAULT_MONO_FONT_STACK), +) + function updateUiScale(value: number) { const clamped = Math.min(200, Math.max(50, Math.round(value))) setUiScale(clamped) @@ -489,6 +631,8 @@ watch( settings.value.transparent_background_blur, settings.value.sidebar_instance_count, settings.value.close_behavior, + settings.value.ui_font, + settings.value.mono_font, ] as const, ([ path, @@ -499,6 +643,8 @@ watch( transparentBlur, sidebarInstanceCount, closeBehavior, + uiFont, + monoFont, ]) => { themeStore.customBackgroundPath = path themeStore.customBackgroundBlur = blur @@ -509,10 +655,19 @@ watch( themeStore.setTransparentBackgroundClass() themeStore.sidebarInstanceCount = sidebarInstanceCount themeStore.closeBehavior = closeBehavior as CloseBehavior + themeStore.uiFont = uiFont + themeStore.monoFont = monoFont + themeStore.setUiFont() + themeStore.setMonoFont() }, { immediate: true }, ) +onMounted(() => { + if (props.scope === 'interface') { + void loadSystemFonts() + } +}) watch( settings, async () => { @@ -1148,6 +1303,118 @@ watch( + + + + + + + + + + + + +
+ {{ formatMessage(messages.fontLoadFailed) }} + +
+
+