From f9b1ab3e46dbd6e70b5f7a742df39c1e4097e289 Mon Sep 17 00:00:00 2001 From: Kilian Schulte Date: Fri, 17 Oct 2025 16:51:47 +0200 Subject: [PATCH 1/3] feat: migrate os selector to jaspr --- site/lib/jaspr_options.dart | 6 ++ site/lib/main.dart | 7 ++ site/lib/src/components/os_selector.dart | 92 +++++++++++++++++++ site/lib/src/util.dart | 40 ++++++++ site/web/assets/js/main.js | 53 ----------- src/content/get-started/quick.md | 2 +- src/content/install/manual.md | 2 +- src/content/install/uninstall.md | 2 +- src/content/install/with-vs-code.md | 2 +- .../platform-integration/android/setup.md | 2 +- 10 files changed, 150 insertions(+), 58 deletions(-) create mode 100644 site/lib/src/components/os_selector.dart diff --git a/site/lib/jaspr_options.dart b/site/lib/jaspr_options.dart index 20e403928dc..e842a739d5f 100644 --- a/site/lib/jaspr_options.dart +++ b/site/lib/jaspr_options.dart @@ -20,6 +20,8 @@ import 'package:docs_flutter_dev_site/src/components/cookie_notice.dart' import 'package:docs_flutter_dev_site/src/components/copy_button.dart' as prefix6; import 'package:docs_flutter_dev_site/src/components/feedback.dart' as prefix7; +import 'package:docs_flutter_dev_site/src/components/os_selector.dart' + as prefix8; /// Default [JasprOptions] for use with your jaspr project. /// @@ -72,6 +74,10 @@ JasprOptions get defaultJasprOptions => JasprOptions( prefix4.ThemeSwitcher: ClientTarget( 'src/components/header/theme_switcher', ), + + prefix8.OsSelector: ClientTarget( + 'src/components/os_selector', + ), }, styles: () => [], ); diff --git a/site/lib/main.dart b/site/lib/main.dart index 24264916e5a..c9c6dee5202 100644 --- a/site/lib/main.dart +++ b/site/lib/main.dart @@ -9,6 +9,7 @@ import 'package:path/path.dart' as path; import 'jaspr_options.dart'; // Generated. Do not remove or edit. import 'src/components/card.dart'; +import 'src/components/os_selector.dart'; import 'src/components/pages/learning_resource_index.dart'; import 'src/components/tabs.dart'; import 'src/data/learning_resources.dart'; @@ -67,6 +68,12 @@ final RegExp _passThroughPattern = RegExp(r'.*\.(txt|json|pdf)$'); /// Custom "components" that can be used from Markdown files. List get _embeddableComponents => [ const DashTabs(), + CustomComponent( + pattern: RegExp('OSSelector', caseSensitive: false), + builder: (name, attributes, child) { + return const OsSelector(); + }, + ), CustomComponent( pattern: RegExp('Card', caseSensitive: false), builder: (name, attributes, child) { diff --git a/site/lib/src/components/os_selector.dart b/site/lib/src/components/os_selector.dart new file mode 100644 index 00000000000..d046fa4f643 --- /dev/null +++ b/site/lib/src/components/os_selector.dart @@ -0,0 +1,92 @@ +// ignore_for_file: constant_identifier_names + +import 'package:jaspr/jaspr.dart'; +import 'package:universal_web/web.dart' as web; + +import '../util.dart'; + +@client +class OsSelector extends StatefulComponent { + const OsSelector({super.key}); + + @override + State createState() => _OsSelectorState(); +} + +class _OsSelectorState extends State { + // This value is currently not synced across potential multiple instances + // of the OS selector on the page. In practice, this currently does not + // happen, but would need to be addressed if changed. + OperatingSystem selectedOs = OperatingSystem.windows; + + @override + void initState() { + super.initState(); + + if (kIsWeb) { + final currentOs = getOS() ?? OperatingSystem.windows; + setOS(currentOs); + } + } + + void setOS(OperatingSystem os) { + setState(() { + selectedOs = os; + }); + + final selectedOsTextSpans = web.document.querySelectorAll( + '.selected-os-text', + ); + for (var i = 0; i < selectedOsTextSpans.length; i++) { + final span = selectedOsTextSpans.item(i) as web.Element; + span.textContent = os.label; + } + + web.document.body!.classList.remove('show-macos'); + web.document.body!.classList.remove('show-linux'); + web.document.body!.classList.remove('show-windows'); + web.document.body!.classList.remove('show-chromeos'); + web.document.body!.classList.add('show-${os.name}'); + } + + @override + Component build(BuildContext context) { + return div(classes: 'card-grid narrow os-selector', [ + for (final os in OperatingSystem.values) + button( + id: 'install-${os.name}', + classes: [ + 'card outlined-card install-card', + if (selectedOs == os) 'selected-card', + ].toClasses, + attributes: { + 'data-os': os.name, + 'aria-label': 'Update docs to cover ${os.label}', + }, + events: { + 'click': (event) { + setOS(os); + }, + }, + [ + div(classes: 'card-leading', [ + img( + src: '/assets/images/docs/brand-svg/${os.name}.svg', + alt: '${os.label} logo', + attributes: { + 'width': '72', + 'height': '72', + 'aria-hidden': 'true', + }, + ), + ]), + div(classes: 'card-header text-center', [ + span(classes: 'card-title', [ + text(os.label), + ]), + ]), + ], + ), + ]); + } +} diff --git a/site/lib/src/util.dart b/site/lib/src/util.dart index 7a5cd70d3ee..349cec2e0e2 100644 --- a/site/lib/src/util.dart +++ b/site/lib/src/util.dart @@ -4,6 +4,7 @@ import 'package:jaspr/jaspr.dart'; import 'package:path/path.dart' as path; +import 'package:universal_web/web.dart' as web; /// Whether this build of the site will be deployed to production. const productionBuild = bool.fromEnvironment('PRODUCTION'); @@ -109,3 +110,42 @@ extension ListToClasses on List { /// that can be added to an HTML element. String get toClasses => join(' '); } + +enum OperatingSystem { + windows('Windows'), + macos('macOS'), + linux('Linux'), + chromeos('ChromeOS'); + + const OperatingSystem(this.label); + final String label; +} + +/// Get the user's current operating system, or +/// `null` if not of one "macos", "windows", "linux", or "chromeos". +OperatingSystem? getOS() { + final userAgent = web.window.navigator.userAgent; + if (userAgent.contains('Mac')) { + // macOS or iPhone + return OperatingSystem.macos; + } + + if (userAgent.contains('Win')) { + // Windows + return OperatingSystem.windows; + } + + if ((userAgent.contains('Linux') || userAgent.contains('X11')) + && !userAgent.contains('Android')) { + // Linux, but not Android + return OperatingSystem.linux; + } + + if (userAgent.contains('CrOS')) { + // ChromeOS + return OperatingSystem.chromeos; + } + + // Anything else + return null; +} \ No newline at end of file diff --git a/site/web/assets/js/main.js b/site/web/assets/js/main.js index 0cb1ea9e830..78e83a01bdb 100644 --- a/site/web/assets/js/main.js +++ b/site/web/assets/js/main.js @@ -180,61 +180,8 @@ function setupPlatformKeys() { }); } -function _osNameFromId(osId) { - switch (osId) { - case 'macos': - return 'macOS'; - case 'linux': - return 'Linux'; - case 'chromeos': - return 'ChromeOS'; - default: - return 'Windows'; - } -} - -function _adjustSelectedOs(osId) { - if (!osId) return; - - const osName = _osNameFromId(osId); - const selectedOsTextSpans = document.querySelectorAll('.selected-os-text'); - selectedOsTextSpans.forEach(function (span) { - span.textContent = osName; - }); - - const osSelectors = document.querySelectorAll('.os-selector button'); - osSelectors.forEach(function (osSelector) { - if (osSelector.getAttribute('data-os') === osId) { - osSelector.classList.add('selected-card'); - } else { - osSelector.classList.remove('selected-card'); - } - }); - - document.body.classList.remove('show-macos', 'show-linux', 'show-windows', 'show-chromeos'); - document.body.classList.add(`show-${osId}`); -} - -function setupOsSelectors() { - const currentOsId = getOS() ?? 'windows'; - _adjustSelectedOs(currentOsId); - - const osSelectors = document.querySelectorAll('.os-selector'); - - for (const osSelector of osSelectors) { - const osButtons = osSelector.querySelectorAll('button'); - for (const osButton of osButtons) { - const osId = osButton.getAttribute('data-os'); - osButton.addEventListener('click', (e) => { - _adjustSelectedOs(osId); - }); - } - } -} function setupSite() { - setupOsSelectors(); - setupToc(); setupPlatformKeys(); setupCollapsibleElements(); diff --git a/src/content/get-started/quick.md b/src/content/get-started/quick.md index 72df38a86bf..4140be2bc2f 100644 --- a/src/content/get-started/quick.md +++ b/src/content/get-started/quick.md @@ -35,7 +35,7 @@ installing and trying out Flutter on a **Windows**{:.selected-os-text} device. If you'd like to follow the instructions for a different OS, please select one of the following. -{% osSelector %} + ## Download prerequisite software {: #download-prerequisites} diff --git a/src/content/install/manual.md b/src/content/install/manual.md index bc8db376264..ba04609bd20 100644 --- a/src/content/install/manual.md +++ b/src/content/install/manual.md @@ -29,7 +29,7 @@ installing Flutter on a **Windows**{:.selected-os-text} device. If you'd like to follow the instructions for a different OS, please select one of the following. -{% osSelector %} + ## Download prerequisite software {: #download-prerequisites} diff --git a/src/content/install/uninstall.md b/src/content/install/uninstall.md index ccc58adb6b2..8450d54fe7f 100644 --- a/src/content/install/uninstall.md +++ b/src/content/install/uninstall.md @@ -17,7 +17,7 @@ uninstall Flutter on a **Windows**{:.selected-os-text} device. If you'd like to follow the instructions for a different OS, please select one of the following. -{% osSelector %} + ## Uninstall the Flutter SDK {: #uninstall } diff --git a/src/content/install/with-vs-code.md b/src/content/install/with-vs-code.md index 05f89c35ea4..0e42a56c576 100644 --- a/src/content/install/with-vs-code.md +++ b/src/content/install/with-vs-code.md @@ -28,7 +28,7 @@ installing Flutter on a **Windows**{:.selected-os-text} device. If you'd like to follow the instructions for a different OS, please select one of the following. -{% osSelector %} + ## Download prerequisite software {: #download-prerequisites} diff --git a/src/content/platform-integration/android/setup.md b/src/content/platform-integration/android/setup.md index 0c70fa89b56..575ff54fe02 100644 --- a/src/content/platform-integration/android/setup.md +++ b/src/content/platform-integration/android/setup.md @@ -27,7 +27,7 @@ setting up Android development on a **Windows**{:.selected-os-text} device. If you'd like to follow the instructions for a different OS, please select one of the following. -{% osSelector %} + ## Set up Android tooling {: #set-up-tooling} From 6c51cc9facde9a6266e3e66c6d406016f52dac41 Mon Sep 17 00:00:00 2001 From: Kilian Schulte Date: Fri, 17 Oct 2025 17:01:08 +0200 Subject: [PATCH 2/3] apply suggestion --- site/lib/src/components/os_selector.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/site/lib/src/components/os_selector.dart b/site/lib/src/components/os_selector.dart index d046fa4f643..122b1c16a9e 100644 --- a/site/lib/src/components/os_selector.dart +++ b/site/lib/src/components/os_selector.dart @@ -42,11 +42,11 @@ class _OsSelectorState extends State { span.textContent = os.label; } - web.document.body!.classList.remove('show-macos'); - web.document.body!.classList.remove('show-linux'); - web.document.body!.classList.remove('show-windows'); - web.document.body!.classList.remove('show-chromeos'); - web.document.body!.classList.add('show-${os.name}'); + final bodyClasses = web.document.body!.classList; + for (final os in OperatingSystem.values) { + bodyClasses.remove('show-${os.name}'); + } + bodyClasses.add('show-${os.name}'); } @override From f786d43b3756f9c8bd8cb366c2b5a35487d5051c Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Mon, 20 Oct 2025 08:47:22 +0200 Subject: [PATCH 3/3] Add license header and fix formatting --- site/lib/src/components/os_selector.dart | 16 +++++++++------- site/lib/src/util.dart | 6 +++--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/site/lib/src/components/os_selector.dart b/site/lib/src/components/os_selector.dart index 122b1c16a9e..c3d5428a163 100644 --- a/site/lib/src/components/os_selector.dart +++ b/site/lib/src/components/os_selector.dart @@ -1,4 +1,6 @@ -// ignore_for_file: constant_identifier_names +// Copyright 2025 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. import 'package:jaspr/jaspr.dart'; import 'package:universal_web/web.dart' as web; @@ -14,7 +16,7 @@ class OsSelector extends StatefulComponent { } class _OsSelectorState extends State { - // This value is currently not synced across potential multiple instances + // This value is currently not synced across potential multiple instances // of the OS selector on the page. In practice, this currently does not // happen, but would need to be addressed if changed. OperatingSystem selectedOs = OperatingSystem.windows; @@ -42,11 +44,11 @@ class _OsSelectorState extends State { span.textContent = os.label; } - final bodyClasses = web.document.body!.classList; - for (final os in OperatingSystem.values) { - bodyClasses.remove('show-${os.name}'); - } - bodyClasses.add('show-${os.name}'); + final bodyClasses = web.document.body!.classList; + for (final os in OperatingSystem.values) { + bodyClasses.remove('show-${os.name}'); + } + bodyClasses.add('show-${os.name}'); } @override diff --git a/site/lib/src/util.dart b/site/lib/src/util.dart index 349cec2e0e2..3c1f930b14c 100644 --- a/site/lib/src/util.dart +++ b/site/lib/src/util.dart @@ -135,8 +135,8 @@ OperatingSystem? getOS() { return OperatingSystem.windows; } - if ((userAgent.contains('Linux') || userAgent.contains('X11')) - && !userAgent.contains('Android')) { + if ((userAgent.contains('Linux') || userAgent.contains('X11')) && + !userAgent.contains('Android')) { // Linux, but not Android return OperatingSystem.linux; } @@ -148,4 +148,4 @@ OperatingSystem? getOS() { // Anything else return null; -} \ No newline at end of file +}