diff --git a/README.md b/README.md index f32570c..4f78155 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ works. | `0013-Explain-what-went-wrong-...` | The Logs tab says what a crash was and offers a fix where there is one, without a connection. | | `0014-Browse-skins-from-...` | A Browse tab on the skin page: Ely.by's catalogue in the app, and NameMC, laby.net and crafty.gg in a window. | | `0015-Use-the-desktop-s-file-picker-...` | File pickers on Linux are the desktop's own, such as KDE's, through the XDG desktop portal. | +| `0016-Show-the-account-in-the-title-bar-...` | With the right sidebar folded away, the Minecraft account is shown in the title bar and managed from there. | ### Offline accounts @@ -49,11 +50,11 @@ Microsoft sign-in opens your own browser rather than a webview inside the launch password manager, autofill and passkeys work, and you can see in the address bar that the page is really Microsoft's. -Microsoft cannot hand the result back: the client id the launcher uses is Minecraft's own, whose -only registered redirect is a fixed page on `login.live.com`, with no loopback address for the -launcher to listen on. So the browser lands on that page with the code in the address and you copy -the address into the launcher. The webview is still one click away for anyone the browser does not -work out for. +The client id the launcher uses is Minecraft's own, with no redirect the launcher could listen on. +So the browser signs in on Microsoft's device code page, with the code already filled in, while the +launcher asks Microsoft every few seconds whether that has happened. Once it has, the account is +added and the launcher comes back to the front, without anything to paste. The window inside the +launcher is still one click away in the dialog, for when the browser does not work out. ### Ely.by accounts @@ -80,8 +81,10 @@ Settings > Features > Sidebar. The news section in the right sidebar folds away by clicking its heading. The right sidebar itself folds away with the arrow button in the title bar, which upstream only shows once "Hide right sidebar" is turned on in settings. Both remember what they were set to across restarts, and the -title bar button briefly shows a check once its state is saved. On pages that need the sidebar, such -as the mod browser, the button stays in place, greyed out. +title bar button briefly shows a check once its state is saved; the first time, a short note +explains this. On pages that need the sidebar, such as the mod browser, the button stays in place, +greyed out. While the sidebar is folded away, the +Minecraft account sits in the title bar next to the window buttons, to switch, add or remove accounts. None of the three reach Modrinth. Preference syncing maps a fixed list of named fields in both directions and these are not in it, so they are neither sent to your Modrinth account nor diff --git a/patches/0005-Make-the-sidebars-foldable.patch b/patches/0005-Make-the-sidebars-foldable.patch index a9793da..25e21cb 100644 --- a/patches/0005-Make-the-sidebars-foldable.patch +++ b/patches/0005-Make-the-sidebars-foldable.patch @@ -30,14 +30,14 @@ page headed "Sync features across devices" and would otherwise look like it syncs. The other two are not settings entries - a heading and a title bar button - so nothing there promises anything. --- - apps/app-frontend/src/App.vue | 153 ++++++++++++++++-- - .../ui/settings/display/FeaturesSettings.vue | 28 ++++ - .../src/composables/use-app-settings.ts | 3 + - packages/app-lib/src/state/settings.rs | 3 + - 4 files changed, 172 insertions(+), 15 deletions(-) + apps/app-frontend/src/App.vue | 184 ++++++++++++++++-- + .../ui/settings/display/FeaturesSettings.vue | 28 +++ + .../src/composables/use-app-settings.ts | 4 + + packages/app-lib/src/state/settings.rs | 4 + + 4 files changed, 205 insertions(+), 15 deletions(-) diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue -index d750a5f..ac8308c 100644 +index d750a5f..c47484d 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -10,6 +10,8 @@ import { @@ -49,7 +49,23 @@ index d750a5f..ac8308c 100644 ChevronLeftIcon, ChevronRightIcon, CompassIcon, -@@ -254,12 +256,41 @@ const credentials = ref() +@@ -34,6 +36,7 @@ import { + AccountSwitchOverlay, + Admonition, + Avatar, ++ Button, + ButtonLink, + commonMessages, + commonSettingsMessages, +@@ -44,6 +47,7 @@ import { + I18nDebugPanel, + IconButton, + LoadingBar, ++ NewModal, + NewsArticleCard, + NotificationPanel, + PopupNotificationPanel, +@@ -254,12 +258,46 @@ const credentials = ref() const storedModrinthAccounts = ref([]) let credentialsRefreshId = 0 const sidebarToggled = ref(true) @@ -72,29 +88,34 @@ index d750a5f..ac8308c 100644 + (hide) => setSidebarCollapsed(hide), ) + -+// Briefly shows a check on the fold button once the state is saved, so it is -+// clear the choice is remembered. ++// Briefly shows a check on the fold button once the state is saved, and the ++// first time also explains that the choice is remembered. +const sidebarSaved = ref(false) +let sidebarSavedTimeout ++const sidebarRememberedModal = ref(null) + +function setSidebarCollapsed(collapsed) { ++ const explain = !appSettings.featureFlags.sidebar_fold_explained + appSettings.featureFlags.right_sidebar_collapsed = collapsed ++ appSettings.featureFlags.sidebar_fold_explained = true + getSettings() + .then((settings) => { + settings.feature_flags.right_sidebar_collapsed = collapsed ++ settings.feature_flags.sidebar_fold_explained = true + return setSettings(settings) + }) + .then(() => { + sidebarSaved.value = true + clearTimeout(sidebarSavedTimeout) + sidebarSavedTimeout = setTimeout(() => (sidebarSaved.value = false), 1600) ++ if (explain) sidebarRememberedModal.value?.show() + }) + .catch(handleError) +} const forceSidebar = computed( () => route.path.startsWith('/browse') || -@@ -358,6 +389,21 @@ useQuery({ +@@ -358,6 +396,21 @@ useQuery({ // ad and the gradient being drawn above it. const showAd = computed(() => false) const adConsentAvailable = computed(() => false) @@ -116,15 +137,24 @@ index d750a5f..ac8308c 100644 providePageContext({ hierarchicalSidebarAvailable: ref(true), showAds: showAd, -@@ -588,6 +634,16 @@ const messages = defineMessages({ +@@ -588,6 +641,25 @@ const messages = defineMessages({ goBack: { id: 'app.navigation.go-back', defaultMessage: 'Go back' }, goForward: { id: 'app.navigation.go-forward', defaultMessage: 'Go forward' }, nextImage: { id: 'app.navigation.next-image', defaultMessage: 'Next image' }, + hideSidebar: { id: 'app.navigation.hide-sidebar', defaultMessage: 'Hide sidebar' }, + showSidebar: { id: 'app.navigation.show-sidebar', defaultMessage: 'Show sidebar' }, -+ sidebarRemembered: { -+ id: 'app.navigation.sidebar-remembered', -+ defaultMessage: 'Remembered after restart', ++ sidebarRememberedTitle: { ++ id: 'app.navigation.sidebar-remembered.title', ++ defaultMessage: 'The sidebar stays how you leave it', ++ }, ++ sidebarRememberedBody: { ++ id: 'app.navigation.sidebar-remembered.body', ++ defaultMessage: ++ 'Folding the sidebar away or bringing it back is saved right away, so the app opens the same way next time, even after a restart. The check on the button shows when it has been saved.', ++ }, ++ sidebarRememberedDone: { ++ id: 'app.navigation.sidebar-remembered.done', ++ defaultMessage: 'Done', + }, + sidebarRequired: { + id: 'app.navigation.sidebar-required', @@ -133,7 +163,7 @@ index d750a5f..ac8308c 100644 updateDownloadMissingVersion: { id: 'app.update.download-error.missing-version', defaultMessage: 'Failed to download update: no version available', -@@ -2252,6 +2308,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload) +@@ -2252,6 +2324,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
@@ -162,11 +192,9 @@ index d750a5f..ac8308c 100644 + formatMessage( + forceSidebar + ? messages.sidebarRequired -+ : sidebarSaved -+ ? messages.sidebarRemembered -+ : sidebarToggled -+ ? messages.hideSidebar -+ : messages.showSidebar, ++ : sidebarToggled ++ ? messages.hideSidebar ++ : messages.showSidebar, + ) + " + class="relative mr-3 flex" @@ -195,7 +223,7 @@ index d750a5f..ac8308c 100644
-@@ -2502,10 +2587,18 @@ provideAppUpdateDownloadProgress(appUpdateDownload) +@@ -2502,10 +2601,18 @@ provideAppUpdateDownloadProgress(appUpdateDownload) class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid" />
@@ -217,7 +245,29 @@ index d750a5f..ac8308c 100644 + + ++ ++

{{ formatMessage(messages.sidebarRememberedBody) }}

++ ++
+ + + +@@ -2743,6 +2865,38 @@ provideAppUpdateDownloadProgress(appUpdateDownload) display: contents; } @@ -238,9 +288,11 @@ index d750a5f..ac8308c 100644 + opacity: 0; + } + ++ /* The path runs from the long stroke's tip back to the short one, so it is ++ revealed from its end: the check is drawn left to right. */ + .sidebar-saved-badge :deep(path) { -+ stroke-dasharray: 20; -+ stroke-dashoffset: 20; ++ stroke-dasharray: 24; ++ stroke-dashoffset: -24; + animation: sidebar-saved-draw 0.3s 0.15s ease-out forwards; + } +} @@ -336,29 +388,31 @@ index 1d3e379..ae88550 100644

{{ formatMessage(messages.quickInstancesTitle) }} diff --git a/apps/app-frontend/src/composables/use-app-settings.ts b/apps/app-frontend/src/composables/use-app-settings.ts -index 0d58903..59da62b 100644 +index 0d58903..28c78c3 100644 --- a/apps/app-frontend/src/composables/use-app-settings.ts +++ b/apps/app-frontend/src/composables/use-app-settings.ts -@@ -24,6 +24,9 @@ export const DEFAULT_FEATURE_FLAGS = { +@@ -24,6 +24,10 @@ export const DEFAULT_FEATURE_FLAGS = { friends_pending_collapsed: true, dismissed_photosensitivity_filter_warning: false, localhost_sign_in: false, + show_hosting_in_sidebar: true, + news_collapsed: false, ++ sidebar_fold_explained: false, + right_sidebar_collapsed: false, } export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS diff --git a/packages/app-lib/src/state/settings.rs b/packages/app-lib/src/state/settings.rs -index 20601b8..2fd0d87 100644 +index 20601b8..b5f2ec6 100644 --- a/packages/app-lib/src/state/settings.rs +++ b/packages/app-lib/src/state/settings.rs -@@ -88,6 +88,9 @@ pub enum FeatureFlag { +@@ -88,6 +88,10 @@ pub enum FeatureFlag { FriendsPendingCollapsed, DismissedPhotosensitivityFilterWarning, LocalhostSignIn, + ShowHostingInSidebar, + NewsCollapsed, ++ SidebarFoldExplained, + RightSidebarCollapsed, } diff --git a/patches/0007-Sign-in-to-Microsoft-in-the-player-s-own-browser.patch b/patches/0007-Sign-in-to-Microsoft-in-the-player-s-own-browser.patch index dad0a69..fb39349 100644 --- a/patches/0007-Sign-in-to-Microsoft-in-the-player-s-own-browser.patch +++ b/patches/0007-Sign-in-to-Microsoft-in-the-player-s-own-browser.patch @@ -8,30 +8,30 @@ which is the one place a player cannot use the tools they use everywhere else: no password manager, no autofill, no passkeys, and no way to tell by looking that the page is really Microsoft's. -The browser gets all of it. What it cannot do is hand the code back: -this client id is Minecraft's own, its only registered redirect is a -fixed page on login.live.com, and no loopback address is registered for -it, so there is nothing for the launcher to listen on. The browser -therefore lands on that page with the code in the address, and the -player copies the address over - which is what the new modal asks for, -and what `login_browser_finish` reads the code out of. +The browser gets all of it. What it cannot do is hand a code back: this +client id is Minecraft's own, and no loopback address is registered for +it. So the browser signs in on Microsoft's device code page, with the +code already filled in, while the launcher polls for the result and +comes back to the front once it is there. Every entry point goes through it, since they all end up at `AccountsCard.login()`. The webview is still one click away in that modal for anyone the browser does not work out for. --- - .../src/components/ui/AccountsCard.vue | 17 +- - .../src/components/ui/MicrosoftLoginModal.vue | 213 ++++++++++++++++++ + .../src/components/ui/AccountsCard.vue | 21 +- + .../src/components/ui/MicrosoftLoginModal.vue | 219 ++++++++++++++++++ .../MinecraftRequiredModal.vue | 29 +-- - apps/app-frontend/src/helpers/auth.js | 25 ++ + apps/app-frontend/src/helpers/auth.js | 23 ++ apps/app/build.rs | 2 + - apps/app/src/api/auth.rs | 77 +++++++ + apps/app/src/api/auth.rs | 64 +++++ + packages/app-lib/src/api/minecraft_auth.rs | 30 +++ packages/app-lib/src/api/mod.rs | 12 +- - 7 files changed, 333 insertions(+), 42 deletions(-) + packages/app-lib/src/state/minecraft_auth.rs | 145 +++++++++++- + 9 files changed, 499 insertions(+), 46 deletions(-) create mode 100644 apps/app-frontend/src/components/ui/MicrosoftLoginModal.vue diff --git a/apps/app-frontend/src/components/ui/AccountsCard.vue b/apps/app-frontend/src/components/ui/AccountsCard.vue -index 35c21fa..c81603c 100644 +index 35c21fa..224d776 100644 --- a/apps/app-frontend/src/components/ui/AccountsCard.vue +++ b/apps/app-frontend/src/components/ui/AccountsCard.vue @@ -105,6 +105,7 @@ @@ -84,46 +84,64 @@ index 35c21fa..c81603c 100644 } async function accountAdded() { +@@ -316,8 +309,8 @@ const messages = defineMessages({ + defaultMessage: 'Not signed in', + }, + addAccount: { +- id: 'minecraft-account.add-account', +- defaultMessage: 'Add account', ++ id: 'minecraft-account.add-microsoft-account', ++ defaultMessage: 'Add Microsoft account', + }, + addOfflineAccount: { + id: 'minecraft-account.add-offline-account', diff --git a/apps/app-frontend/src/components/ui/MicrosoftLoginModal.vue b/apps/app-frontend/src/components/ui/MicrosoftLoginModal.vue new file mode 100644 -index 0000000..283b89c +index 0000000..d587736 --- /dev/null +++ b/apps/app-frontend/src/components/ui/MicrosoftLoginModal.vue -@@ -0,0 +1,213 @@ +@@ -0,0 +1,219 @@ + + + + + diff --git a/apps/app/build.rs b/apps/app/build.rs -index fc78fe1..4553d48 100644 +index f98395e..0fb53a5 100644 --- a/apps/app/build.rs +++ b/apps/app/build.rs @@ -44,6 +44,17 @@ fn main() { @@ -2075,7 +2075,7 @@ index 0000000..9a6f7e8 + } +} diff --git a/packages/app-lib/src/api/mod.rs b/packages/app-lib/src/api/mod.rs -index ade3ca8..566810b 100644 +index a175675..4daf723 100644 --- a/packages/app-lib/src/api/mod.rs +++ b/packages/app-lib/src/api/mod.rs @@ -1,5 +1,6 @@ diff --git a/patches/0014-Browse-skins-from-Ely.by-NameMC-laby.net-and-crafty..patch b/patches/0014-Browse-skins-from-Ely.by-NameMC-laby.net-and-crafty..patch index 937fd74..2f024ef 100644 --- a/patches/0014-Browse-skins-from-Ely.by-NameMC-laby.net-and-crafty..patch +++ b/patches/0014-Browse-skins-from-Ely.by-NameMC-laby.net-and-crafty..patch @@ -976,7 +976,7 @@ index bcd79ec..9a10f0e 100644

diff --git a/apps/app/build.rs b/apps/app/build.rs -index 4553d48..90e22ef 100644 +index 0fb53a5..f01ba91 100644 --- a/apps/app/build.rs +++ b/apps/app/build.rs @@ -44,6 +44,19 @@ fn main() { @@ -1165,7 +1165,7 @@ index 8429e4c..04eedbd 100644 .plugin(api::onboarding_checklist::init()) .plugin(api::import::init()) diff --git a/packages/app-lib/src/api/mod.rs b/packages/app-lib/src/api/mod.rs -index 566810b..a6f770d 100644 +index 4daf723..e64a606 100644 --- a/packages/app-lib/src/api/mod.rs +++ b/packages/app-lib/src/api/mod.rs @@ -17,6 +17,7 @@ pub mod process; diff --git a/patches/0016-Show-the-account-in-the-title-bar-when-the-sidebar-i.patch b/patches/0016-Show-the-account-in-the-title-bar-when-the-sidebar-i.patch new file mode 100644 index 0000000..e1494a6 --- /dev/null +++ b/patches/0016-Show-the-account-in-the-title-bar-when-the-sidebar-i.patch @@ -0,0 +1,268 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Modrinth Enhanced +Date: Tue, 15 Sep 2026 15:14:28 +0200 +Subject: [PATCH] Show the account in the title bar when the sidebar is folded + +The Minecraft account lives in the right sidebar, so folding the sidebar +away took with it any way to see which account is selected or to switch +to another. While the sidebar is folded, the account now sits in the +title bar next to the window buttons: its head and name, opening a menu +to switch accounts, add one or remove one. + +The menu stands in for the sidebar's account card rather than being a +second one. That card stays mounted while the sidebar is hidden, so the +selection, the avatar and the sign-in dialogs are the card's own and +stay in step with it, and a dialog opened from the menu outlives the +menu closing. +--- + apps/app-frontend/src/App.vue | 8 + + .../src/components/ui/AccountsCard.vue | 32 ++-- + .../components/ui/TitleBarAccountSwitcher.vue | 163 ++++++++++++++++++ + 3 files changed, 191 insertions(+), 12 deletions(-) + create mode 100644 apps/app-frontend/src/components/ui/TitleBarAccountSwitcher.vue + +diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue +index 0ffddff..5cbe7d8 100644 +--- a/apps/app-frontend/src/App.vue ++++ b/apps/app-frontend/src/App.vue +@@ -79,6 +79,7 @@ import { computed, nextTick, onMounted, onUnmounted, provide, ref, watch } from + import { RouterView, useRoute, useRouter } from 'vue-router' + + import AccountsCard from '@/components/ui/AccountsCard.vue' ++import TitleBarAccountSwitcher from '@/components/ui/TitleBarAccountSwitcher.vue' + import AppActionBar from '@/components/ui/AppActionBar.vue' + import Breadcrumbs from '@/components/ui/Breadcrumbs.vue' + import ErrorModal from '@/components/ui/ErrorModal.vue' +@@ -2509,6 +2510,13 @@ provideAppUpdateDownloadProgress(appUpdateDownload) + +
+
++ ++ ++ + +
+ +diff --git a/apps/app-frontend/src/components/ui/AccountsCard.vue b/apps/app-frontend/src/components/ui/AccountsCard.vue +index 224d776..632483e 100644 +--- a/apps/app-frontend/src/components/ui/AccountsCard.vue ++++ b/apps/app-frontend/src/components/ui/AccountsCard.vue +@@ -226,18 +226,6 @@ function showElyAccountModal(event?: MouseEvent) { + elyAccountModal.value?.show(event) + } + +-defineExpose({ +- refreshValues, +- showOfflineAccountModal, +- showElyAccountModal, +- setEquippedSkin, +- setLoginDisabled, +- login, +- loginDisabled, +-}) +- +-await refreshValues() +- + const selectedAccount = computed(() => + accounts.value.find((account) => account.profile.id === defaultUser.value), + ) +@@ -256,6 +244,26 @@ const avatarUrl = computed(() => { + return 'https://launcher-files.modrinth.com/assets/steve_head.png' + }) + ++defineExpose({ ++ refreshValues, ++ showOfflineAccountModal, ++ showElyAccountModal, ++ setEquippedSkin, ++ setLoginDisabled, ++ login, ++ loginDisabled, ++ // For the title bar's account switcher, which stands in for this card while ++ // the sidebar is folded away. ++ accounts, ++ selectedAccount, ++ avatarUrl, ++ getAccountAvatarUrl, ++ setAccount, ++ logout, ++}) ++ ++await refreshValues() ++ + function getAccountAvatarUrl(account: MinecraftCredential) { + if ( + account.profile.id === selectedAccount.value?.profile?.id && +diff --git a/apps/app-frontend/src/components/ui/TitleBarAccountSwitcher.vue b/apps/app-frontend/src/components/ui/TitleBarAccountSwitcher.vue +new file mode 100644 +index 0000000..70ab9e9 +--- /dev/null ++++ b/apps/app-frontend/src/components/ui/TitleBarAccountSwitcher.vue +@@ -0,0 +1,163 @@ ++ ++ ++ ++ diff --git a/scripts/check.sh b/scripts/check.sh index 2e83bcb..5d407e4 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -76,7 +76,9 @@ done < <(grep -A1 '\.plugin($' "$WORKTREE/apps/app/build.rs" | log "Microsoft sign-in" check "the browser flow is registered" \ - contains "$WORKTREE/apps/app/src/api/auth.rs" 'login_browser_begin,' + contains "$WORKTREE/apps/app/src/api/auth.rs" 'login_device_begin,' +check "the browser sign-in comes back without pasting" \ + contains "$WORKTREE/apps/app-frontend/src/components/ui/MicrosoftLoginModal.vue" 'login_device_poll(' check "the sign-in button opens it" \ contains "$WORKTREE/apps/app-frontend/src/components/ui/AccountsCard.vue" 'microsoftLoginModal.value?.show' @@ -135,6 +137,10 @@ check "the news section can be collapsed" \ contains "$WORKTREE/apps/app-frontend/src/App.vue" 'setNewsCollapsed' check "the right sidebar has a fold button" \ contains "$WORKTREE/apps/app-frontend/src/App.vue" 'setSidebarCollapsed(sidebarToggled)' +check "folding the sidebar is explained once" \ + contains "$WORKTREE/apps/app-frontend/src/App.vue" 'sidebarRememberedModal.value?.show()' +check "the account stays reachable with the sidebar folded" \ + contains "$WORKTREE/apps/app-frontend/src/App.vue" '