├── .github ├── images │ ├── dark.png │ └── light.png └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── analysis_options.yaml ├── assets ├── images │ ├── Theme_thumbnails-Dark.png │ ├── Theme_thumbnails-Light.png │ ├── appearance │ │ ├── auto-hide-dock-mode │ │ │ ├── auto-hide-dock-bottom.svg │ │ │ ├── auto-hide-dock-left.svg │ │ │ └── auto-hide-dock-right.svg │ │ ├── auto-hide-panel-mode │ │ │ ├── auto-hide-panel-bottom.svg │ │ │ ├── auto-hide-panel-left.svg │ │ │ └── auto-hide-panel-right.svg │ │ ├── dock-mode │ │ │ ├── dock-mode-bottom.svg │ │ │ ├── dock-mode-left.svg │ │ │ └── dock-mode-right.svg │ │ └── panel-mode │ │ │ ├── panel-mode-bottom.svg │ │ │ ├── panel-mode-left.svg │ │ │ └── panel-mode-right.svg │ ├── cursor │ │ ├── left_ptr_24px.png │ │ ├── left_ptr_32px.png │ │ ├── left_ptr_48px.png │ │ ├── left_ptr_64px.png │ │ └── left_ptr_96px.png │ └── multitasking │ │ ├── active-screen-edges-dock-mode │ │ ├── active-screen-edges-dock-bottom.svg │ │ ├── active-screen-edges-dock-left.svg │ │ └── active-screen-edges-dock-right.svg │ │ ├── active-screen-edges-panel-mode │ │ ├── active-screen-edges-panel-bottom.svg │ │ ├── active-screen-edges-panel-left.svg │ │ └── active-screen-edges-panel-right.svg │ │ ├── hot-corner-dock-mode │ │ ├── hot-corner-dock-bottom.svg │ │ ├── hot-corner-dock-left.svg │ │ └── hot-corner-dock-right.svg │ │ ├── hot-corner-panel-mode │ │ ├── hot-corner-panel-bottom.svg │ │ ├── hot-corner-panel-left.svg │ │ └── hot-corner-panel-right.svg │ │ ├── workspaces-dock-mode │ │ ├── workspaces-primary-display-dock-bottom.svg │ │ ├── workspaces-primary-display-dock-left.svg │ │ ├── workspaces-primary-display-dock-right.svg │ │ ├── workspaces-span-displays-dock-bottom.svg │ │ ├── workspaces-span-displays-dock-left.svg │ │ └── workspaces-span-displays-dock-right.svg │ │ └── workspaces-panel-mode │ │ ├── workspaces-primary-display-panel-bottom.svg │ │ ├── workspaces-primary-display-panel-left.svg │ │ ├── workspaces-primary-display-panel-right.svg │ │ ├── workspaces-span-displays-panel-bottom.svg │ │ ├── workspaces-span-displays-panel-left.svg │ │ └── workspaces-span-displays-panel-right.svg ├── pdf_assets │ └── cof.png └── rive │ └── ubuntu_cof.riv ├── l10n.yaml ├── lib ├── app.dart ├── app_model.dart ├── constants.dart ├── generated │ └── dbus │ │ └── display-config-remote-object.dart ├── l10n │ ├── app_da.arb │ ├── app_de.arb │ ├── app_en.arb │ ├── app_fi.arb │ ├── app_fr.arb │ ├── app_id.arb │ ├── app_nb.arb │ ├── app_pl.arb │ ├── app_pt.arb │ ├── app_sv.arb │ ├── app_tr.arb │ └── l10n.dart ├── main.dart ├── schemas │ └── schemas.dart ├── services │ ├── bluetooth_service.dart │ ├── display │ │ ├── display_dbus_service.dart │ │ ├── display_service.dart │ │ └── objects │ │ │ └── dbus_displays_config.dart │ ├── hostname_service.dart │ ├── house_keeping_service.dart │ ├── input_source_service.dart │ ├── keyboard_service.dart │ ├── locale_service.dart │ ├── pdf_service.dart │ ├── power_profile_service.dart │ └── power_settings_service.dart ├── utils.dart └── view │ ├── app_theme.dart │ ├── common │ ├── duration_dropdown_button.dart │ ├── link.dart │ ├── section_description.dart │ ├── selectable_svg_image.dart │ ├── settings_section.dart │ ├── title_bar_tab.dart │ ├── yaru_checkbox_row.dart │ ├── yaru_extra_option_row.dart │ ├── yaru_single_info_row.dart │ ├── yaru_slider_row.dart │ ├── yaru_switch_row.dart │ └── yaru_toggle_buttons_row.dart │ └── pages │ ├── accessibility │ ├── accessibility_model.dart │ ├── accessibility_page.dart │ ├── global_section.dart │ ├── hearing_section.dart │ ├── pointing_and_clicking_section.dart │ ├── seeing_section.dart │ └── typing_section.dart │ ├── accounts │ ├── accounts_model.dart │ ├── accounts_page.dart │ └── user_model.dart │ ├── appearance │ ├── appearance_page.dart │ ├── dock_model.dart │ ├── dock_section.dart │ └── theme_section.dart │ ├── apps │ └── apps_page.dart │ ├── bluetooth │ ├── bluetooth_device_model.dart │ ├── bluetooth_device_row.dart │ ├── bluetooth_model.dart │ └── bluetooth_page.dart │ ├── color │ └── color_page.dart │ ├── connections │ ├── connections_page.dart │ ├── data │ │ └── authentication.dart │ ├── extensions │ │ └── network_service_x.dart │ ├── models │ │ ├── access_point_model.dart │ │ ├── property_stream_notifier.dart │ │ ├── wifi_device_model.dart │ │ └── wifi_model.dart │ ├── widgets │ │ ├── access_point_tile.dart │ │ └── authentication_dialog.dart │ └── wifi_content.dart │ ├── date_and_time │ ├── date_time_model.dart │ ├── date_time_page.dart │ └── timezones.dart │ ├── default_apps │ └── default_apps_page.dart │ ├── displays │ ├── displays_configuration.dart │ ├── displays_model.dart │ ├── displays_page.dart │ ├── nightlight_model.dart │ ├── nightlight_page.dart │ └── widgets │ │ └── monitor_section.dart │ ├── info │ ├── info_model.dart │ └── info_page.dart │ ├── keyboard │ ├── input_source_model.dart │ ├── input_source_section.dart │ ├── input_source_selection_section.dart │ ├── keyboard_page.dart │ ├── keyboard_settings_page.dart │ ├── keyboard_shortcut_row.dart │ ├── keyboard_shortcuts_model.dart │ ├── keyboard_shortcuts_page.dart │ ├── special_characters_model.dart │ └── special_characters_section.dart │ ├── mouse_and_touchpad │ ├── general_section.dart │ ├── mouse_and_touchpad_model.dart │ ├── mouse_and_touchpad_page.dart │ ├── mouse_section.dart │ └── touchpad_section.dart │ ├── multitasking │ ├── multi_tasking_model.dart │ └── multi_tasking_page.dart │ ├── notifications │ ├── app_notifications_section.dart │ ├── global_notifications_section.dart │ ├── notifications_model.dart │ └── notifications_page.dart │ ├── online_accounts │ └── online_accounts_page.dart │ ├── page_items.dart │ ├── power │ ├── battery_model.dart │ ├── battery_section.dart │ ├── battery_widgets.dart │ ├── lid_close_action.dart │ ├── lid_close_model.dart │ ├── lid_close_section.dart │ ├── power_page.dart │ ├── power_profile_model.dart │ ├── power_profile_section.dart │ ├── power_profile_widgets.dart │ ├── power_profile_x.dart │ ├── power_settings.dart │ ├── power_settings_dialogs.dart │ ├── power_settings_model.dart │ ├── power_settings_section.dart │ ├── suspend.dart │ ├── suspend_model.dart │ └── suspend_section.dart │ ├── privacy │ ├── connectivity_model.dart │ ├── connectivity_page.dart │ ├── house_keeping_page.dart │ ├── location_model.dart │ ├── location_page.dart │ ├── privacy_model.dart │ ├── privacy_page.dart │ ├── reporting_model.dart │ ├── reporting_page.dart │ ├── screen_saver_model.dart │ └── screen_saver_page.dart │ ├── region_and_language │ ├── region_and_language_model.dart │ └── region_and_language_page.dart │ ├── removable_media │ ├── removable_media_model.dart │ └── removable_media_page.dart │ ├── search │ └── search_page.dart │ ├── settings_alert_dialog.dart │ ├── settings_page.dart │ ├── settings_page_item.dart │ ├── settings_simple_dialog.dart │ ├── sound │ ├── sound_model.dart │ └── sound_page.dart │ ├── users │ └── users.dart │ └── wallpaper │ ├── color_shading_option_row.dart │ ├── wallpaper_model.dart │ └── wallpaper_page.dart ├── linux ├── .gitignore ├── CMakeLists.txt ├── flutter │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake ├── main.cc ├── my_application.cc └── my_application.h ├── pubspec.yaml ├── snap ├── gui │ ├── settings.desktop │ └── settings.png └── snapcraft.yml ├── test ├── hostname_service_test.dart ├── hostname_service_test.mocks.dart ├── test_helper.dart └── widgets │ ├── app_theme_test.dart │ ├── app_theme_test.mocks.dart │ ├── yaru_check_box_row_test.dart │ ├── yaru_extra_options_row_test.dart │ ├── yaru_single_info_row_test.dart │ ├── yaru_switch_row_test.dart │ └── yaru_togggle_button_row_test.dart └── tools └── dbus ├── generate-remote-object.sh └── interfaces └── org.gnome.Mutter.DisplayConfig.xml /.github/images/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubuntu-flutter-community/settings/6a92f728163fbbc8ba47c7e2066c4bc695825031/.github/images/dark.png -------------------------------------------------------------------------------- /.github/images/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubuntu-flutter-community/settings/6a92f728163fbbc8ba47c7e2066c4bc695825031/.github/images/light.png -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | workflow_dispatch: 11 | 12 | env: 13 | FLUTTER_VERSION: 3.24.3 14 | 15 | jobs: 16 | analyze: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: subosito/flutter-action@v2 21 | with: 22 | flutter-version: ${{env.FLUTTER_VERSION}} 23 | - run: flutter pub get 24 | - run: flutter analyze 25 | 26 | format: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v3 30 | - uses: subosito/flutter-action@v2 31 | with: 32 | flutter-version: ${{env.FLUTTER_VERSION}} 33 | - run: dart format --set-exit-if-changed . 34 | 35 | linux: 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v3 39 | - uses: subosito/flutter-action@v2 40 | with: 41 | flutter-version: ${{env.FLUTTER_VERSION}} 42 | - run: sudo apt update 43 | - run: sudo apt install -y clang cmake curl libgtk-3-dev ninja-build pkg-config unzip 44 | env: 45 | DEBIAN_FRONTEND: noninteractive 46 | - run: flutter pub get 47 | - run: flutter build linux -v 48 | 49 | test: 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@v3 53 | - uses: subosito/flutter-action@v2 54 | with: 55 | flutter-version: ${{env.FLUTTER_VERSION}} 56 | - run: flutter test 57 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | 9 | jobs: 10 | release: 11 | permissions: 12 | contents: write 13 | pull-requests: write 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: googleapis/release-please-action@v4 17 | with: 18 | release-type: dart -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | android/.project 48 | android/.settings/org.eclipse.buildship.core.prefs 49 | android/app/.classpath 50 | android/app/.project 51 | android/app/.settings/org.eclipse.buildship.core.prefs 52 | .vscode/ 53 | pubspec.lock 54 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 2b9537c783063d0459b6282a218658a6955938d9 8 | channel: dev 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Settings App for the Ubuntu Desktop 2 | 3 | The goal of this project is to build a feature complete settings app for the Ubuntu desktop (GNOME, gtk and gnome-shell) with the Flutter UI toolkit. 4 | 5 | | Light | Dark 6 | | - | - | 7 | | ![](.github/images/light.png) | ![](.github/images/dark.png) | 8 | 9 | # Releases 10 | 11 | The app will be soon available as a snap. 12 | 13 | # Building 14 | 15 | The following steps are needed to run the app from the source code. 16 | 17 | ## Install Flutter 18 | 19 | ```bash 20 | sudo apt -y install git curl cmake meson make clang libgtk-3-dev pkg-config && mkdir -p ~/development && cd ~/development && git clone https://github.com/flutter/flutter.git -b stable && echo 'export PATH="$PATH:$HOME/development/flutter/bin"' >> ~/.bashrc && source ~/.bashrc 21 | ``` 22 | 23 | ## Run 24 | 25 | Run the app with vscode or with 26 | ```dart 27 | flutter run 28 | ``` 29 | 30 | # TODO 31 | 32 | - [X] use real yaru icons - thanks to @Jupi007 33 | - [X] responsive layout 34 | - [X] [MVVM software pattern](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel) - thanks to @jpnurmi 35 | - [X] search 36 | - [X] WIFI page - WIP 37 | - [ ] Ethernet page 38 | - [ ] Cellular Network page 39 | - [X] Bluetooth page - WIP 40 | - [X] Wallpaper page 41 | - [X] Appearance page 42 | - [X] Multi-Tasking page 43 | - [X] Notifications page 44 | - [ ] Search page 45 | - [X] Apps page (forward to snap-store) 46 | - [X] Privacy/Security page - WIP 47 | - [ ] Online Accounts page 48 | - [ ] Sound page - WIP 49 | - [X] Power page 50 | - [X] Displays page - WIP 51 | - [X] Mouse and touchpad page 52 | - [X] Keyboard shortcuts page - WIP 53 | - [ ] Printers page - WIP 54 | - [X] Removable Media page 55 | - [ ] Color page 56 | - [X] Region and language page 57 | - [X] Accessability page 58 | - [ ] Users page 59 | - [ ] Preferred apps page 60 | - [X] Date and time page 61 | - [ ] Wacom page 62 | - [X] Info page 63 | 64 | ## Contributing 65 | 66 | This project really needs help to finish the last pages and also in the future when the GNOME desktop changes. Any help is welcome! 67 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | analyzer: 4 | exclude: 5 | - "**/*.freezed.dart" 6 | - "**/*.g.dart" 7 | - "**/*.mocks.dart" 8 | - "**/l10/*.dart" 9 | - "**/generated/**" 10 | 11 | linter: 12 | rules: 13 | prefer_single_quotes: true 14 | require_trailing_commas: true 15 | always_declare_return_types: true 16 | avoid_catches_without_on_clauses: true 17 | avoid_equals_and_hash_code_on_mutable_classes: true 18 | avoid_types_on_closure_parameters: true 19 | cancel_subscriptions: true 20 | directives_ordering: true 21 | eol_at_end_of_file: true 22 | omit_local_variable_types: true 23 | prefer_asserts_in_initializer_lists: true 24 | prefer_const_constructors: true 25 | prefer_final_in_for_each: true 26 | prefer_final_locals: true 27 | prefer_null_aware_method_calls: true 28 | prefer_null_aware_operators: true 29 | sort_constructors_first: true 30 | sort_unnamed_constructors_first: true 31 | sort_pub_dependencies: true 32 | type_annotate_public_apis: true 33 | unawaited_futures: true 34 | unnecessary_lambdas: true 35 | unnecessary_parenthesis: true 36 | use_named_constants: true 37 | use_super_parameters: true 38 | close_sinks: true 39 | -------------------------------------------------------------------------------- /assets/images/Theme_thumbnails-Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubuntu-flutter-community/settings/6a92f728163fbbc8ba47c7e2066c4bc695825031/assets/images/Theme_thumbnails-Dark.png -------------------------------------------------------------------------------- /assets/images/Theme_thumbnails-Light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubuntu-flutter-community/settings/6a92f728163fbbc8ba47c7e2066c4bc695825031/assets/images/Theme_thumbnails-Light.png -------------------------------------------------------------------------------- /assets/images/appearance/auto-hide-dock-mode/auto-hide-dock-bottom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/appearance/auto-hide-panel-mode/auto-hide-panel-bottom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/appearance/auto-hide-panel-mode/auto-hide-panel-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/appearance/auto-hide-panel-mode/auto-hide-panel-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/appearance/dock-mode/dock-mode-bottom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/appearance/dock-mode/dock-mode-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/appearance/dock-mode/dock-mode-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/appearance/panel-mode/panel-mode-bottom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/appearance/panel-mode/panel-mode-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/appearance/panel-mode/panel-mode-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/cursor/left_ptr_24px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubuntu-flutter-community/settings/6a92f728163fbbc8ba47c7e2066c4bc695825031/assets/images/cursor/left_ptr_24px.png -------------------------------------------------------------------------------- /assets/images/cursor/left_ptr_32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubuntu-flutter-community/settings/6a92f728163fbbc8ba47c7e2066c4bc695825031/assets/images/cursor/left_ptr_32px.png -------------------------------------------------------------------------------- /assets/images/cursor/left_ptr_48px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubuntu-flutter-community/settings/6a92f728163fbbc8ba47c7e2066c4bc695825031/assets/images/cursor/left_ptr_48px.png -------------------------------------------------------------------------------- /assets/images/cursor/left_ptr_64px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubuntu-flutter-community/settings/6a92f728163fbbc8ba47c7e2066c4bc695825031/assets/images/cursor/left_ptr_64px.png -------------------------------------------------------------------------------- /assets/images/cursor/left_ptr_96px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubuntu-flutter-community/settings/6a92f728163fbbc8ba47c7e2066c4bc695825031/assets/images/cursor/left_ptr_96px.png -------------------------------------------------------------------------------- /assets/images/multitasking/active-screen-edges-dock-mode/active-screen-edges-dock-bottom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/multitasking/active-screen-edges-dock-mode/active-screen-edges-dock-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/multitasking/active-screen-edges-dock-mode/active-screen-edges-dock-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/multitasking/active-screen-edges-panel-mode/active-screen-edges-panel-bottom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/multitasking/active-screen-edges-panel-mode/active-screen-edges-panel-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/multitasking/active-screen-edges-panel-mode/active-screen-edges-panel-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/multitasking/workspaces-dock-mode/workspaces-primary-display-dock-bottom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/multitasking/workspaces-dock-mode/workspaces-primary-display-dock-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/multitasking/workspaces-dock-mode/workspaces-primary-display-dock-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/multitasking/workspaces-dock-mode/workspaces-span-displays-dock-bottom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/multitasking/workspaces-dock-mode/workspaces-span-displays-dock-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/multitasking/workspaces-dock-mode/workspaces-span-displays-dock-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/multitasking/workspaces-panel-mode/workspaces-primary-display-panel-bottom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/multitasking/workspaces-panel-mode/workspaces-primary-display-panel-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/multitasking/workspaces-panel-mode/workspaces-primary-display-panel-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/multitasking/workspaces-panel-mode/workspaces-span-displays-panel-bottom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/multitasking/workspaces-panel-mode/workspaces-span-displays-panel-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/multitasking/workspaces-panel-mode/workspaces-span-displays-panel-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/pdf_assets/cof.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubuntu-flutter-community/settings/6a92f728163fbbc8ba47c7e2066c4bc695825031/assets/pdf_assets/cof.png -------------------------------------------------------------------------------- /assets/rive/ubuntu_cof.riv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubuntu-flutter-community/settings/6a92f728163fbbc8ba47c7e2066c4bc695825031/assets/rive/ubuntu_cof.riv -------------------------------------------------------------------------------- /l10n.yaml: -------------------------------------------------------------------------------- 1 | arb-dir: lib/l10n 2 | template-arb-file: app_en.arb 3 | output-localization-file: app_localizations.dart 4 | nullable-getter: false 5 | -------------------------------------------------------------------------------- /lib/app_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:safe_change_notifier/safe_change_notifier.dart'; 2 | 3 | class AppModel extends SafeChangeNotifier { 4 | String? _searchQuery; 5 | String? get searchQuery => _searchQuery; 6 | void setSearchQuery(String? value) { 7 | if (value == _searchQuery) return; 8 | _searchQuery = value; 9 | notifyListeners(); 10 | } 11 | 12 | bool? _searchActive; 13 | bool? get searchActive => _searchActive; 14 | void setSearchActive(bool? value) { 15 | if (value == _searchActive) return; 16 | _searchActive = value; 17 | notifyListeners(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/constants.dart: -------------------------------------------------------------------------------- 1 | const kDefaultWidth = 500.0; 2 | -------------------------------------------------------------------------------- /lib/l10n/app_fi.arb: -------------------------------------------------------------------------------- 1 | { 2 | "appTitle": "Ubuntu Asetukset", 3 | "searchHint": "Hae..." 4 | } -------------------------------------------------------------------------------- /lib/l10n/app_nb.arb: -------------------------------------------------------------------------------- 1 | { 2 | "appTitle": "Ubuntu Innstillinger", 3 | "searchHint": "Søk..." 4 | } -------------------------------------------------------------------------------- /lib/l10n/l10n.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 3 | 4 | export 'package:flutter_gen/gen_l10n/app_localizations.dart'; 5 | 6 | extension LocalizationsContext on BuildContext { 7 | AppLocalizations get l10n => AppLocalizations.of(this); 8 | } 9 | -------------------------------------------------------------------------------- /lib/schemas/schemas.dart: -------------------------------------------------------------------------------- 1 | const String schemaDesktopA11y = 'org.gnome.desktop.a11y'; 2 | const String schemaA11yApps = 'org.gnome.desktop.a11y.applications'; 3 | const String schemaA11yKeyboard = 'org.gnome.desktop.a11y.keyboard'; 4 | const String schemaA11yMagnifier = 'org.gnome.desktop.a11y.magnifier'; 5 | const String schemaA11yMouse = 'org.gnome.desktop.a11y.mouse'; 6 | const String schemaInterface = 'org.gnome.desktop.interface'; 7 | const String schemaPeripheralsKeyboard = 8 | 'org.gnome.desktop.peripherals.keyboard'; 9 | const String schemaInputSources = 'org.gnome.desktop.input-sources'; 10 | const String schemaWmPreferences = 'org.gnome.desktop.wm.preferences'; 11 | const schemaWmKeybindings = 'org.gnome.desktop.wm.keybindings'; 12 | const schemaGnomeShellKeybinding = 'org.gnome.shell.keybindings'; 13 | const schemaGnomeShellAppSwitcher = 'org.gnome.shell.app-switcher'; 14 | const String schemaPeripheralsMouse = 'org.gnome.desktop.peripherals.mouse'; 15 | const String schemaPeripheralTouchpad = 16 | 'org.gnome.desktop.peripherals.touchpad'; 17 | const String schemaSound = 'org.gnome.desktop.sound'; 18 | const String schemaDashToDock = 'org.gnome.shell.extensions.dash-to-dock'; 19 | const String schemaNotifications = 'org.gnome.desktop.notifications'; 20 | const String schemaMediaHandling = 'org.gnome.desktop.media-handling'; 21 | const String schemaBackground = 'org.gnome.desktop.background'; 22 | const String schemaMutter = 'org.gnome.mutter'; 23 | const schemaSettingsDaemonPowerPlugin = 24 | 'org.gnome.settings-daemon.plugins.power'; 25 | const schemaPrivacy = 'org.gnome.desktop.privacy'; 26 | const schemaLocation = 'org.gnome.system.location'; 27 | const schemaScreenSaver = 'org.gnome.desktop.screensaver'; 28 | const schemaSession = 'org.gnome.desktop.session'; 29 | const schemaDateTime = 'org.gnome.desktop.datetime'; 30 | const schemaCalendar = 'org.gnome.desktop.calendar'; 31 | const schemaSettingsDaemonColorPlugin = 32 | 'org.gnome.settings-daemon.plugins.color'; 33 | -------------------------------------------------------------------------------- /lib/services/house_keeping_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:dbus/dbus.dart'; 5 | 6 | const _kHouseKeepingInterface = 'org.gnome.SettingsDaemon.Housekeeping'; 7 | const _kHouseKeepingPath = '/org/gnome/SettingsDaemon/Housekeeping'; 8 | const _kEmptyTrashMethodName = 'EmptyTrash'; 9 | const _kRemoveTempFiles = 'RemoveTempFiles'; 10 | const _kRecentlyUsedFilePathSuffix = '/.local/share/recently-used.xbel'; 11 | 12 | class HouseKeepingService { 13 | HouseKeepingService() : _object = _createObject(); 14 | final DBusRemoteObject _object; 15 | 16 | static DBusRemoteObject _createObject() { 17 | return DBusRemoteObject( 18 | DBusClient.session(), 19 | name: _kHouseKeepingInterface, 20 | path: DBusObjectPath(_kHouseKeepingPath), 21 | ); 22 | } 23 | 24 | Future dispose() async { 25 | await _object.client.close(); 26 | } 27 | 28 | void emptyTrash() => _object.emptyTrash(); 29 | 30 | void removeTempFiles() => _object.removeTempFiles(); 31 | 32 | void clearRecentlyUsed() { 33 | final path = Platform.environment['HOME']! + _kRecentlyUsedFilePathSuffix; 34 | if (Platform.environment['HOME'] == null) return; 35 | final file = File(path); 36 | final sink = file.openWrite(); 37 | const cleanContent = ''' 38 | 39 | '''; 40 | sink.write(cleanContent); 41 | sink.close(); 42 | } 43 | } 44 | 45 | extension _HouseKeepingObject on DBusRemoteObject { 46 | Future emptyTrash() { 47 | return callMethod(_kHouseKeepingInterface, _kEmptyTrashMethodName, []); 48 | } 49 | 50 | Future removeTempFiles() { 51 | return callMethod(_kHouseKeepingInterface, _kRemoveTempFiles, []); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/services/input_source_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:xml/xml.dart'; 4 | 5 | class InputSourceService { 6 | InputSourceService() { 7 | inputSources = _loadInputSources(); 8 | } 9 | static const pathToXml = '/usr/share/X11/xkb/rules/base.xml'; 10 | late final List inputSources; 11 | 12 | List _loadInputSources() { 13 | final document = XmlDocument.parse(File(pathToXml).readAsStringSync()); 14 | 15 | final layouts = document.findAllElements('layout'); 16 | return layouts 17 | .map( 18 | (layout) => InputSource( 19 | variants: layout.getElement('variantList') != null 20 | ? layout 21 | .getElement('variantList')! 22 | .childElements 23 | .map( 24 | (variant) => InputSourceVariant( 25 | name: variant 26 | .getElement('configItem')! 27 | .getElement('name')! 28 | .innerText, 29 | description: variant 30 | .getElement('configItem')! 31 | .getElement('description')! 32 | .innerText, 33 | ), 34 | ) 35 | .toList() 36 | : [], 37 | description: layout 38 | .getElement('configItem') 39 | ?.getElement('description') 40 | ?.innerText, 41 | name: 42 | layout.getElement('configItem')?.getElement('name')?.innerText, 43 | ), 44 | ) 45 | .toList(); 46 | } 47 | } 48 | 49 | class InputSource { 50 | InputSource({this.name, this.description, required this.variants}); 51 | final String? name; 52 | final String? description; 53 | final List variants; 54 | } 55 | 56 | class InputSourceVariant { 57 | InputSourceVariant({this.name, this.description}); 58 | final String? name; 59 | final String? description; 60 | } 61 | -------------------------------------------------------------------------------- /lib/services/keyboard_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | 3 | const _methodChannel = MethodChannel('settings/keyboard'); 4 | 5 | abstract class KeyboardService { 6 | Future grab(); 7 | Future ungrab(); 8 | } 9 | 10 | class KeyboardMethodChannel implements KeyboardService { 11 | @override 12 | Future grab() { 13 | return _methodChannel 14 | .invokeMethod('grabKeyboard') 15 | .then((value) => value ?? false); 16 | } 17 | 18 | @override 19 | Future ungrab() async { 20 | return _methodChannel 21 | .invokeMethod('ungrabKeyboard') 22 | .then((value) => value ?? false); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/services/pdf_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/services.dart'; 4 | import 'package:path_provider/path_provider.dart'; 5 | import 'package:pdf/widgets.dart'; 6 | 7 | class PdfService { 8 | static Future generateSystemData( 9 | String osName, 10 | String osVersion, 11 | String kernelVersion, 12 | String processorName, 13 | String processorCount, 14 | String memory, 15 | String graphics, 16 | String diskCapacity, 17 | String osType, 18 | String gnomeVersion, 19 | String windowServer, 20 | ) async { 21 | final pdf = Document(); 22 | final imageCOF = (await rootBundle.load('assets/pdf_assets/cof.png')) 23 | .buffer 24 | .asUint8List(); 25 | pdf.addPage( 26 | Page( 27 | build: (context) => Column( 28 | crossAxisAlignment: CrossAxisAlignment.start, 29 | children: [ 30 | Row( 31 | children: [ 32 | SizedBox( 33 | child: Image(MemoryImage(imageCOF)), 34 | height: 48, 35 | width: 48, 36 | ), 37 | Text( 38 | '$osName $osVersion', 39 | style: const TextStyle( 40 | fontSize: 32, 41 | ), 42 | ), 43 | ], 44 | mainAxisAlignment: MainAxisAlignment.center, 45 | ), 46 | SizedBox(height: 20), 47 | Divider(), 48 | Text('Hardware', style: const TextStyle(fontSize: 20)), 49 | Divider(), 50 | Text('Processor: $processorName x$processorCount'), 51 | Text('Memory: $memory Gb'), 52 | Text('Graphics: $graphics'), 53 | Text('Disk Capacity: $diskCapacity'), 54 | SizedBox(height: 20), 55 | Divider(), 56 | Text('System', style: const TextStyle(fontSize: 20)), 57 | Divider(), 58 | Text('OS: $osName $osVersion ($osType-bit)'), 59 | Text('Kernel version: $kernelVersion'), 60 | Text('GNOME version: $gnomeVersion'), 61 | Text('Windowing System: $windowServer'), 62 | ], 63 | ), 64 | ), 65 | ); 66 | 67 | return saveDocument(name: 'System Data.pdf', pdf: pdf); 68 | } 69 | 70 | static Future saveDocument({ 71 | required String name, 72 | required Document pdf, 73 | }) async { 74 | final bytes = await pdf.save(); 75 | final dir = await getApplicationDocumentsDirectory(); 76 | final file = File('${dir.path}/$name'); 77 | await file.writeAsBytes(bytes); 78 | 79 | return file; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:duration/duration.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | Color colorFromHex(String hexString) { 5 | final buffer = StringBuffer(); 6 | if (hexString.length == 6 || hexString.length == 7) buffer.write('ff'); 7 | buffer.write(hexString.replaceFirst('#', '')); 8 | return Color(int.tryParse(buffer.toString(), radix: 16) ?? 0); 9 | } 10 | 11 | /// Darken a color by [percent] amount (100 = black) 12 | Color darken(Color c, [int percent = 10]) { 13 | assert(1 <= percent && percent <= 100); 14 | final f = 1 - percent / 100; 15 | return Color.fromARGB( 16 | c.alpha, 17 | (c.red * f).round(), 18 | (c.green * f).round(), 19 | (c.blue * f).round(), 20 | ); 21 | } 22 | 23 | /// Lighten a color by [percent] amount (100 = white) 24 | Color lighten(Color c, [int percent = 10]) { 25 | assert(1 <= percent && percent <= 100); 26 | final p = percent / 100; 27 | return Color.fromARGB( 28 | c.alpha, 29 | c.red + ((255 - c.red) * p).round(), 30 | c.green + ((255 - c.green) * p).round(), 31 | c.blue + ((255 - c.blue) * p).round(), 32 | ); 33 | } 34 | 35 | /// Convert duration in seconds into legible string 36 | String formatTime(int seconds) { 37 | return prettyDuration( 38 | Duration(seconds: seconds), 39 | tersity: DurationTersity.second, 40 | ); 41 | } 42 | 43 | /// Convert string in camel case to string split by dash 44 | String camelCaseToSplitByDash(String value) { 45 | final beforeCapitalLetterRegex = RegExp(r'(?=[A-Z])'); 46 | final parts = value.split(beforeCapitalLetterRegex); 47 | var newString = ''; 48 | for (final part in parts) { 49 | if (newString.isEmpty) { 50 | newString = part.toLowerCase(); 51 | } else { 52 | newString = '${newString.toLowerCase()}-${part.toLowerCase()}'; 53 | } 54 | } 55 | return newString; 56 | } 57 | -------------------------------------------------------------------------------- /lib/view/app_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:yaru/yaru.dart'; 3 | 4 | class AppTheme extends ValueNotifier { 5 | AppTheme(this._settings) : super(ThemeMode.system); 6 | 7 | final GnomeSettings _settings; 8 | 9 | void apply(Brightness brightness, YaruVariant variant) { 10 | switch (brightness) { 11 | case Brightness.dark: 12 | value = ThemeMode.dark; 13 | _settings.setValue('gtk-theme', variant.gtkThemeNameDark); 14 | _settings.setValue('color-scheme', 'prefer-dark'); 15 | _settings.setValue('icon-theme', variant.gtkThemeNameDark); 16 | break; 17 | case Brightness.light: 18 | value = ThemeMode.light; 19 | _settings.setValue('gtk-theme', variant.gtkThemeName); 20 | _settings.setValue('color-scheme', 'default'); 21 | _settings.setValue('icon-theme', variant.gtkThemeName); 22 | break; 23 | } 24 | } 25 | 26 | @override 27 | void dispose() { 28 | _settings.dispose(); 29 | super.dispose(); 30 | } 31 | } 32 | 33 | const List globalThemeList = [ 34 | YaruVariant.orange, 35 | YaruVariant.bark, 36 | YaruVariant.sage, 37 | YaruVariant.olive, 38 | YaruVariant.viridian, 39 | YaruVariant.prussianGreen, 40 | YaruVariant.blue, 41 | YaruVariant.purple, 42 | YaruVariant.magenta, 43 | YaruVariant.red, 44 | ]; 45 | 46 | extension YaruVariantName on YaruVariant { 47 | String get gtkThemeName { 48 | return this == YaruVariant.orange ? 'Yaru' : 'Yaru-${name.toLowerCase()}'; 49 | } 50 | 51 | String get gtkThemeNameDark { 52 | return '$gtkThemeName-dark'; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/view/common/duration_dropdown_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:settings/l10n/l10n.dart'; 3 | import 'package:settings/utils.dart'; 4 | import 'package:yaru/yaru.dart'; 5 | 6 | class DurationDropdownButton extends StatelessWidget { 7 | const DurationDropdownButton({ 8 | super.key, 9 | required this.value, 10 | required this.values, 11 | required this.onChanged, 12 | this.zeroValueText, 13 | }); 14 | 15 | /// The current value of the [DropdownButton] 16 | final int? value; 17 | 18 | /// The list of values for [DropdownMenuItem] elements 19 | final List values; 20 | 21 | /// The callback that gets invoked when the [DropdownButton] value changes 22 | final ValueChanged onChanged; 23 | 24 | /// Optional string for value 0 25 | /// 26 | /// Defaults to 'Never' 27 | final String? zeroValueText; 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | return YaruPopupMenuButton( 32 | initialValue: value, 33 | itemBuilder: (context) { 34 | return [ 35 | for (final value in values.where((value) => value > 0)) 36 | PopupMenuItem( 37 | value: value, 38 | child: Text(formatTime(value)), 39 | onTap: () => onChanged(value), 40 | ), 41 | if (values.contains(0)) 42 | PopupMenuItem( 43 | value: 0, 44 | child: Text(zeroValueText ?? context.l10n.never), 45 | ), 46 | ]; 47 | }, 48 | child: Text( 49 | value != null && value != 0 50 | ? formatTime(value!) 51 | : zeroValueText ?? context.l10n.never, 52 | ), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/view/common/link.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:url_launcher/url_launcher.dart'; 3 | 4 | class Link extends StatelessWidget { 5 | const Link({super.key, required this.url, required this.linkText}); 6 | 7 | final String url; 8 | final String linkText; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return InkWell( 13 | onTap: () async => await launchUrl(Uri.parse(url)), 14 | child: Text( 15 | linkText, 16 | style: Theme.of(context) 17 | .textTheme 18 | .bodySmall 19 | ?.copyWith(color: Theme.of(context).colorScheme.primary), 20 | ), 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/view/common/section_description.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SectionDescription extends StatelessWidget { 4 | const SectionDescription({ 5 | super.key, 6 | required this.width, 7 | required this.text, 8 | }); 9 | 10 | final double width; 11 | final String text; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return SizedBox( 16 | width: width, 17 | child: Padding( 18 | padding: const EdgeInsets.only(bottom: 20), 19 | child: Row( 20 | children: [ 21 | Flexible( 22 | child: Text( 23 | text, 24 | style: Theme.of(context).textTheme.bodySmall, 25 | ), 26 | ), 27 | ], 28 | ), 29 | ), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/view/common/selectable_svg_image.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_svg/flutter_svg.dart'; 3 | 4 | class SelectableSvgImage extends StatelessWidget { 5 | const SelectableSvgImage({ 6 | super.key, 7 | required this.path, 8 | this.onTap, 9 | required this.selected, 10 | required this.height, 11 | required this.selectedColor, 12 | this.padding, 13 | }); 14 | 15 | final String path; 16 | final VoidCallback? onTap; 17 | final bool selected; 18 | final double height; 19 | final double? padding; 20 | final Color selectedColor; 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return InkWell( 25 | borderRadius: BorderRadius.circular(6.0), 26 | onTap: onTap, 27 | child: Container( 28 | decoration: BoxDecoration( 29 | borderRadius: BorderRadius.circular(8.0), 30 | ), 31 | child: Padding( 32 | padding: EdgeInsets.all(padding ?? 0.0), 33 | child: ClipRRect( 34 | borderRadius: BorderRadius.circular(4), 35 | child: SvgPicture.asset( 36 | path, 37 | colorFilter: ColorFilter.mode( 38 | selected 39 | ? selectedColor 40 | : Theme.of(context).colorScheme.surface, 41 | selected ? BlendMode.srcIn : BlendMode.color, 42 | ), 43 | height: height, 44 | ), 45 | ), 46 | ), 47 | ), 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/view/common/settings_section.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:yaru/yaru.dart'; 3 | 4 | class SettingsSection extends StatelessWidget { 5 | const SettingsSection({ 6 | super.key, 7 | this.width, 8 | this.headline, 9 | this.headerWidget, 10 | required this.children, 11 | }); 12 | 13 | final double? width; 14 | final Widget? headline; 15 | final Widget? headerWidget; 16 | final List children; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return YaruSection( 21 | width: width, 22 | margin: const EdgeInsets.only(bottom: 20), 23 | headline: headline != null || headerWidget != null 24 | ? Row( 25 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 26 | children: [ 27 | if (headline != null) headline!, 28 | if (headerWidget != null) headerWidget!, 29 | ], 30 | ) 31 | : null, 32 | child: Column(children: children), 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/view/common/title_bar_tab.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class TitleBarTab extends StatelessWidget { 4 | const TitleBarTab({ 5 | super.key, 6 | required this.text, 7 | required this.iconData, 8 | }); 9 | 10 | final String text; 11 | final IconData iconData; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Tab( 16 | child: Row( 17 | mainAxisSize: MainAxisSize.min, 18 | mainAxisAlignment: MainAxisAlignment.center, 19 | children: [ 20 | Icon( 21 | iconData, 22 | size: 20, 23 | ), 24 | const SizedBox( 25 | width: 5, 26 | ), 27 | Flexible( 28 | child: Text( 29 | text, 30 | textAlign: TextAlign.center, 31 | overflow: TextOverflow.ellipsis, 32 | ), 33 | ), 34 | ], 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/view/common/yaru_single_info_row.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:yaru/yaru.dart'; 3 | 4 | class YaruSingleInfoRow extends StatelessWidget { 5 | /// Creates an info widget with infoLabel and infoValue. 6 | /// Useful when there is a need of copying an info from the app. 7 | /// `infoValue` value is placed inside a [SelectableText] so that the value can be copied. 8 | /// 9 | /// for example: 10 | /// ```dart 11 | /// YaruSingleInfoRow( 12 | /// infoLabel: "Info Label", 13 | /// infoValue: "Info Value", 14 | /// ); 15 | /// ``` 16 | const YaruSingleInfoRow({ 17 | super.key, 18 | required this.infoLabel, 19 | required this.infoValue, 20 | this.padding = const EdgeInsets.all(8.0), 21 | }); 22 | 23 | /// Specifies the label for the information and is placed at the trailing position. 24 | final String infoLabel; 25 | 26 | /// The information that needs to be shown. 27 | /// This property is placed inside a [SelectableText] so that the value passed to the 28 | /// `infoValue` can be selected and will also allow to copy that value. 29 | /// 30 | /// Default color of the text will be [Theme.of(context).colorScheme.onSurface.withAlpha(150)]. 31 | final String infoValue; 32 | 33 | /// The padding [EdgeInsets] which defaults to `EdgeInsets.all(8.0)`. 34 | final EdgeInsets padding; 35 | 36 | @override 37 | Widget build(BuildContext context) { 38 | return YaruTile( 39 | enabled: true, 40 | title: Text(infoLabel), 41 | trailing: Expanded( 42 | flex: 2, 43 | child: SelectableText( 44 | infoValue, 45 | style: TextStyle( 46 | color: Theme.of(context).colorScheme.onSurface.withAlpha(150), 47 | ), 48 | textAlign: TextAlign.right, 49 | ), 50 | ), 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/view/common/yaru_switch_row.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:yaru/yaru.dart'; 3 | 4 | class YaruSwitchRow extends StatelessWidget { 5 | /// Creates yaru style switch. The [Switch] will be aligned horizontally along with the `trailingWidget`. 6 | /// 7 | /// for example: 8 | ///```dart 9 | /// bool _yaruSwitchEnabled = false; 10 | /// YaruSwitchRow( 11 | /// value: _yaruSwitchEnabled, 12 | /// onChanged: (v) { 13 | /// setState(() { 14 | /// _yaruSwitchEnabled = v; 15 | /// }); 16 | /// }, 17 | /// trailingWidget: Text("Trailing Widget"), 18 | /// ), 19 | ///``` 20 | const YaruSwitchRow({ 21 | super.key, 22 | this.enabled = true, 23 | required this.trailingWidget, 24 | this.actionDescription, 25 | required this.value, 26 | required this.onChanged, 27 | this.padding = const EdgeInsets.all(8.0), 28 | }); 29 | 30 | /// Whether or not we can interact with the widget 31 | final bool enabled; 32 | 33 | /// The [Widget] placed at the trailing position. 34 | final Widget trailingWidget; 35 | 36 | /// The text that is placed below the `trailingWidget`. 37 | final String? actionDescription; 38 | 39 | /// The current value of the [Switch]. 40 | final bool? value; 41 | 42 | /// The callback that gets invoked when the [Switch] value changes. 43 | final Function(bool) onChanged; 44 | 45 | /// The padding [EdgeInsets] which defaults to `EdgeInsets.all(8.0)`. 46 | final EdgeInsets padding; 47 | 48 | @override 49 | Widget build(BuildContext context) { 50 | final enabled = this.enabled && value != null; 51 | 52 | return YaruTile( 53 | enabled: enabled, 54 | title: trailingWidget, 55 | subtitle: actionDescription != null ? Text(actionDescription!) : null, 56 | trailing: YaruSwitch( 57 | value: value ?? false, 58 | onChanged: enabled ? onChanged : null, 59 | ), 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/view/pages/accessibility/accessibility_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:settings/l10n/l10n.dart'; 4 | import 'package:settings/view/pages/accessibility/accessibility_model.dart'; 5 | import 'package:settings/view/pages/accessibility/global_section.dart'; 6 | import 'package:settings/view/pages/accessibility/hearing_section.dart'; 7 | import 'package:settings/view/pages/accessibility/pointing_and_clicking_section.dart'; 8 | import 'package:settings/view/pages/accessibility/seeing_section.dart'; 9 | import 'package:settings/view/pages/accessibility/typing_section.dart'; 10 | import 'package:settings/view/pages/settings_page.dart'; 11 | import 'package:watch_it/watch_it.dart'; 12 | import 'package:yaru/yaru.dart'; 13 | 14 | class AccessibilityPage extends StatelessWidget { 15 | const AccessibilityPage({super.key}); 16 | 17 | static Widget create(BuildContext context) { 18 | return ChangeNotifierProvider( 19 | create: (_) => AccessibilityModel(di()), 20 | child: const AccessibilityPage(), 21 | ); 22 | } 23 | 24 | static Widget createTitle(BuildContext context) => 25 | Text(context.l10n.accessibilityPageTitle); 26 | 27 | static bool searchMatches(String value, BuildContext context) => 28 | value.isNotEmpty 29 | ? context.l10n.accessibilityPageTitle 30 | .toLowerCase() 31 | .contains(value.toLowerCase()) 32 | : false; 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | return const SettingsPage( 37 | children: [ 38 | GlobalSection(), 39 | SeeingSection(), 40 | HearingSection(), 41 | TypingSection(), 42 | PointingAndClickingSection(), 43 | ], 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/view/pages/accessibility/global_section.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:settings/constants.dart'; 4 | import 'package:settings/l10n/l10n.dart'; 5 | import 'package:settings/view/common/settings_section.dart'; 6 | import 'package:settings/view/common/yaru_switch_row.dart'; 7 | import 'package:settings/view/pages/accessibility/accessibility_model.dart'; 8 | 9 | class GlobalSection extends StatelessWidget { 10 | const GlobalSection({super.key}); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | final model = context.watch(); 15 | return SettingsSection( 16 | width: kDefaultWidth, 17 | headline: Text(context.l10n.global), 18 | children: [ 19 | YaruSwitchRow( 20 | trailingWidget: Text(context.l10n.alwaysShowUniversalAccessMenu), 21 | value: model.universalAccessStatus, 22 | onChanged: model.setUniversalAccessStatus, 23 | ), 24 | ], 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/view/pages/accessibility/hearing_section.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:settings/constants.dart'; 4 | import 'package:settings/l10n/l10n.dart'; 5 | import 'package:settings/view/common/settings_section.dart'; 6 | import 'package:settings/view/common/yaru_extra_option_row.dart'; 7 | import 'package:settings/view/pages/accessibility/accessibility_model.dart'; 8 | import 'package:settings/view/pages/settings_simple_dialog.dart'; 9 | import 'package:yaru/yaru.dart'; 10 | 11 | class HearingSection extends StatelessWidget { 12 | const HearingSection({super.key}); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return SettingsSection( 17 | width: kDefaultWidth, 18 | headline: Text(context.l10n.hearing), 19 | children: const [ 20 | _VisualAlerts(), 21 | ], 22 | ); 23 | } 24 | } 25 | 26 | class _VisualAlerts extends StatelessWidget { 27 | const _VisualAlerts(); 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | final model = context.watch(); 32 | return YaruExtraOptionRow( 33 | iconData: YaruIcons.gear, 34 | actionLabel: context.l10n.visualAlerts, 35 | actionDescription: context.l10n.visualAlertsDescription, 36 | value: model.visualAlerts, 37 | onChanged: model.setVisualAlerts, 38 | onPressed: () => showDialog( 39 | context: context, 40 | builder: (_) => ChangeNotifierProvider.value( 41 | value: model, 42 | child: const _VisualAlertsSettings(), 43 | ), 44 | ), 45 | ); 46 | } 47 | } 48 | 49 | class _VisualAlertsSettings extends StatelessWidget { 50 | const _VisualAlertsSettings(); 51 | 52 | @override 53 | Widget build(BuildContext context) { 54 | final model = context.watch(); 55 | return SettingsSimpleDialog( 56 | width: kDefaultWidth, 57 | title: context.l10n.visualAlerts, 58 | closeIconData: YaruIcons.window_close, 59 | children: [ 60 | ListTile( 61 | title: Text(context.l10n.flashEntireWindow), 62 | leading: YaruRadio( 63 | value: 'frame-flash', 64 | groupValue: model.visualAlertsType, 65 | onChanged: (value) => model.setVisualAlertsType(value!), 66 | ), 67 | ), 68 | ListTile( 69 | title: Text(context.l10n.flashEntireScreen), 70 | leading: YaruRadio( 71 | value: 'fullscreen-flash', 72 | groupValue: model.visualAlertsType, 73 | onChanged: (value) => model.setVisualAlertsType(value!), 74 | ), 75 | ), 76 | ], 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/view/pages/accounts/accounts_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:safe_change_notifier/safe_change_notifier.dart'; 4 | import 'package:xdg_accounts/xdg_accounts.dart'; 5 | 6 | class AccountsModel extends SafeChangeNotifier { 7 | AccountsModel(this._xdgAccounts); 8 | 9 | final XdgAccounts _xdgAccounts; 10 | 11 | StreamSubscription? _usersChangedSub; 12 | List? get users => _xdgAccounts.xdgUsers; 13 | 14 | Future addUser({ 15 | required String name, 16 | required String fullname, 17 | required int accountType, 18 | required String password, 19 | required String passwordHint, 20 | }) async { 21 | final path = await _xdgAccounts.createUser( 22 | name: name, 23 | fullname: fullname, 24 | accountType: accountType, 25 | ); 26 | final user = _xdgAccounts.findUserByPath(path); 27 | if (user != null) { 28 | await user.setLocked(false); 29 | await user.setPasswordMode(0); 30 | await user.setPassword(password, passwordHint); 31 | } 32 | } 33 | 34 | Future deleteUser({ 35 | required int id, 36 | required String name, 37 | required bool removeFiles, 38 | }) async => 39 | await _xdgAccounts.deleteUser( 40 | id: id, 41 | name: name, 42 | removeFiles: removeFiles, 43 | ); 44 | 45 | Future init() async { 46 | await _xdgAccounts.init(); 47 | _usersChangedSub = _xdgAccounts.usersChanged.listen((event) { 48 | notifyListeners(); 49 | }); 50 | notifyListeners(); 51 | } 52 | 53 | @override 54 | void dispose() { 55 | super.dispose(); 56 | _usersChangedSub?.cancel(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/view/pages/accounts/user_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:safe_change_notifier/safe_change_notifier.dart'; 5 | import 'package:xdg_accounts/xdg_accounts.dart'; 6 | 7 | class UserModel extends SafeChangeNotifier { 8 | UserModel(this._user); 9 | 10 | final XdgUser _user; 11 | StreamSubscription? _userNameSub; 12 | 13 | String? get userName => _user.userName; 14 | set userName(String? value) { 15 | if (value == null) return; 16 | _user.setUserName(value, allowInteractiveAuthorization: true); 17 | } 18 | 19 | int? get id => _user.uid; 20 | 21 | File? get iconFile { 22 | String? iconFile; 23 | try { 24 | iconFile = _user.iconFile; 25 | } on Exception catch (_) { 26 | return null; 27 | } 28 | 29 | if (iconFile == null || iconFile.endsWith('.face')) { 30 | return null; 31 | } else { 32 | return File(_user.iconFile!.toString()); 33 | } 34 | } 35 | 36 | XdgAccountType? get accountType => _user.accountType; 37 | 38 | Future init() async { 39 | _userNameSub = _user.userNameChanged.listen((event) => notifyListeners()); 40 | } 41 | 42 | @override 43 | void dispose() { 44 | _userNameSub?.cancel(); 45 | super.dispose(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/view/pages/appearance/appearance_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:settings/l10n/l10n.dart'; 4 | import 'package:settings/view/pages/appearance/dock_model.dart'; 5 | import 'package:settings/view/pages/appearance/dock_section.dart'; 6 | import 'package:settings/view/pages/appearance/theme_section.dart'; 7 | import 'package:settings/view/pages/settings_page.dart'; 8 | import 'package:watch_it/watch_it.dart'; 9 | import 'package:yaru/yaru.dart'; 10 | 11 | class AppearancePage extends StatelessWidget { 12 | const AppearancePage({super.key}); 13 | 14 | static Widget create(BuildContext context) { 15 | return ChangeNotifierProvider( 16 | create: (_) => DockModel(di()), 17 | child: const AppearancePage(), 18 | ); 19 | } 20 | 21 | static Widget createTitle(BuildContext context) => 22 | Text(context.l10n.appearancePageTitle); 23 | 24 | static bool searchMatches(String value, BuildContext context) => 25 | value.isNotEmpty 26 | ? context.l10n.appearancePageTitle 27 | .toLowerCase() 28 | .contains(value.toLowerCase()) 29 | : false; 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | return const SettingsPage( 34 | children: [ 35 | ThemeSection(), 36 | DockSection(), 37 | ], 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/view/pages/appearance/theme_section.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:linux_system_info/linux_system_info.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:settings/constants.dart'; 5 | import 'package:settings/view/app_theme.dart'; 6 | import 'package:settings/view/common/settings_section.dart'; 7 | import 'package:settings/view/common/yaru_switch_row.dart'; 8 | import 'package:yaru/yaru.dart'; 9 | 10 | class ThemeSection extends StatefulWidget { 11 | const ThemeSection({super.key}); 12 | 13 | @override 14 | State createState() => _ThemeSectionState(); 15 | } 16 | 17 | class _ThemeSectionState extends State { 18 | late String _osVersion; 19 | @override 20 | void initState() { 21 | _osVersion = SystemInfo().os_version; 22 | super.initState(); 23 | } 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | final theme = context.watch(); 28 | 29 | return SettingsSection( 30 | width: kDefaultWidth, 31 | headline: const Text('Theme'), 32 | children: [ 33 | YaruSwitchRow( 34 | trailingWidget: Theme.of(context).brightness == Brightness.light 35 | ? const Row( 36 | children: [ 37 | Icon(YaruIcons.sun), 38 | SizedBox(width: 8), 39 | Text('Dark mode is turned off'), 40 | ], 41 | ) 42 | : const Row( 43 | children: [ 44 | Icon(YaruIcons.clear_night), 45 | SizedBox(width: 8), 46 | Text('Dark mode is turned on'), 47 | ], 48 | ), 49 | value: Theme.of(context).brightness == Brightness.dark, 50 | onChanged: (_) { 51 | theme.apply( 52 | Theme.of(context).brightness == Brightness.dark 53 | ? Brightness.light 54 | : Brightness.dark, 55 | YaruTheme.of(context).variant ?? YaruVariant.orange, 56 | ); 57 | }, 58 | ), 59 | if (int.parse(_osVersion.substring(0, 2)) >= 22) 60 | Wrap( 61 | spacing: 5, 62 | runSpacing: 5, 63 | children: [ 64 | for (final globalTheme in globalThemeList) 65 | YaruColorDisk( 66 | onPressed: () { 67 | theme.apply(Theme.of(context).brightness, globalTheme); 68 | }, 69 | color: globalTheme.color, 70 | selected: YaruTheme.of(context).variant == globalTheme, 71 | ), 72 | ], 73 | ), 74 | ], 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/view/pages/apps/apps_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:settings/constants.dart'; 5 | import 'package:settings/l10n/l10n.dart'; 6 | import 'package:settings/view/pages/settings_page.dart'; 7 | import 'package:yaru/yaru.dart'; 8 | 9 | class AppsPage extends StatelessWidget { 10 | const AppsPage({super.key}); 11 | 12 | static Widget create(BuildContext context) => const AppsPage(); 13 | 14 | static Widget createTitle(BuildContext context) => 15 | Text(context.l10n.appsPageTitle); 16 | 17 | static bool searchMatches(String value, BuildContext context) => value 18 | .isNotEmpty 19 | ? context.l10n.appsPageTitle.toLowerCase().contains(value.toLowerCase()) 20 | : false; 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return SettingsPage( 25 | children: [ 26 | SizedBox( 27 | width: kDefaultWidth, 28 | child: YaruSection( 29 | child: YaruTile( 30 | leading: const Text('Apps can be managed in the App Store'), 31 | trailing: ElevatedButton.icon( 32 | onPressed: () => Process.start('snap-store', []), 33 | label: const Text('Open'), 34 | icon: const Icon(YaruIcons.application_bag), 35 | ), 36 | ), 37 | ), 38 | ), 39 | ], 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/view/pages/color/color_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:settings/l10n/l10n.dart'; 3 | import 'package:settings/view/pages/settings_page.dart'; 4 | 5 | class ColorPage extends StatelessWidget { 6 | const ColorPage({super.key}); 7 | 8 | static Widget create(BuildContext context) => const ColorPage(); 9 | 10 | static Widget createTitle(BuildContext context) => Text(context.l10n.color); 11 | 12 | static bool searchMatches(String value, BuildContext context) => 13 | value.isNotEmpty 14 | ? context.l10n.color.toLowerCase().contains(value.toLowerCase()) 15 | : false; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return SettingsPage( 20 | children: [Center(child: Text(context.l10n.color))], 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/view/pages/connections/data/authentication.dart: -------------------------------------------------------------------------------- 1 | enum StorePassword { thisUser, allUsers, askAlways } 2 | 3 | enum WifiSecurity { wpa2Personal, wpa3Personal } 4 | 5 | class Authentication { 6 | const Authentication({ 7 | required this.password, 8 | required this.storePassword, 9 | required this.wifiSecurity, 10 | }); 11 | 12 | final String password; 13 | final StorePassword storePassword; 14 | final WifiSecurity? wifiSecurity; 15 | } 16 | -------------------------------------------------------------------------------- /lib/view/pages/connections/models/access_point_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:nm/nm.dart'; 3 | import 'package:settings/view/pages/connections/models/property_stream_notifier.dart'; 4 | 5 | enum ActiveConnectionState { 6 | unknown, 7 | activating, 8 | activated, 9 | deactivating, 10 | deactivated 11 | } 12 | 13 | class AccessPointModel extends PropertyStreamNotifier { 14 | AccessPointModel( 15 | this._networkManagerAccessPoint, 16 | this._networkManagerDeviceWireless, 17 | ) { 18 | addProperties(_networkManagerAccessPoint.propertiesChanged); 19 | addPropertyListener('Strength', notifyListeners); 20 | } 21 | final NetworkManagerAccessPoint _networkManagerAccessPoint; 22 | NetworkManagerAccessPoint get networkManagerAccessPoint => 23 | _networkManagerAccessPoint; 24 | late final NetworkManagerDeviceWireless _networkManagerDeviceWireless; 25 | 26 | bool get isActive => 27 | listEquals(_networkManagerDeviceWireless.activeAccessPoint?.ssid, ssid); 28 | 29 | List get ssid => _networkManagerAccessPoint.ssid; 30 | 31 | String get name => String.fromCharCodes(ssid); 32 | 33 | bool get isLocked => _networkManagerAccessPoint.flags 34 | .contains(NetworkManagerWifiAccessPointFlag.privacy); 35 | 36 | WifiStrengthLevel get strengthLevel => 37 | WifiStrengthLevelX.fromStrength(strength); 38 | 39 | int get strength => _networkManagerAccessPoint.strength; 40 | } 41 | 42 | enum WifiStrengthLevel { 43 | none, 44 | weak, 45 | ok, 46 | good, 47 | excellent, 48 | } 49 | 50 | extension WifiStrengthLevelX on WifiStrengthLevel { 51 | static WifiStrengthLevel fromStrength(int strength) { 52 | assert(strength >= 0 && strength <= 100); 53 | 54 | if (strength >= 0 && strength <= 20) return WifiStrengthLevel.none; 55 | if (strength > 20 && strength <= 40) return WifiStrengthLevel.weak; 56 | if (strength > 40 && strength < 60) return WifiStrengthLevel.ok; 57 | if (strength > 60 && strength <= 80) return WifiStrengthLevel.good; 58 | return WifiStrengthLevel.excellent; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/view/pages/connections/models/property_stream_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:safe_change_notifier/safe_change_notifier.dart'; 5 | 6 | /// Listens and notifies a stream of property changes. 7 | class PropertyStreamNotifier extends SafeChangeNotifier { 8 | final _callbacks = {}; 9 | final _subscriptions = >>[]; 10 | 11 | /// Adds a stream of [properties]. 12 | void addProperties(Stream> properties) { 13 | _subscriptions.add( 14 | properties.listen((changedProperties) { 15 | for (final property in changedProperties) { 16 | _callbacks[property]?.call(); 17 | } 18 | }), 19 | ); 20 | } 21 | 22 | /// Listens [property] and calls [onChanged] when it changes. 23 | void addPropertyListener(String property, VoidCallback onChanged) { 24 | _callbacks[property] = onChanged; 25 | } 26 | 27 | @override 28 | void dispose() { 29 | for (final subscription in _subscriptions) { 30 | subscription.cancel(); 31 | } 32 | super.dispose(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/view/pages/connections/models/wifi_device_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:nm/nm.dart'; 4 | import 'package:settings/view/pages/connections/models/access_point_model.dart'; 5 | import 'package:settings/view/pages/connections/models/property_stream_notifier.dart'; 6 | 7 | class WifiDeviceModel extends PropertyStreamNotifier { 8 | WifiDeviceModel(this._networkManagerDevice) { 9 | _networkManagerDeviceWireless = _networkManagerDevice.wireless!; 10 | 11 | addProperties(_networkManagerDeviceWireless.propertiesChanged); 12 | addPropertyListener('AccessPoints', notifyListeners); 13 | addPropertyListener('ActiveAccessPoint', notifyListeners); 14 | addPropertyListener('LastScan', notifyListeners); 15 | } 16 | final NetworkManagerDevice _networkManagerDevice; 17 | NetworkManagerDevice get networkManagerDevice => _networkManagerDevice; 18 | late final NetworkManagerDeviceWireless _networkManagerDeviceWireless; 19 | 20 | List get accesPoints { 21 | final acceptedAccessPoints = []; 22 | 23 | /// filter duplicate access points 24 | // ignore: prefer_function_declarations_over_variables 25 | final isAccessPointAlreadyAdded = (newAP) { 26 | return acceptedAccessPoints.any( 27 | (ap) => 28 | // ap.hwAddress == newAP.hwAddress && 29 | listEquals(ap.ssid, newAP.ssid), 30 | ); 31 | }; 32 | 33 | // filter hidden or empyty SSIDs 34 | // ignore: prefer_function_declarations_over_variables 35 | final hasSsid = (ssid) { 36 | return ssid.isNotEmpty || String.fromCharCodes(ssid).trim().isNotEmpty; 37 | }; 38 | 39 | // ignore: prefer_function_declarations_over_variables 40 | final isAccessPointAccepted = (accessPoint) { 41 | if (hasSsid(accessPoint.ssid) && 42 | !isAccessPointAlreadyAdded(accessPoint)) { 43 | acceptedAccessPoints.add(accessPoint); 44 | return true; 45 | } 46 | return false; 47 | }; 48 | 49 | return _networkManagerDeviceWireless.accessPoints 50 | .where(isAccessPointAccepted) 51 | .map( 52 | (networkManagerAccessPoint) => AccessPointModel( 53 | networkManagerAccessPoint, 54 | _networkManagerDeviceWireless, 55 | ), 56 | ) 57 | .sorted((ap1, ap2) => ap2.strength.compareTo(ap1.strength)); 58 | } 59 | 60 | String get driverName => _networkManagerDevice.driver; 61 | String get interface => _networkManagerDevice.interface; 62 | } 63 | -------------------------------------------------------------------------------- /lib/view/pages/connections/widgets/access_point_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:settings/view/pages/connections/models/access_point_model.dart'; 3 | import 'package:yaru/yaru.dart'; 4 | 5 | class AccessPointTile extends StatelessWidget { 6 | const AccessPointTile({ 7 | required this.accessPointModel, 8 | required this.onTap, 9 | super.key, 10 | }); 11 | final AccessPointModel accessPointModel; 12 | final VoidCallback onTap; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return AnimatedBuilder( 17 | animation: accessPointModel, 18 | builder: (_, __) { 19 | return InkWell( 20 | borderRadius: BorderRadius.circular(4), 21 | onTap: onTap, 22 | child: YaruTile( 23 | title: Row( 24 | children: [ 25 | Icon(accessPointModel.wifiIconData), 26 | const SizedBox( 27 | width: 10, 28 | ), 29 | Text(accessPointModel.name), 30 | ], 31 | ), 32 | trailing: Row( 33 | children: [ 34 | Icon(accessPointModel.isActiveIconData), 35 | const SizedBox(width: 8.0), 36 | YaruOptionButton( 37 | onPressed: () {}, 38 | child: const Icon(YaruIcons.gear), 39 | ), 40 | ], 41 | ), 42 | ), 43 | ); 44 | }, 45 | ); 46 | } 47 | } 48 | 49 | extension _AccessPointX on AccessPointModel { 50 | IconData? get isActiveIconData { 51 | if (isActive) return Icons.done; 52 | return null; 53 | } 54 | 55 | IconData get wifiIconData { 56 | switch (strengthLevel) { 57 | case WifiStrengthLevel.none: 58 | return isLocked 59 | ? YaruIcons.network_wireless_signal_none_secure 60 | : YaruIcons.network_wireless_signal_none; 61 | case WifiStrengthLevel.weak: 62 | return isLocked 63 | ? YaruIcons.network_wireless_signal_weak_secure 64 | : YaruIcons.network_wireless_signal_weak; 65 | case WifiStrengthLevel.ok: 66 | return isLocked 67 | ? YaruIcons.network_wireless_signal_ok_secure 68 | : YaruIcons.network_wireless_signal_ok; 69 | case WifiStrengthLevel.good: 70 | return isLocked 71 | ? YaruIcons.network_wireless_signal_good_secure 72 | : YaruIcons.network_wireless_signal_good; 73 | case WifiStrengthLevel.excellent: 74 | return isLocked 75 | ? YaruIcons.network_wireless_signal_excellent_secure 76 | : YaruIcons.network_wireless_signal_excellent; 77 | 78 | default: 79 | throw StateError('Illigal Satet $strengthLevel'); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/view/pages/default_apps/default_apps_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:settings/l10n/l10n.dart'; 3 | import 'package:settings/view/pages/settings_page.dart'; 4 | 5 | class DefaultAppsPage extends StatelessWidget { 6 | const DefaultAppsPage({super.key}); 7 | 8 | static Widget create(BuildContext context) => const DefaultAppsPage(); 9 | 10 | static Widget createTitle(BuildContext context) => 11 | Text(context.l10n.defaultAppsPageTitle); 12 | 13 | static bool searchMatches(String value, BuildContext context) => 14 | value.isNotEmpty 15 | ? context.l10n.defaultAppsPageTitle 16 | .toLowerCase() 17 | .contains(value.toLowerCase()) 18 | : false; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return SettingsPage( 23 | children: [ 24 | Center( 25 | child: Text(context.l10n.defaultAppsPageTitle), 26 | ), 27 | ], 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/view/pages/displays/nightlight_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:safe_change_notifier/safe_change_notifier.dart'; 2 | import 'package:settings/schemas/schemas.dart'; 3 | import 'package:yaru/yaru.dart'; 4 | 5 | const _setNightLight = 'night-light-enabled'; 6 | const _setNightlightTemp = 'night-light-temperature'; 7 | const _setNightlightScheduleFrom = 'night-light-schedule-from'; 8 | const _setNightlightScheduleTo = 'night-light-schedule-to'; 9 | 10 | class NightlightModel extends SafeChangeNotifier { 11 | NightlightModel(GSettingsService service) 12 | : _nightlightSettings = service.lookup(schemaSettingsDaemonColorPlugin) { 13 | _nightlightSettings?.addListener(notifyListeners); 14 | } 15 | 16 | @override 17 | void dispose() { 18 | _nightlightSettings?.removeListener(notifyListeners); 19 | super.dispose(); 20 | } 21 | 22 | final GnomeSettings? _nightlightSettings; 23 | 24 | bool? get nightLightEnabled => _nightlightSettings?.boolValue(_setNightLight); 25 | 26 | void setNightLightEnabled(bool? value) { 27 | _nightlightSettings?.setValue(_setNightLight, value!); 28 | notifyListeners(); 29 | } 30 | 31 | double? get nightLightTemp => 32 | (_nightlightSettings?.intValue(_setNightlightTemp) ?? 4000).toDouble(); 33 | 34 | void setNightLightTemp(double? value) { 35 | _nightlightSettings?.setUint32Value(_setNightlightTemp, value!.toInt()); 36 | notifyListeners(); 37 | } 38 | 39 | DateTime getNightLightSchedule({required bool isFrom}) { 40 | final key = isFrom ? _setNightlightScheduleFrom : _setNightlightScheduleTo; 41 | final time = _nightlightSettings?.doubleValue(key) ?? 0; 42 | final hours = time.truncate(); 43 | final minutes = ((time - hours) * 60).round(); 44 | return DateTime(0, 0, 0, hours, minutes); 45 | } 46 | 47 | void setNightLightSchedule( 48 | double value, { 49 | required bool isFrom, 50 | required bool isHours, 51 | }) { 52 | final key = isFrom ? _setNightlightScheduleFrom : _setNightlightScheduleTo; 53 | final time = isHours 54 | ? (value + getNightLightSchedule(isFrom: isFrom).minute / 60) 55 | : getNightLightSchedule(isFrom: isFrom).hour + (value / 60); 56 | _nightlightSettings?.setValue( 57 | key, 58 | time, 59 | ); 60 | notifyListeners(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/view/pages/info/info_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ffi'; 2 | import 'dart:io'; 3 | 4 | import 'package:linux_system_info/linux_system_info.dart'; 5 | import 'package:safe_change_notifier/safe_change_notifier.dart'; 6 | import 'package:settings/services/hostname_service.dart'; 7 | import 'package:udisks/udisks.dart'; 8 | 9 | const kUbuntuLogoPath = '/usr/share/plymouth/ubuntu-logo.png'; 10 | 11 | class InfoModel extends SafeChangeNotifier { 12 | InfoModel({ 13 | required HostnameService hostnameService, 14 | required UDisksClient uDisksClient, 15 | List? cpus, 16 | SystemInfo? systemInfo, 17 | MemInfo? memInfo, 18 | GnomeInfo? gnomeInfo, 19 | }) : _hostnameService = hostnameService, 20 | _uDisksClient = uDisksClient, 21 | _cpus = cpus ?? CpuInfo.getProcessors(), 22 | _systemInfo = systemInfo ?? SystemInfo(), 23 | _memInfo = memInfo ?? MemInfo(), 24 | _gnomeInfo = gnomeInfo ?? GnomeInfo(); 25 | 26 | final HostnameService _hostnameService; 27 | final UDisksClient _uDisksClient; 28 | final List _cpus; 29 | final SystemInfo _systemInfo; 30 | final MemInfo _memInfo; 31 | final GnomeInfo _gnomeInfo; 32 | 33 | String? _gpuName = ''; 34 | int? _diskCapacity; 35 | 36 | Future init() async { 37 | await _hostnameService.init(); 38 | 39 | await GpuInfo.load().then((gpus) { 40 | _gpuName = gpus.isNotEmpty ? gpus.first.model : null; 41 | }); 42 | 43 | await _uDisksClient.connect().then((value) { 44 | _diskCapacity = 45 | _uDisksClient.drives.fold(0, (sum, drive) => sum + drive.size); 46 | }); 47 | 48 | notifyListeners(); 49 | } 50 | 51 | String get hostname => _hostnameService.hostname; 52 | String get staticHostname => _hostnameService.staticHostname; 53 | 54 | void setHostname(String hostname) { 55 | _hostnameService.setHostname(hostname).then((_) => notifyListeners()); 56 | } 57 | 58 | String get osName => _systemInfo.os_name; 59 | String get osVersion => _systemInfo.os_version; 60 | int get osType => sizeOf() * 8; 61 | String get kernelVersion => _systemInfo.kernel_version; 62 | 63 | String get processorName => _cpus[0].model_name; 64 | int get processorCount => _cpus.length + 1; 65 | int get memory => _memInfo.mem_total_gb; 66 | String? get graphics => _gpuName; 67 | int? get diskCapacity => _diskCapacity; 68 | 69 | String get gnomeVersion => _gnomeInfo.version; 70 | String get windowServer => Platform.environment['XDG_SESSION_TYPE'] ?? ''; 71 | } 72 | -------------------------------------------------------------------------------- /lib/view/pages/keyboard/input_source_section.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:settings/constants.dart'; 4 | import 'package:settings/view/common/settings_section.dart'; 5 | import 'package:settings/view/pages/keyboard/input_source_model.dart'; 6 | import 'package:yaru/yaru.dart'; 7 | 8 | class InputSourceSection extends StatelessWidget { 9 | const InputSourceSection({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | final inputSourceModel = context.watch(); 14 | 15 | return Column( 16 | children: [ 17 | SettingsSection( 18 | width: kDefaultWidth, 19 | headline: const Text('Change input sources'), 20 | children: [ 21 | ListTile( 22 | shape: RoundedRectangleBorder( 23 | borderRadius: BorderRadius.circular(4), 24 | ), 25 | title: const Text('Use the same input for all windows'), 26 | leading: YaruRadio( 27 | value: false, 28 | groupValue: inputSourceModel.perWindow, 29 | onChanged: (_) => inputSourceModel.perWindow = false, 30 | ), 31 | ), 32 | ListTile( 33 | shape: RoundedRectangleBorder( 34 | borderRadius: BorderRadius.circular(4), 35 | ), 36 | title: const Text('Give each window its own input source'), 37 | leading: YaruRadio( 38 | value: true, 39 | groupValue: inputSourceModel.perWindow, 40 | onChanged: (_) => inputSourceModel.perWindow = true, 41 | ), 42 | ), 43 | ], 44 | ), 45 | ], 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/view/pages/keyboard/keyboard_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:settings/l10n/l10n.dart'; 3 | import 'package:settings/view/common/title_bar_tab.dart'; 4 | import 'package:settings/view/pages/keyboard/keyboard_settings_page.dart'; 5 | import 'package:settings/view/pages/keyboard/keyboard_shortcuts_page.dart'; 6 | import 'package:yaru/yaru.dart'; 7 | 8 | class KeyboardPage extends StatefulWidget { 9 | const KeyboardPage({super.key}); 10 | 11 | static Widget create(BuildContext context) => const KeyboardPage(); 12 | 13 | static Widget createTitle(BuildContext context) => 14 | Text(context.l10n.keyboardPageTitle); 15 | 16 | static bool searchMatches(String value, BuildContext context) => 17 | value.isNotEmpty 18 | ? context.l10n.keyboardPageTitle 19 | .toLowerCase() 20 | .contains(value.toLowerCase()) 21 | : false; 22 | 23 | @override 24 | State createState() => _KeyboardPageState(); 25 | } 26 | 27 | class _KeyboardPageState extends State 28 | with TickerProviderStateMixin { 29 | late TabController tabController; 30 | 31 | @override 32 | void initState() { 33 | tabController = TabController(length: 2, vsync: this); 34 | super.initState(); 35 | } 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | const views = [KeyboardSettingsPage(), KeyboardShortcutsPage()]; 40 | return DefaultTabController( 41 | length: 2, 42 | child: Scaffold( 43 | appBar: YaruWindowTitleBar( 44 | border: BorderSide.none, 45 | backgroundColor: Theme.of(context).scaffoldBackgroundColor, 46 | title: const SizedBox( 47 | width: 400, 48 | child: YaruTabBar( 49 | tabs: [ 50 | TitleBarTab( 51 | text: 'Keyboard Settings', 52 | iconData: YaruIcons.keyboard, 53 | ), 54 | TitleBarTab( 55 | text: 'Keyboard Shortcuts', 56 | iconData: YaruIcons.keyboard_shortcuts, 57 | ), 58 | ], 59 | ), 60 | ), 61 | ), 62 | body: TabBarView( 63 | children: views 64 | .map( 65 | (e) => Padding( 66 | padding: const EdgeInsets.only(top: kYaruPagePadding), 67 | child: e, 68 | ), 69 | ) 70 | .toList(), 71 | ), 72 | ), 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/view/pages/keyboard/keyboard_settings_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:settings/services/input_source_service.dart'; 4 | import 'package:settings/view/pages/keyboard/input_source_model.dart'; 5 | import 'package:settings/view/pages/keyboard/input_source_section.dart'; 6 | import 'package:settings/view/pages/keyboard/input_source_selection_section.dart'; 7 | import 'package:settings/view/pages/keyboard/special_characters_model.dart'; 8 | import 'package:settings/view/pages/keyboard/special_characters_section.dart'; 9 | import 'package:settings/view/pages/settings_page.dart'; 10 | import 'package:watch_it/watch_it.dart'; 11 | import 'package:yaru/yaru.dart'; 12 | 13 | class KeyboardSettingsPage extends StatelessWidget { 14 | const KeyboardSettingsPage({ 15 | super.key, 16 | }); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | final settingsService = di(); 21 | final inputSourceService = di(); 22 | 23 | return SettingsPage( 24 | children: [ 25 | ChangeNotifierProvider( 26 | create: (_) => InputSourceModel(settingsService, inputSourceService), 27 | child: const InputSourceSelectionSection(), 28 | ), 29 | ChangeNotifierProvider( 30 | create: (_) => InputSourceModel(settingsService, inputSourceService), 31 | child: const InputSourceSection(), 32 | ), 33 | ChangeNotifierProvider( 34 | create: (_) => SpecialCharactersModel(settingsService), 35 | child: const SpecialCharactersSection(), 36 | ), 37 | ], 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/view/pages/keyboard/keyboard_shortcuts_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:safe_change_notifier/safe_change_notifier.dart'; 2 | import 'package:settings/services/keyboard_service.dart'; 3 | import 'package:yaru/yaru.dart'; 4 | 5 | class KeyboardShortcutsModel extends SafeChangeNotifier { 6 | KeyboardShortcutsModel({ 7 | required KeyboardService keyboard, 8 | required GSettingsService settings, 9 | required this.schemaId, 10 | }) : _keyboard = keyboard, 11 | _shortcutSettings = settings.lookup(schemaId) { 12 | _shortcutSettings?.addListener(notifyListeners); 13 | } 14 | final String schemaId; 15 | 16 | @override 17 | void dispose() { 18 | _shortcutSettings?.removeListener(notifyListeners); 19 | super.dispose(); 20 | } 21 | 22 | final KeyboardService _keyboard; 23 | final GnomeSettings? _shortcutSettings; 24 | 25 | Future grabKeyboard() => _keyboard.grab(); 26 | Future ungrabKeyboard() => _keyboard.ungrab(); 27 | 28 | List getShortcutStrings(String shortcutId) { 29 | final keys = _shortcutSettings?.stringArrayValue(shortcutId); 30 | return keys?.whereType().toList() ?? []; 31 | } 32 | 33 | void setShortcut(String shortcutId, List keys) { 34 | _shortcutSettings?.setValue(shortcutId, keys); 35 | notifyListeners(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/view/pages/keyboard/keyboard_shortcuts_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:settings/constants.dart'; 4 | import 'package:settings/schemas/schemas.dart'; 5 | import 'package:settings/services/keyboard_service.dart'; 6 | import 'package:settings/view/common/settings_section.dart'; 7 | import 'package:settings/view/pages/keyboard/keyboard_shortcut_row.dart'; 8 | import 'package:settings/view/pages/keyboard/keyboard_shortcuts_model.dart'; 9 | import 'package:settings/view/pages/settings_page.dart'; 10 | import 'package:watch_it/watch_it.dart'; 11 | import 'package:yaru/yaru.dart'; 12 | 13 | class KeyboardShortcutsPage extends StatelessWidget { 14 | const KeyboardShortcutsPage({super.key}); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return SettingsPage( 19 | children: [ 20 | ChangeNotifierProvider( 21 | create: (_) => KeyboardShortcutsModel( 22 | keyboard: di(), 23 | settings: di(), 24 | schemaId: schemaWmKeybindings, 25 | ), 26 | child: const SettingsSection( 27 | width: kDefaultWidth, 28 | headline: Text('Navigation Shortcuts'), 29 | children: [ 30 | KeyboardShortcutRow( 31 | label: 'Switch windows', 32 | shortcutId: 'switch-windows', 33 | ), 34 | KeyboardShortcutRow( 35 | label: 'Switch windows backward', 36 | shortcutId: 'switch-windows-backward', 37 | ), 38 | ], 39 | ), 40 | ), 41 | ChangeNotifierProvider( 42 | create: (_) => KeyboardShortcutsModel( 43 | keyboard: di(), 44 | settings: di(), 45 | schemaId: schemaGnomeShellKeybinding, 46 | ), 47 | child: const SettingsSection( 48 | width: kDefaultWidth, 49 | headline: Text('System'), 50 | children: [ 51 | KeyboardShortcutRow( 52 | label: 'Toggle Apps Grid', 53 | shortcutId: 'toggle-application-view', 54 | ), 55 | ], 56 | ), 57 | ), 58 | ], 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/view/pages/mouse_and_touchpad/general_section.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:settings/constants.dart'; 4 | import 'package:settings/view/common/settings_section.dart'; 5 | import 'package:settings/view/common/yaru_toggle_buttons_row.dart'; 6 | 7 | import 'mouse_and_touchpad_model.dart'; 8 | 9 | class GeneralSection extends StatelessWidget { 10 | const GeneralSection({super.key}); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | final model = context.watch(); 15 | 16 | return SettingsSection( 17 | width: kDefaultWidth, 18 | headline: const Text('General'), 19 | children: [ 20 | YaruToggleButtonsRow( 21 | actionLabel: 'Primary Button', 22 | labels: const ['Left', 'Right'], 23 | actionDescription: 24 | 'Sets the order of physical buttons on mice and touchpads', 25 | selectedValues: model.leftHanded != null 26 | ? [!model.leftHanded!, model.leftHanded!] 27 | : null, 28 | onPressed: (index) => model.setLeftHanded(index == 1), 29 | ), 30 | ], 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/view/pages/mouse_and_touchpad/mouse_and_touchpad_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:settings/l10n/l10n.dart'; 4 | import 'package:settings/view/pages/mouse_and_touchpad/general_section.dart'; 5 | import 'package:settings/view/pages/mouse_and_touchpad/mouse_and_touchpad_model.dart'; 6 | import 'package:settings/view/pages/mouse_and_touchpad/mouse_section.dart'; 7 | import 'package:settings/view/pages/mouse_and_touchpad/touchpad_section.dart'; 8 | import 'package:settings/view/pages/settings_page.dart'; 9 | import 'package:watch_it/watch_it.dart'; 10 | import 'package:yaru/yaru.dart'; 11 | 12 | class MouseAndTouchpadPage extends StatelessWidget { 13 | const MouseAndTouchpadPage({super.key}); 14 | 15 | static Widget create(BuildContext context) { 16 | return ChangeNotifierProvider( 17 | create: (_) => MouseAndTouchpadModel(di()), 18 | child: const MouseAndTouchpadPage(), 19 | ); 20 | } 21 | 22 | static Widget createTitle(BuildContext context) => 23 | Text(context.l10n.mouseAndTouchPadPageTitle); 24 | 25 | static bool searchMatches(String value, BuildContext context) => 26 | value.isNotEmpty 27 | ? context.l10n.mouseAndTouchPadPageTitle 28 | .toLowerCase() 29 | .contains(value.toLowerCase()) 30 | : false; 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | return const SettingsPage( 35 | children: [ 36 | GeneralSection(), 37 | MouseSection(), 38 | TouchpadSection(), 39 | ], 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/view/pages/mouse_and_touchpad/mouse_section.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:settings/constants.dart'; 4 | import 'package:settings/view/common/settings_section.dart'; 5 | import 'package:settings/view/common/yaru_slider_row.dart'; 6 | import 'package:settings/view/common/yaru_switch_row.dart'; 7 | import 'package:settings/view/pages/mouse_and_touchpad/mouse_and_touchpad_model.dart'; 8 | 9 | class MouseSection extends StatelessWidget { 10 | const MouseSection({super.key}); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | final model = context.watch(); 15 | 16 | return SettingsSection( 17 | width: kDefaultWidth, 18 | headline: const Text('Mouse'), 19 | children: [ 20 | YaruSliderRow( 21 | actionLabel: 'Speed', 22 | value: model.mouseSpeed, 23 | showValue: false, 24 | min: -1, 25 | max: 1, 26 | defaultValue: 0, 27 | onChanged: model.setMouseSpeed, 28 | ), 29 | YaruSwitchRow( 30 | trailingWidget: const Text('Natural Scrolling'), 31 | actionDescription: 'Scrolling moves the content, not the view', 32 | value: model.mouseNaturalScroll, 33 | onChanged: model.setMouseNaturalScroll, 34 | ), 35 | ], 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/view/pages/mouse_and_touchpad/touchpad_section.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:settings/constants.dart'; 4 | import 'package:settings/view/common/settings_section.dart'; 5 | import 'package:settings/view/common/yaru_slider_row.dart'; 6 | import 'package:settings/view/common/yaru_switch_row.dart'; 7 | import 'package:settings/view/pages/mouse_and_touchpad/mouse_and_touchpad_model.dart'; 8 | 9 | class TouchpadSection extends StatelessWidget { 10 | const TouchpadSection({super.key}); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | final model = context.watch(); 15 | 16 | return SettingsSection( 17 | width: kDefaultWidth, 18 | headline: const Text('Touchpad'), 19 | children: [ 20 | YaruSliderRow( 21 | actionLabel: 'Speed', 22 | value: model.touchpadSpeed, 23 | showValue: false, 24 | min: -1, 25 | max: 1, 26 | defaultValue: 0, 27 | onChanged: model.setTouchpadSpeed, 28 | ), 29 | YaruSwitchRow( 30 | trailingWidget: const Text('Natural Scrolling'), 31 | actionDescription: 'Scrolling moves the content, not the view', 32 | value: model.touchpadNaturalScroll, 33 | onChanged: model.setTouchpadNaturalScroll, 34 | ), 35 | YaruSwitchRow( 36 | trailingWidget: const Text('Tap To Click'), 37 | value: model.touchpadTapToClick, 38 | onChanged: model.setTouchpadTapToClick, 39 | ), 40 | YaruSwitchRow( 41 | trailingWidget: const Text('Disable While Typing'), 42 | value: model.touchpadDisableWhileTyping, 43 | onChanged: model.setTouchpadDisableWhileTyping, 44 | ), 45 | YaruSwitchRow( 46 | trailingWidget: const Text('Two-finger Scrolling'), 47 | value: model.twoFingerScrolling, 48 | onChanged: model.setTwoFingerScrolling, 49 | ), 50 | YaruSwitchRow( 51 | trailingWidget: const Text('Edge Scrolling'), 52 | value: model.edgeScrolling, 53 | onChanged: model.setEdgeScrolling, 54 | ), 55 | ], 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/view/pages/notifications/app_notifications_section.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:settings/constants.dart'; 4 | import 'package:settings/view/common/settings_section.dart'; 5 | import 'package:settings/view/common/yaru_switch_row.dart'; 6 | import 'package:settings/view/pages/notifications/notifications_model.dart'; 7 | import 'package:watch_it/watch_it.dart'; 8 | import 'package:yaru/yaru.dart'; 9 | 10 | class AppNotificationsSection extends StatelessWidget { 11 | const AppNotificationsSection({super.key}); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final model = context.watch(); 16 | 17 | return SettingsSection( 18 | width: kDefaultWidth, 19 | headline: const Text('App notifications'), 20 | children: model.applications 21 | ?.map( 22 | (appId) => 23 | AppNotificationsSettingRow.create(context, appId: appId), 24 | ) 25 | .toList() ?? 26 | const [Text('Schema not installed ')], 27 | ); 28 | } 29 | } 30 | 31 | class AppNotificationsSettingRow extends StatelessWidget { 32 | const AppNotificationsSettingRow({super.key}); 33 | 34 | static Widget create(BuildContext context, {required String appId}) { 35 | return ChangeNotifierProvider( 36 | create: (_) => AppNotificationsModel(appId, di()), 37 | child: const AppNotificationsSettingRow(), 38 | ); 39 | } 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | final model = context.watch(); 44 | return YaruSwitchRow( 45 | trailingWidget: Text(model.appId), 46 | value: model.enable, 47 | onChanged: model.setEnable, 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/view/pages/notifications/global_notifications_section.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:settings/constants.dart'; 4 | import 'package:settings/view/common/settings_section.dart'; 5 | import 'package:settings/view/common/yaru_switch_row.dart'; 6 | import 'package:settings/view/pages/notifications/notifications_model.dart'; 7 | 8 | class GlobalNotificationsSection extends StatelessWidget { 9 | const GlobalNotificationsSection({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | final model = context.watch(); 14 | 15 | return SettingsSection( 16 | width: kDefaultWidth, 17 | headline: const Text('Global'), 18 | children: [ 19 | YaruSwitchRow( 20 | trailingWidget: const Text('Do Not Disturb'), 21 | value: model.doNotDisturb, 22 | onChanged: model.setDoNotDisturb, 23 | ), 24 | YaruSwitchRow( 25 | trailingWidget: const Text('Show Notifications On Lock Screen'), 26 | value: model.showOnLockScreen, 27 | onChanged: model.setShowOnLockScreen, 28 | ), 29 | ], 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/view/pages/notifications/notifications_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:safe_change_notifier/safe_change_notifier.dart'; 2 | import 'package:settings/schemas/schemas.dart'; 3 | import 'package:yaru/yaru.dart'; 4 | 5 | class NotificationsModel extends SafeChangeNotifier { 6 | NotificationsModel(GSettingsService service) 7 | : _notificationSettings = service.lookup(schemaNotifications) { 8 | _notificationSettings?.addListener(notifyListeners); 9 | } 10 | static const _showBannersKey = 'show-banners'; 11 | static const _showInLockScreenKey = 'show-in-lock-screen'; 12 | 13 | @override 14 | void dispose() { 15 | _notificationSettings?.removeListener(notifyListeners); 16 | super.dispose(); 17 | } 18 | 19 | final GnomeSettings? _notificationSettings; 20 | 21 | // Global section 22 | 23 | bool? get doNotDisturb { 24 | return _notificationSettings?.boolValue(_showBannersKey) == false; 25 | } 26 | 27 | void setDoNotDisturb(bool value) { 28 | _notificationSettings?.setValue(_showBannersKey, !value); 29 | notifyListeners(); 30 | } 31 | 32 | bool? get showOnLockScreen => 33 | _notificationSettings?.boolValue(_showInLockScreenKey); 34 | 35 | void setShowOnLockScreen(bool value) { 36 | _notificationSettings?.setValue(_showInLockScreenKey, value); 37 | notifyListeners(); 38 | } 39 | 40 | // App section 41 | 42 | List? get applications => _notificationSettings 43 | ?.stringArrayValue('application-children') 44 | ?.whereType() 45 | .toList(); 46 | } 47 | 48 | class AppNotificationsModel extends SafeChangeNotifier { 49 | AppNotificationsModel(this.appId, GSettingsService service) 50 | : _appNotificationSettings = 51 | service.lookup(_appSchemaId, path: _getPath(appId)) { 52 | _appNotificationSettings?.addListener(notifyListeners); 53 | } 54 | static const _enableKey = 'enable'; 55 | static const _appSchemaId = '$schemaNotifications.application'; 56 | 57 | @override 58 | void dispose() { 59 | _appNotificationSettings?.removeListener(notifyListeners); 60 | super.dispose(); 61 | } 62 | 63 | final String appId; 64 | final GnomeSettings? _appNotificationSettings; 65 | 66 | static String _getPath(String appId) { 67 | return '/${_appSchemaId.replaceAll('.', '/')}/$appId/'; 68 | } 69 | 70 | bool? get enable => _appNotificationSettings?.boolValue(_enableKey); 71 | void setEnable(bool value) { 72 | _appNotificationSettings?.setValue(_enableKey, value); 73 | notifyListeners(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/view/pages/notifications/notifications_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:settings/l10n/l10n.dart'; 4 | import 'package:settings/view/pages/notifications/app_notifications_section.dart'; 5 | import 'package:settings/view/pages/notifications/global_notifications_section.dart'; 6 | import 'package:settings/view/pages/notifications/notifications_model.dart'; 7 | import 'package:settings/view/pages/settings_page.dart'; 8 | import 'package:watch_it/watch_it.dart'; 9 | import 'package:yaru/yaru.dart'; 10 | 11 | class NotificationsPage extends StatelessWidget { 12 | const NotificationsPage({super.key}); 13 | 14 | static Widget create(BuildContext context) => 15 | ChangeNotifierProvider( 16 | create: (_) => NotificationsModel(di()), 17 | child: const NotificationsPage(), 18 | ); 19 | 20 | static Widget createTitle(BuildContext context) => 21 | Text(context.l10n.notificationsPageTitle); 22 | 23 | static bool searchMatches(String value, BuildContext context) => 24 | value.isNotEmpty 25 | ? context.l10n.notificationsPageTitle 26 | .toLowerCase() 27 | .contains(value.toLowerCase()) 28 | : false; 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return const SettingsPage( 33 | children: [ 34 | GlobalNotificationsSection(), 35 | AppNotificationsSection(), 36 | ], 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/view/pages/online_accounts/online_accounts_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:settings/l10n/l10n.dart'; 3 | import 'package:settings/view/pages/settings_page.dart'; 4 | 5 | class OnlineAccountsPage extends StatelessWidget { 6 | const OnlineAccountsPage({super.key}); 7 | 8 | static Widget create(BuildContext context) => const OnlineAccountsPage(); 9 | 10 | static Widget createTitle(BuildContext context) => 11 | Text(context.l10n.onlineAccountsPageTitle); 12 | 13 | static bool searchMatches(String value, BuildContext context) => 14 | value.isNotEmpty 15 | ? context.l10n.onlineAccountsPageTitle 16 | .toLowerCase() 17 | .contains(value.toLowerCase()) 18 | : false; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return SettingsPage( 23 | children: [Center(child: Text(context.l10n.onlineAccountsPageTitle))], 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/view/pages/power/battery_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:safe_change_notifier/safe_change_notifier.dart'; 2 | import 'package:upower/upower.dart'; 3 | 4 | export 'package:settings/services/power_profile_service.dart' show PowerProfile; 5 | 6 | class BatteryModel extends SafeChangeNotifier { 7 | UPowerClient? _client; 8 | 9 | void init(UPowerClient client) { 10 | client.connect().then((_) { 11 | _client = client; 12 | notifyListeners(); 13 | }); 14 | } 15 | 16 | UPowerDevice? get _device => _client?.displayDevice; 17 | 18 | UPowerDeviceState? get state => _device?.state; 19 | double get percentage => _device?.percentage ?? 0; 20 | int get timeToEmpty => _device?.timeToEmpty ?? 0; 21 | int get timeToFull => _device?.timeToFull ?? 0; 22 | } 23 | -------------------------------------------------------------------------------- /lib/view/pages/power/battery_section.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:settings/constants.dart'; 4 | import 'package:settings/l10n/l10n.dart'; 5 | import 'package:settings/view/common/settings_section.dart'; 6 | import 'package:settings/view/pages/power/battery_model.dart'; 7 | import 'package:settings/view/pages/power/battery_widgets.dart'; 8 | import 'package:upower/upower.dart'; 9 | import 'package:watch_it/watch_it.dart'; 10 | import 'package:yaru/yaru.dart'; 11 | 12 | class BatterySection extends StatefulWidget { 13 | const BatterySection({super.key}); 14 | 15 | static Widget create(BuildContext context) { 16 | return ChangeNotifierProvider( 17 | create: (_) => BatteryModel(), 18 | child: const BatterySection(), 19 | ); 20 | } 21 | 22 | @override 23 | State createState() => _BatterySectionState(); 24 | } 25 | 26 | class _BatterySectionState extends State { 27 | @override 28 | void initState() { 29 | super.initState(); 30 | 31 | final model = context.read(); 32 | model.init(di()); 33 | } 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | final theme = Theme.of(context); 38 | final model = context.watch(); 39 | return SettingsSection( 40 | width: kDefaultWidth, 41 | headline: Text(context.l10n.batterySectionHeadline), 42 | children: [ 43 | Padding( 44 | padding: const EdgeInsets.all(8.0), 45 | child: YaruLinearProgressIndicator( 46 | value: model.percentage / 100.0, 47 | color: model.percentage > 80.0 48 | ? theme.colorScheme.success 49 | : model.percentage < 30.0 50 | ? theme.colorScheme.error 51 | : theme.colorScheme.warning, 52 | ), 53 | ), 54 | Padding( 55 | padding: const EdgeInsets.all(8.0), 56 | child: Row( 57 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 58 | children: [ 59 | BatteryStateLabel( 60 | state: model.state, 61 | percentage: model.percentage, 62 | timeToFull: model.timeToFull, 63 | timeToEmpty: model.timeToEmpty, 64 | ), 65 | Text(context.l10n.batteryPercentage(model.percentage.round())), 66 | ], 67 | ), 68 | ), 69 | ], 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/view/pages/power/battery_widgets.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:settings/l10n/l10n.dart'; 3 | import 'package:settings/utils.dart'; 4 | import 'package:upower/upower.dart'; 5 | 6 | class BatteryStateLabel extends StatelessWidget { 7 | const BatteryStateLabel({ 8 | super.key, 9 | required this.state, 10 | required this.percentage, 11 | required this.timeToFull, 12 | required this.timeToEmpty, 13 | }); 14 | 15 | final UPowerDeviceState? state; 16 | final double percentage; 17 | final int timeToFull; 18 | final int timeToEmpty; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | switch (state) { 23 | case UPowerDeviceState.charging: 24 | return Text(context.l10n.batteryCharging(formatTime(timeToFull))); 25 | case UPowerDeviceState.discharging: 26 | case UPowerDeviceState.pendingDischarge: 27 | if (percentage < 20) { 28 | return Text(context.l10n.batteryLow(formatTime(timeToEmpty))); 29 | } 30 | return Text(context.l10n.batteryDischarging(formatTime(timeToEmpty))); 31 | case UPowerDeviceState.fullyCharged: 32 | return Text(context.l10n.batteryFullyCharged); 33 | case UPowerDeviceState.pendingCharge: 34 | return Text(context.l10n.batteryNotCharging); 35 | case UPowerDeviceState.empty: 36 | return Text(context.l10n.batteryEmpty); 37 | case UPowerDeviceState.unknown: 38 | default: 39 | return Text(context.l10n.unknown); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/view/pages/power/lid_close_action.dart: -------------------------------------------------------------------------------- 1 | import 'package:settings/l10n/l10n.dart'; 2 | 3 | enum LidCloseAction { 4 | blank, 5 | suspend, 6 | shutdown, 7 | hibernate, 8 | interactive, 9 | nothing, 10 | logout; 11 | 12 | String localize(AppLocalizations l10n) { 13 | switch (this) { 14 | case LidCloseAction.blank: 15 | return l10n.lidCloseActionBlank; 16 | case LidCloseAction.suspend: 17 | return l10n.lidCloseActionSuspend; 18 | case LidCloseAction.shutdown: 19 | return l10n.lidCloseActionShutdown; 20 | case LidCloseAction.hibernate: 21 | return l10n.lidCloseActionHibernate; 22 | case LidCloseAction.interactive: 23 | return l10n.lidCloseActionInteractive; 24 | case LidCloseAction.nothing: 25 | return l10n.lidCloseActionNothing; 26 | case LidCloseAction.logout: 27 | return l10n.lidCloseActionLogout; 28 | default: 29 | return l10n.unknown; 30 | } 31 | } 32 | } 33 | 34 | extension LidCloseActionString on String { 35 | LidCloseAction? toLidCloseAction() { 36 | try { 37 | return LidCloseAction.values.byName(this); 38 | } on ArgumentError { 39 | return null; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/view/pages/power/lid_close_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:safe_change_notifier/safe_change_notifier.dart'; 2 | import 'package:settings/schemas/schemas.dart'; 3 | import 'package:settings/view/pages/power/lid_close_action.dart'; 4 | import 'package:yaru/yaru.dart'; 5 | 6 | const _lidCloseBatteryActionKey = 'lid-close-battery-action'; 7 | const _lidCloseAcActionKey = 'lid-close-ac-action'; 8 | 9 | class LidCloseModel extends SafeChangeNotifier { 10 | LidCloseModel(GSettingsService settings) 11 | : _daemonSettings = settings.lookup(schemaSettingsDaemonPowerPlugin) { 12 | _daemonSettings?.addListener(notifyListeners); 13 | } 14 | final GnomeSettings? _daemonSettings; 15 | 16 | @override 17 | void dispose() { 18 | _daemonSettings?.removeListener(notifyListeners); 19 | super.dispose(); 20 | } 21 | 22 | LidCloseAction? get acLidCloseAction => 23 | _daemonSettings?.stringValue(_lidCloseAcActionKey)?.toLidCloseAction(); 24 | 25 | set acLidCloseAction(LidCloseAction? value) { 26 | if (value != null) { 27 | _daemonSettings?.setValue(_lidCloseAcActionKey, value.name); 28 | notifyListeners(); 29 | } 30 | } 31 | 32 | LidCloseAction? get batteryLidCloseAction => _daemonSettings 33 | ?.stringValue(_lidCloseBatteryActionKey) 34 | ?.toLidCloseAction(); 35 | 36 | set batteryLidCloseAction(LidCloseAction? value) { 37 | if (value != null) { 38 | _daemonSettings?.setValue(_lidCloseBatteryActionKey, value.name); 39 | notifyListeners(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/view/pages/power/lid_close_section.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:settings/constants.dart'; 4 | import 'package:settings/l10n/l10n.dart'; 5 | import 'package:settings/view/common/settings_section.dart'; 6 | import 'package:settings/view/pages/power/lid_close_action.dart'; 7 | import 'package:settings/view/pages/power/lid_close_model.dart'; 8 | import 'package:watch_it/watch_it.dart'; 9 | import 'package:yaru/yaru.dart'; 10 | 11 | class LidCloseSection extends StatelessWidget { 12 | const LidCloseSection({super.key}); 13 | 14 | static Widget create(BuildContext context) { 15 | return ChangeNotifierProvider( 16 | create: (_) => LidCloseModel(di()), 17 | child: const LidCloseSection(), 18 | ); 19 | } 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | final model = context.watch(); 24 | return SettingsSection( 25 | width: kDefaultWidth, 26 | headline: Text(context.l10n.lidCloseHeadline), 27 | children: [ 28 | YaruTile( 29 | enabled: model.acLidCloseAction != null, 30 | title: Text(context.l10n.lidCloseActionOnAc), 31 | trailing: YaruPopupMenuButton( 32 | enabled: model.acLidCloseAction != null, 33 | initialValue: model.acLidCloseAction, 34 | itemBuilder: (c) => LidCloseAction.values.map((action) { 35 | return PopupMenuItem( 36 | value: action, 37 | child: Text(action.localize(context.l10n)), 38 | onTap: () => model.acLidCloseAction = action, 39 | ); 40 | }).toList(), 41 | child: Text( 42 | model.acLidCloseAction != null 43 | ? model.acLidCloseAction!.localize(context.l10n) 44 | : '', 45 | ), 46 | ), 47 | ), 48 | YaruTile( 49 | enabled: model.batteryLidCloseAction != null, 50 | title: Text(context.l10n.lidCloseActionOnBattery), 51 | trailing: YaruPopupMenuButton( 52 | enabled: model.batteryLidCloseAction != null, 53 | initialValue: model.batteryLidCloseAction, 54 | itemBuilder: (c) => LidCloseAction.values.map((action) { 55 | return PopupMenuItem( 56 | value: action, 57 | child: Text(action.localize(context.l10n)), 58 | onTap: () => model.batteryLidCloseAction = action, 59 | ); 60 | }).toList(), 61 | child: Text( 62 | model.batteryLidCloseAction != null 63 | ? model.batteryLidCloseAction!.localize(context.l10n) 64 | : '', 65 | ), 66 | ), 67 | ), 68 | ], 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/view/pages/power/power_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:settings/l10n/l10n.dart'; 4 | import 'package:settings/view/pages/power/battery_section.dart'; 5 | import 'package:settings/view/pages/power/lid_close_section.dart'; 6 | import 'package:settings/view/pages/power/power_profile_section.dart'; 7 | import 'package:settings/view/pages/power/power_settings_section.dart'; 8 | import 'package:settings/view/pages/power/suspend_section.dart'; 9 | import 'package:settings/view/pages/settings_page.dart'; 10 | 11 | class PowerPage extends StatelessWidget { 12 | const PowerPage({super.key}); 13 | 14 | static Widget create(BuildContext context) => const PowerPage(); 15 | 16 | static Widget createTitle(BuildContext context) => 17 | Text(context.l10n.powerPageTitle); 18 | 19 | static bool searchMatches(String value, BuildContext context) => value 20 | .isNotEmpty 21 | ? context.l10n.powerPageTitle.toLowerCase().contains(value.toLowerCase()) 22 | : false; 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return SettingsPage( 27 | children: [ 28 | BatterySection.create(context), 29 | PowerProfileSection.create(context), 30 | PowerSettingsSection.create(context), 31 | SuspendSection.create(context), 32 | LidCloseSection.create(context), 33 | ], 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/view/pages/power/power_profile_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:safe_change_notifier/safe_change_notifier.dart'; 4 | import 'package:settings/services/power_profile_service.dart'; 5 | 6 | export 'package:settings/services/power_profile_service.dart' show PowerProfile; 7 | 8 | class PowerProfileModel extends SafeChangeNotifier { 9 | PowerProfileModel(this._service); 10 | 11 | final PowerProfileService _service; 12 | StreamSubscription? _subscription; 13 | 14 | void init() { 15 | _service.init().then((_) { 16 | _subscription = _service.profileChanged.listen((_) { 17 | notifyListeners(); 18 | }); 19 | notifyListeners(); 20 | }); 21 | } 22 | 23 | @override 24 | void dispose() { 25 | _subscription?.cancel(); 26 | super.dispose(); 27 | } 28 | 29 | PowerProfile? get profile => _service.profile; 30 | void setProfile(PowerProfile? profile) => _service.setProfile(profile); 31 | } 32 | -------------------------------------------------------------------------------- /lib/view/pages/power/power_profile_section.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:settings/constants.dart'; 4 | import 'package:settings/l10n/l10n.dart'; 5 | import 'package:settings/services/power_profile_service.dart'; 6 | import 'package:settings/view/common/settings_section.dart'; 7 | import 'package:settings/view/pages/power/power_profile_model.dart'; 8 | import 'package:settings/view/pages/power/power_profile_widgets.dart'; 9 | import 'package:settings/view/pages/power/power_profile_x.dart'; 10 | import 'package:watch_it/watch_it.dart'; 11 | import 'package:yaru/yaru.dart'; 12 | 13 | class PowerProfileSection extends StatefulWidget { 14 | const PowerProfileSection({super.key}); 15 | 16 | static Widget create(BuildContext context) { 17 | return ChangeNotifierProvider( 18 | create: (_) => PowerProfileModel(di()), 19 | child: const PowerProfileSection(), 20 | ); 21 | } 22 | 23 | @override 24 | State createState() => _PowerProfileSectionState(); 25 | } 26 | 27 | class _PowerProfileSectionState extends State { 28 | @override 29 | void initState() { 30 | super.initState(); 31 | context.read().init(); 32 | } 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | final model = context.watch(); 37 | return SettingsSection( 38 | width: kDefaultWidth, 39 | headline: Text(context.l10n.powerMode), 40 | children: [ 41 | for (final profile in PowerProfile.values) 42 | ListTile( 43 | shape: 44 | RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), 45 | title: ProfileModeTitle( 46 | powerProfile: profile, 47 | title: Text(profile.localize(context.l10n)), 48 | ), 49 | subtitle: Text(profile.localizeDescription(context.l10n)), 50 | leading: YaruRadio( 51 | value: profile, 52 | groupValue: model.profile, 53 | onChanged: model.setProfile, 54 | ), 55 | ), 56 | ], 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/view/pages/power/power_profile_widgets.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:settings/services/power_profile_service.dart'; 3 | import 'package:settings/view/pages/power/power_profile_x.dart'; 4 | 5 | class ProfileModeTitle extends StatelessWidget { 6 | const ProfileModeTitle({ 7 | super.key, 8 | required this.title, 9 | required this.powerProfile, 10 | }); 11 | 12 | final Widget title; 13 | final PowerProfile powerProfile; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | final theme = Theme.of(context); 18 | return Padding( 19 | padding: const EdgeInsets.only(bottom: 5), 20 | child: Row( 21 | children: [ 22 | Padding( 23 | padding: const EdgeInsets.only(right: 5), 24 | child: Icon( 25 | powerProfile.getIcon(), 26 | color: powerProfile.getColor(theme), 27 | ), 28 | ), 29 | title, 30 | ], 31 | ), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/view/pages/power/power_profile_x.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:settings/l10n/l10n.dart'; 3 | import 'package:settings/services/power_profile_service.dart'; 4 | import 'package:settings/utils.dart'; 5 | import 'package:yaru/yaru.dart'; 6 | 7 | extension PowerProfileX on PowerProfile { 8 | String localize(AppLocalizations l10n) { 9 | switch (this) { 10 | case PowerProfile.performance: 11 | return l10n.powerProfilePerformance; 12 | case PowerProfile.balanced: 13 | return l10n.powerProfileBalanced; 14 | case PowerProfile.powerSaver: 15 | return l10n.powerProfilePowerSaver; 16 | } 17 | } 18 | 19 | String localizeDescription(AppLocalizations l10n) { 20 | switch (this) { 21 | case PowerProfile.performance: 22 | return l10n.powerProfilePerformanceDescription; 23 | case PowerProfile.balanced: 24 | return l10n.powerProfileBalancedDescription; 25 | case PowerProfile.powerSaver: 26 | return l10n.powerProfilePowerSaverDescription; 27 | } 28 | } 29 | 30 | IconData getIcon() { 31 | switch (this) { 32 | case PowerProfile.performance: 33 | return YaruIcons.meter_5; 34 | case PowerProfile.balanced: 35 | return YaruIcons.meter_3; 36 | case PowerProfile.powerSaver: 37 | return YaruIcons.meter_1; 38 | } 39 | } 40 | 41 | Color getColor(ThemeData theme) { 42 | final light = theme.brightness == Brightness.light; 43 | switch (this) { 44 | case PowerProfile.performance: 45 | return light ? YaruColors.red : lighten(YaruColors.red, 30); 46 | case PowerProfile.balanced: 47 | return light ? YaruColors.inkstone : YaruColors.porcelain; 48 | case PowerProfile.powerSaver: 49 | return light 50 | ? theme.colorScheme.success 51 | : lighten(theme.colorScheme.success, 30); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/view/pages/power/power_settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:settings/l10n/l10n.dart'; 3 | 4 | enum AutomaticSuspend { 5 | off, 6 | battery, 7 | pluggedIn, 8 | both, 9 | } 10 | 11 | extension AutomaticSuspendL10n on AutomaticSuspend { 12 | String localize(BuildContext context) { 13 | switch (this) { 14 | case AutomaticSuspend.off: 15 | return context.l10n.powerAutomaticSuspendOff; 16 | case AutomaticSuspend.battery: 17 | return context.l10n.powerAutomaticSuspendBattery; 18 | case AutomaticSuspend.pluggedIn: 19 | return context.l10n.powerAutomaticSuspendPluggedIn; 20 | case AutomaticSuspend.both: 21 | return context.l10n.powerAutomaticSuspendBoth; 22 | default: 23 | return context.l10n.unknown; 24 | } 25 | } 26 | } 27 | 28 | enum SleepInactiveType { 29 | blank, 30 | suspend, 31 | shutdown, 32 | hibernate, 33 | interactive, 34 | nothing, 35 | logout, 36 | } 37 | 38 | extension SleepInactiveTypeString on String { 39 | SleepInactiveType? toSleepInactiveType() { 40 | try { 41 | return SleepInactiveType.values.byName(this); 42 | } on ArgumentError { 43 | return null; 44 | } 45 | } 46 | } 47 | 48 | class IdleDelay { 49 | static const values = [ 50 | 1 * 60, 51 | 2 * 60, 52 | 3 * 60, 53 | 4 * 60, 54 | 5 * 60, 55 | 8 * 60, 56 | 10 * 60, 57 | 12 * 60, 58 | 15 * 60, 59 | 0, 60 | ]; 61 | 62 | static int? validate(int? delay) => values.contains(delay) ? delay : null; 63 | } 64 | 65 | class SuspendDelay { 66 | static const values = [ 67 | 15 * 60, 68 | 20 * 60, 69 | 25 * 60, 70 | 30 * 60, 71 | 45 * 60, 72 | 60 * 60, 73 | 80 * 60, 74 | 90 * 60, 75 | 100 * 60, 76 | 120 * 60, 77 | ]; 78 | 79 | static int? validate(int? delay) => values.contains(delay) ? delay : null; 80 | } 81 | 82 | class ScreenLockDelay { 83 | static const values = [ 84 | 30, 85 | 1 * 60, 86 | 2 * 60, 87 | 3 * 60, 88 | 5 * 60, 89 | 30 * 60, 90 | 60 * 60, 91 | 0, 92 | ]; 93 | 94 | static int? validate(int? delay) => values.contains(delay) ? delay : null; 95 | } 96 | -------------------------------------------------------------------------------- /lib/view/pages/power/suspend.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:settings/l10n/l10n.dart'; 3 | 4 | enum PowerButtonAction { 5 | nothing, 6 | suspend, 7 | hibernate, 8 | interactive, 9 | } 10 | 11 | extension PowerButtonActionString on String { 12 | PowerButtonAction? toPowerButtonAction() { 13 | try { 14 | return PowerButtonAction.values.byName(this); 15 | } on ArgumentError { 16 | return null; 17 | } 18 | } 19 | } 20 | 21 | extension PowerButtonActionL10n on PowerButtonAction { 22 | String localize(BuildContext context) { 23 | switch (this) { 24 | case PowerButtonAction.nothing: 25 | return context.l10n.powerButtonActionNothing; 26 | case PowerButtonAction.suspend: 27 | return context.l10n.powerButtonActionSuspend; 28 | case PowerButtonAction.hibernate: 29 | return context.l10n.powerButtonActionHibernate; 30 | case PowerButtonAction.interactive: 31 | return context.l10n.powerButtonActionPowerOff; 32 | default: 33 | return context.l10n.unknown; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/view/pages/power/suspend_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:safe_change_notifier/safe_change_notifier.dart'; 2 | import 'package:settings/schemas/schemas.dart'; 3 | import 'package:settings/view/pages/power/suspend.dart'; 4 | import 'package:yaru/yaru.dart'; 5 | 6 | const _showBatteryPercentageKey = 'show-battery-percentage'; 7 | const _powerButtonActionKey = 'power-button-action'; 8 | 9 | class SuspendModel extends SafeChangeNotifier { 10 | SuspendModel(GSettingsService settings) 11 | : _daemonSettings = settings.lookup(schemaSettingsDaemonPowerPlugin), 12 | _interfaceSettings = settings.lookup(schemaInterface) { 13 | _daemonSettings?.addListener(notifyListeners); 14 | _interfaceSettings?.addListener(notifyListeners); 15 | } 16 | 17 | @override 18 | void dispose() { 19 | _daemonSettings?.removeListener(notifyListeners); 20 | _interfaceSettings?.removeListener(notifyListeners); 21 | super.dispose(); 22 | } 23 | 24 | final GnomeSettings? _daemonSettings; 25 | final GnomeSettings? _interfaceSettings; 26 | 27 | bool? get showBatteryPercentage => 28 | _interfaceSettings?.boolValue(_showBatteryPercentageKey); 29 | 30 | void setShowBatteryPercentage(bool value) { 31 | _interfaceSettings?.setValue(_showBatteryPercentageKey, value); 32 | notifyListeners(); 33 | } 34 | 35 | PowerButtonAction? get _realPowerButtonAction => _daemonSettings 36 | ?.stringValue(_powerButtonActionKey) 37 | ?.toPowerButtonAction(); 38 | 39 | PowerButtonAction? get powerButtonAction => 40 | PowerButtonAction.values.contains(_realPowerButtonAction) 41 | ? _realPowerButtonAction 42 | : null; 43 | 44 | void setPowerButtonAction(PowerButtonAction? value) { 45 | if (value == null) return; 46 | _daemonSettings?.setValue(_powerButtonActionKey, value.name); 47 | notifyListeners(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/view/pages/power/suspend_section.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:settings/constants.dart'; 4 | import 'package:settings/l10n/l10n.dart'; 5 | import 'package:settings/view/common/settings_section.dart'; 6 | import 'package:settings/view/common/yaru_switch_row.dart'; 7 | import 'package:settings/view/pages/power/suspend.dart'; 8 | import 'package:settings/view/pages/power/suspend_model.dart'; 9 | import 'package:watch_it/watch_it.dart'; 10 | import 'package:yaru/yaru.dart'; 11 | 12 | class SuspendSection extends StatefulWidget { 13 | const SuspendSection({super.key}); 14 | 15 | static Widget create(BuildContext context) { 16 | return ChangeNotifierProvider( 17 | create: (_) => SuspendModel( 18 | di(), 19 | ), 20 | child: const SuspendSection(), 21 | ); 22 | } 23 | 24 | @override 25 | State createState() => _SuspendSectionState(); 26 | } 27 | 28 | class _SuspendSectionState extends State { 29 | @override 30 | Widget build(BuildContext context) { 31 | final model = context.watch(); 32 | return SettingsSection( 33 | width: kDefaultWidth, 34 | headline: Text(context.l10n.powerSuspendHeadline), 35 | children: [ 36 | YaruTile( 37 | enabled: model.powerButtonAction != null, 38 | title: Text(context.l10n.powerButtonBehavior), 39 | trailing: YaruPopupMenuButton( 40 | initialValue: model.powerButtonAction, 41 | itemBuilder: (c) => PowerButtonAction.values.map((action) { 42 | return PopupMenuItem( 43 | value: action, 44 | child: Text(action.localize(context)), 45 | onTap: () => model.setPowerButtonAction(action), 46 | ); 47 | }).toList(), 48 | child: Text( 49 | model.powerButtonAction != null 50 | ? model.powerButtonAction!.localize(context) 51 | : '', 52 | ), 53 | ), 54 | ), 55 | YaruSwitchRow( 56 | trailingWidget: Text(context.l10n.batteryShowPercentage), 57 | value: model.showBatteryPercentage, 58 | onChanged: model.setShowBatteryPercentage, 59 | ), 60 | ], 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/view/pages/privacy/connectivity_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:nm/nm.dart'; 2 | import 'package:safe_change_notifier/safe_change_notifier.dart'; 3 | 4 | class ConnectivityModel extends SafeChangeNotifier { 5 | ConnectivityModel(NetworkManagerClient client) : _client = client; 6 | final NetworkManagerClient _client; 7 | 8 | Future init() { 9 | final network = _client.connect(); 10 | return Future.wait([network]); 11 | } 12 | 13 | bool? get checkConnectiviy => _client.connectivityCheckEnabled; 14 | set checkConnectiviy(bool? value) { 15 | if (value == null) return; 16 | _client 17 | .setConnectivityCheckEnabled(value) 18 | .then((value) => notifyListeners()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/view/pages/privacy/connectivity_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:nm/nm.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:settings/constants.dart'; 5 | import 'package:settings/l10n/l10n.dart'; 6 | import 'package:settings/view/common/section_description.dart'; 7 | import 'package:settings/view/common/settings_section.dart'; 8 | import 'package:settings/view/common/yaru_switch_row.dart'; 9 | import 'package:settings/view/pages/privacy/connectivity_model.dart'; 10 | import 'package:settings/view/pages/settings_page.dart'; 11 | import 'package:watch_it/watch_it.dart'; 12 | 13 | class ConnectivityPage extends StatefulWidget { 14 | const ConnectivityPage({super.key}); 15 | 16 | static Widget create(BuildContext context) => ChangeNotifierProvider( 17 | create: (_) => ConnectivityModel(di()), 18 | child: const ConnectivityPage(), 19 | ); 20 | 21 | @override 22 | State createState() => _ConnectivityPageState(); 23 | } 24 | 25 | class _ConnectivityPageState extends State { 26 | @override 27 | void initState() { 28 | final model = context.read(); 29 | model.init(); 30 | super.initState(); 31 | } 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | final model = context.watch(); 36 | return SettingsPage( 37 | children: [ 38 | SectionDescription( 39 | width: kDefaultWidth, 40 | text: context.l10n.checkConnectivityDescription, 41 | ), 42 | SettingsSection( 43 | width: kDefaultWidth, 44 | children: [ 45 | YaruSwitchRow( 46 | enabled: model.checkConnectiviy != null, 47 | trailingWidget: Text(context.l10n.checkConnectivityLabel), 48 | value: model.checkConnectiviy, 49 | onChanged: (v) => model.checkConnectiviy = v, 50 | ), 51 | ], 52 | ), 53 | ], 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/view/pages/privacy/location_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:safe_change_notifier/safe_change_notifier.dart'; 2 | import 'package:settings/schemas/schemas.dart'; 3 | import 'package:yaru/yaru.dart'; 4 | 5 | const _enabledKey = 'enabled'; 6 | 7 | class LocationModel extends SafeChangeNotifier { 8 | LocationModel(GSettingsService service) 9 | : _locationSettings = service.lookup(schemaLocation) { 10 | _locationSettings?.addListener(notifyListeners); 11 | } 12 | final GnomeSettings? _locationSettings; 13 | 14 | bool? get enabled => _locationSettings?.getValue(_enabledKey); 15 | set enabled(bool? value) { 16 | if (value == null) return; 17 | _locationSettings?.setValue(_enabledKey, value); 18 | notifyListeners(); 19 | } 20 | 21 | @override 22 | void dispose() { 23 | _locationSettings?.removeListener(notifyListeners); 24 | super.dispose(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/view/pages/privacy/location_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:settings/constants.dart'; 4 | import 'package:settings/l10n/l10n.dart'; 5 | import 'package:settings/view/common/link.dart'; 6 | import 'package:settings/view/common/section_description.dart'; 7 | import 'package:settings/view/common/settings_section.dart'; 8 | import 'package:settings/view/common/yaru_switch_row.dart'; 9 | import 'package:settings/view/pages/privacy/location_model.dart'; 10 | import 'package:settings/view/pages/settings_page.dart'; 11 | import 'package:watch_it/watch_it.dart'; 12 | import 'package:yaru/yaru.dart'; 13 | 14 | const kPrivacyUrl = 'https://location.services.mozilla.com/privacy'; 15 | 16 | class LocationPage extends StatelessWidget { 17 | const LocationPage({super.key}); 18 | 19 | static Widget create(BuildContext context) => 20 | ChangeNotifierProvider( 21 | create: (_) => LocationModel(di()), 22 | child: const LocationPage(), 23 | ); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | final model = context.watch(); 28 | return SettingsPage( 29 | children: [ 30 | SectionDescription( 31 | width: kDefaultWidth, 32 | text: context.l10n.locationDescription, 33 | ), 34 | Padding( 35 | padding: const EdgeInsets.only(bottom: 20), 36 | child: SizedBox( 37 | width: kDefaultWidth, 38 | child: Row( 39 | children: [ 40 | Text( 41 | context.l10n.locationInfoPrefix, 42 | style: Theme.of(context).textTheme.bodySmall, 43 | ), 44 | const SizedBox( 45 | width: 5, 46 | ), 47 | Link(url: kPrivacyUrl, linkText: context.l10n.locationInfoLink), 48 | ], 49 | ), 50 | ), 51 | ), 52 | SettingsSection( 53 | width: kDefaultWidth, 54 | children: [ 55 | YaruSwitchRow( 56 | trailingWidget: Text(context.l10n.locationActionLabel), 57 | value: model.enabled, 58 | onChanged: (v) => model.enabled = v, 59 | ), 60 | ], 61 | ), 62 | ], 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/view/pages/privacy/privacy_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:safe_change_notifier/safe_change_notifier.dart'; 2 | import 'package:settings/schemas/schemas.dart'; 3 | import 'package:settings/services/house_keeping_service.dart'; 4 | import 'package:yaru/yaru.dart'; 5 | 6 | const _removeOldTrashFilesKey = 'remove-old-trash-files'; 7 | const _removeOldTempFilesKey = 'remove-old-temp-files'; 8 | const _rememberRecentFilesKey = 'remember-recent-files'; 9 | const _recentFilesMaxAgeKey = 'recent-files-max-age'; 10 | const _oldFilesAgeKey = 'old-files-age'; 11 | 12 | class PrivacyModel extends SafeChangeNotifier { 13 | PrivacyModel( 14 | GSettingsService settingsService, 15 | HouseKeepingService houseKeepingService, 16 | ) : _privacySettings = settingsService.lookup(schemaPrivacy), 17 | _houseKeepingService = houseKeepingService { 18 | _privacySettings?.addListener(notifyListeners); 19 | } 20 | final GnomeSettings? _privacySettings; 21 | final HouseKeepingService _houseKeepingService; 22 | 23 | @override 24 | void dispose() { 25 | _privacySettings?.removeListener(notifyListeners); 26 | super.dispose(); 27 | } 28 | 29 | bool? get removeOldTrashFiles => 30 | _privacySettings?.getValue(_removeOldTrashFilesKey); 31 | 32 | set removeOldTrashFiles(bool? value) { 33 | if (value == null) return; 34 | _privacySettings?.setValue(_removeOldTrashFilesKey, value); 35 | notifyListeners(); 36 | } 37 | 38 | bool? get removeOldTempFiles => 39 | _privacySettings?.getValue(_removeOldTempFilesKey); 40 | 41 | set removeOldTempFiles(bool? value) { 42 | if (value == null) return; 43 | _privacySettings?.setValue(_removeOldTempFilesKey, value); 44 | notifyListeners(); 45 | } 46 | 47 | bool? get rememberRecentFiles => 48 | _privacySettings?.getValue(_rememberRecentFilesKey); 49 | 50 | set rememberRecentFiles(bool? value) { 51 | if (value == null) return; 52 | _privacySettings?.setValue(_rememberRecentFilesKey, value); 53 | notifyListeners(); 54 | } 55 | 56 | int? get recentFilesMaxAge => 57 | _privacySettings?.getValue(_recentFilesMaxAgeKey); 58 | set recentFilesMaxAge(int? value) { 59 | if (value == null) return; 60 | _privacySettings?.setValue(_recentFilesMaxAgeKey, value); 61 | notifyListeners(); 62 | } 63 | 64 | int? get oldFilesAge => _privacySettings?.getValue(_oldFilesAgeKey); 65 | set oldFilesAge(int? value) { 66 | if (value == null) return; 67 | _privacySettings?.setUint32Value(_oldFilesAgeKey, value); 68 | notifyListeners(); 69 | } 70 | 71 | void emptyTrash() => _houseKeepingService.emptyTrash(); 72 | 73 | void removeTempFiles() => _houseKeepingService.removeTempFiles(); 74 | 75 | void clearRecentlyUsed() => _houseKeepingService.clearRecentlyUsed(); 76 | } 77 | -------------------------------------------------------------------------------- /lib/view/pages/privacy/reporting_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:safe_change_notifier/safe_change_notifier.dart'; 2 | import 'package:settings/schemas/schemas.dart'; 3 | import 'package:yaru/yaru.dart'; 4 | 5 | const _reportTechnicalProblemsKey = 'report-technical-problems'; 6 | const _sendSoftwareUsageStatsKey = 'send-software-usage-stats'; 7 | 8 | class ReportingModel extends SafeChangeNotifier { 9 | ReportingModel(GSettingsService service) 10 | : _privacySettings = service.lookup(schemaPrivacy) { 11 | _privacySettings?.addListener(notifyListeners); 12 | } 13 | final GnomeSettings? _privacySettings; 14 | 15 | @override 16 | void dispose() { 17 | _privacySettings?.removeListener(notifyListeners); 18 | super.dispose(); 19 | } 20 | 21 | bool? get reportTechnicalProblems => 22 | _privacySettings?.getValue(_reportTechnicalProblemsKey); 23 | set reportTechnicalProblems(bool? value) { 24 | if (value == null) return; 25 | _privacySettings?.setValue(_reportTechnicalProblemsKey, value); 26 | notifyListeners(); 27 | } 28 | 29 | bool? get sendSoftwareUsageStats => 30 | _privacySettings?.getValue(_sendSoftwareUsageStatsKey); 31 | set sendSoftwareUsageStats(bool? value) { 32 | if (value == null) return; 33 | _privacySettings?.setValue(_sendSoftwareUsageStatsKey, value); 34 | notifyListeners(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/view/pages/privacy/reporting_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:settings/constants.dart'; 4 | import 'package:settings/l10n/l10n.dart'; 5 | import 'package:settings/view/common/link.dart'; 6 | import 'package:settings/view/common/section_description.dart'; 7 | import 'package:settings/view/common/settings_section.dart'; 8 | import 'package:settings/view/common/yaru_switch_row.dart'; 9 | import 'package:settings/view/pages/privacy/reporting_model.dart'; 10 | import 'package:settings/view/pages/settings_page.dart'; 11 | import 'package:watch_it/watch_it.dart'; 12 | import 'package:yaru/yaru.dart'; 13 | 14 | const kUbuntuReportingLink = 'https://ubuntu.com/legal/data-privacy'; 15 | 16 | class ReportingPage extends StatelessWidget { 17 | const ReportingPage({super.key}); 18 | 19 | static Widget create(BuildContext context) => ChangeNotifierProvider( 20 | create: (_) => ReportingModel( 21 | di(), 22 | ), 23 | child: const ReportingPage(), 24 | ); 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | final model = context.watch(); 29 | return SettingsPage( 30 | children: [ 31 | SectionDescription( 32 | width: kDefaultWidth, 33 | text: context.l10n.reportingDescription, 34 | ), 35 | Padding( 36 | padding: const EdgeInsets.only(bottom: 20), 37 | child: SizedBox( 38 | width: kDefaultWidth, 39 | child: Row( 40 | children: [ 41 | Link( 42 | url: kUbuntuReportingLink, 43 | linkText: context.l10n.reportingLink, 44 | ), 45 | ], 46 | ), 47 | ), 48 | ), 49 | SettingsSection( 50 | width: kDefaultWidth, 51 | children: [ 52 | YaruSwitchRow( 53 | trailingWidget: Text(context.l10n.reportingActionLabel), 54 | value: model.reportTechnicalProblems, 55 | onChanged: (value) => model.reportTechnicalProblems = value, 56 | ), 57 | YaruSwitchRow( 58 | trailingWidget: Text(context.l10n.reportingUsageActionLabel), 59 | value: model.sendSoftwareUsageStats, 60 | onChanged: (value) => model.sendSoftwareUsageStats = value, 61 | ), 62 | ], 63 | ), 64 | ], 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/view/pages/privacy/screen_saver_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:safe_change_notifier/safe_change_notifier.dart'; 2 | import 'package:settings/schemas/schemas.dart'; 3 | import 'package:settings/view/pages/power/power_settings.dart'; 4 | import 'package:yaru/yaru.dart'; 5 | 6 | const _lockEnabledKey = 'lock-enabled'; 7 | const _lockDelayKey = 'lock-delay'; 8 | const _ubuntuLockOnSuspendKey = 'ubuntu-lock-on-suspend'; 9 | const _showInLockScreenKey = 'show-in-lock-screen'; 10 | const _idleDelayKey = 'idle-delay'; 11 | 12 | class ScreenSaverModel extends SafeChangeNotifier { 13 | ScreenSaverModel(GSettingsService service) 14 | : _screenSaverSettings = service.lookup(schemaScreenSaver), 15 | _notificationSettings = service.lookup(schemaNotifications), 16 | _sessionSettings = service.lookup(schemaSession) { 17 | _screenSaverSettings?.addListener(notifyListeners); 18 | _notificationSettings?.addListener(notifyListeners); 19 | _sessionSettings?.addListener(notifyListeners); 20 | } 21 | final GnomeSettings? _screenSaverSettings; 22 | final GnomeSettings? _notificationSettings; 23 | final GnomeSettings? _sessionSettings; 24 | 25 | @override 26 | void dispose() { 27 | _screenSaverSettings?.removeListener(notifyListeners); 28 | _notificationSettings?.removeListener(notifyListeners); 29 | _sessionSettings?.removeListener(notifyListeners); 30 | 31 | super.dispose(); 32 | } 33 | 34 | bool? get lockEnabled => _screenSaverSettings?.boolValue(_lockEnabledKey); 35 | set lockEnabled(bool? value) { 36 | if (value == null) return; 37 | _screenSaverSettings?.setValue(_lockEnabledKey, value); 38 | notifyListeners(); 39 | } 40 | 41 | bool? get showOnLockScreen => 42 | _notificationSettings?.boolValue(_showInLockScreenKey); 43 | set showOnLockScreen(bool? value) { 44 | if (value == null) return; 45 | _notificationSettings?.setValue(_showInLockScreenKey, value); 46 | notifyListeners(); 47 | } 48 | 49 | bool? get ubuntuLockOnSuspend => 50 | _screenSaverSettings?.boolValue(_ubuntuLockOnSuspendKey); 51 | set ubuntuLockOnSuspend(bool? value) { 52 | if (value == null) return; 53 | _screenSaverSettings?.setValue(_ubuntuLockOnSuspendKey, value); 54 | notifyListeners(); 55 | } 56 | 57 | int? get _realLockDelay => _screenSaverSettings?.intValue(_lockDelayKey); 58 | int? get lockDelay => 59 | ScreenLockDelay.values.contains(_realLockDelay) ? _realLockDelay : null; 60 | set lockDelay(int? value) { 61 | if (value == null) return; 62 | _screenSaverSettings?.setUint32Value(_lockDelayKey, value); 63 | notifyListeners(); 64 | } 65 | 66 | int? get _realIdleDelay => 67 | IdleDelay.validate(_sessionSettings?.intValue(_idleDelayKey)); 68 | int? get idleDelay => 69 | IdleDelay.values.contains(_realIdleDelay) ? _realIdleDelay : null; 70 | void setIdleDelay(int? value) { 71 | if (value == null) return; 72 | _sessionSettings?.setUint32Value(_idleDelayKey, value); 73 | notifyListeners(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/view/pages/region_and_language/region_and_language_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | 5 | import 'package:safe_change_notifier/safe_change_notifier.dart'; 6 | import 'package:settings/services/locale_service.dart'; 7 | 8 | class RegionAndLanguageModel extends SafeChangeNotifier { 9 | RegionAndLanguageModel({required LocaleService localeService}) 10 | : _localeService = localeService; 11 | final LocaleService _localeService; 12 | List? get locales => _localeService.locales; 13 | List installedLocales = []; 14 | 15 | StreamSubscription? _localeSub; 16 | 17 | Future init() { 18 | _initInstalledLocales(); 19 | return _localeService.init().then((_) async { 20 | _localeSub = 21 | _localeService.localeChanged.listen((_) => notifyListeners()); 22 | notifyListeners(); 23 | }); 24 | } 25 | 26 | String get locale => 27 | _localeService.locale != null && _localeService.locale!.first != null 28 | ? _localeService.locale!.first! 29 | : ''; 30 | set locale(String locale) => 31 | _localeService.locale = ['LANG=$locale'.replaceAll('utf8', 'UTF-8')]; 32 | 33 | String get prettyLocale => 34 | locale.replaceAll('.UTF-8', '').replaceAll('LANG=', ''); 35 | 36 | @override 37 | void dispose() async { 38 | await _localeSub?.cancel(); 39 | super.dispose(); 40 | } 41 | 42 | void openGnomeLanguageSelector() { 43 | Process.run('gnome-language-selector', []); 44 | } 45 | 46 | void _initInstalledLocales() async { 47 | await Process.run('locale', ['-a']).then((value) { 48 | installedLocales = const LineSplitter().convert(value.stdout); 49 | installedLocales.retainWhere((element) => element.endsWith('.utf8')); 50 | installedLocales = 51 | installedLocales.map((e) => e.replaceAll('.utf8', '')).toList(); 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/view/pages/search/search_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:settings/l10n/l10n.dart'; 3 | import 'package:settings/view/pages/settings_page.dart'; 4 | 5 | class SearchPage extends StatelessWidget { 6 | const SearchPage({super.key}); 7 | 8 | static Widget create(BuildContext context) => const SearchPage(); 9 | 10 | static Widget createTitle(BuildContext context) => 11 | Text(context.l10n.searchPageTitle); 12 | 13 | static bool searchMatches(String value, BuildContext context) => value 14 | .isNotEmpty 15 | ? context.l10n.searchPageTitle.toLowerCase().contains(value.toLowerCase()) 16 | : false; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return SettingsPage( 21 | children: [Center(child: Text(context.l10n.searchPageTitle))], 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/view/pages/settings_alert_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:settings/constants.dart'; 3 | import 'package:yaru/yaru.dart'; 4 | 5 | class SettingsAlertDialog extends StatelessWidget { 6 | const SettingsAlertDialog({ 7 | super.key, 8 | required this.title, 9 | required this.child, 10 | this.closeIconData, 11 | this.alignment, 12 | this.width, 13 | this.height, 14 | this.titleTextAlign, 15 | this.actions, 16 | this.contentPadding = EdgeInsets.zero, 17 | this.scrollable = false, 18 | }); 19 | 20 | /// The title of the dialog, displayed in a large font at the top of the [YaruDialogTitle]. 21 | final String title; 22 | 23 | /// The icon used inside the close button 24 | final IconData? closeIconData; 25 | 26 | /// The child displayed underneath the title. It comes without any padding 27 | /// or [ScrollView] so one has the full freedom to put anything inside. 28 | final Widget child; 29 | 30 | /// How to align the [Dialog] on the Screen. 31 | /// 32 | /// If null, then [DialogTheme.alignment] is used. If that is also null, the 33 | /// default is [Alignment.center]. 34 | final AlignmentGeometry? alignment; 35 | 36 | /// The width of the dialog which can be provided and constraints all children with the same width. 37 | /// 38 | /// Default is [kYaruPageWidth] 39 | final double? width; 40 | 41 | /// The optional height of the dialog which can be provided to limit the height 42 | /// of the [SingleChildScrollView] where the [children] are placed. 43 | final double? height; 44 | 45 | /// Optional [TextAlign] used for the [YaruDialogTitle] 46 | final TextAlign? titleTextAlign; 47 | 48 | /// A [List] of [Widget] - typically [OutlinedButton], [ElevatedButton] or [TextButton] 49 | final List? actions; 50 | 51 | /// Padding around the [content] 52 | /// 53 | /// Defaults to [EdgeInsets.zero] 54 | final EdgeInsetsGeometry contentPadding; 55 | 56 | /// Forwards the [scrollable] flag to the [AlertDialog] 57 | final bool? scrollable; 58 | 59 | @override 60 | Widget build(BuildContext context) { 61 | return SizedBox( 62 | width: width ?? kDefaultWidth, 63 | child: AlertDialog( 64 | actionsPadding: const EdgeInsets.all(10), 65 | contentPadding: contentPadding, 66 | scrollable: scrollable ?? false, 67 | titlePadding: EdgeInsets.zero, 68 | title: YaruTitleBar( 69 | title: Text(title), 70 | ), 71 | content: SizedBox(height: height, width: width, child: child), 72 | actions: actions, 73 | alignment: alignment, 74 | ), 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/view/pages/settings_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:yaru/yaru.dart'; 3 | 4 | /// Wraps a list of children widget in a [Column], [SingleChildScrollView] and [Padding]. 5 | /// The padding defaults to [kYaruPagePadding] 6 | /// but can be set if wanted. 7 | class SettingsPage extends StatelessWidget { 8 | const SettingsPage({ 9 | super.key, 10 | required this.children, 11 | this.padding, 12 | this.controller, 13 | }); 14 | 15 | final List children; 16 | final EdgeInsets? padding; 17 | final ScrollController? controller; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return SingleChildScrollView( 22 | controller: controller, 23 | child: Center( 24 | child: Padding( 25 | padding: padding ?? const EdgeInsets.all(kYaruPagePadding), 26 | child: Column( 27 | children: children, 28 | ), 29 | ), 30 | ), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/view/pages/settings_page_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | class SettingsPageItem { 4 | SettingsPageItem({ 5 | required this.titleBuilder, 6 | required this.builder, 7 | required this.iconBuilder, 8 | this.searchMatches, 9 | this.title, 10 | this.hasAppBar, 11 | }); 12 | 13 | final WidgetBuilder titleBuilder; 14 | final String? title; 15 | final WidgetBuilder builder; 16 | final Widget Function(BuildContext context, bool selected) iconBuilder; 17 | final bool Function(String value, BuildContext context)? searchMatches; 18 | final bool? hasAppBar; 19 | } 20 | -------------------------------------------------------------------------------- /lib/view/pages/settings_simple_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:yaru/yaru.dart'; 3 | 4 | class SettingsSimpleDialog extends StatelessWidget { 5 | /// Create a [SimpleDialog] with a close button 6 | const SettingsSimpleDialog({ 7 | super.key, 8 | required this.title, 9 | required this.closeIconData, 10 | required this.children, 11 | this.semanticLabel, 12 | this.alignment, 13 | required this.width, 14 | this.titleTextAlign = TextAlign.center, 15 | }); 16 | 17 | /// The title of the dialog, displayed in a large font at the top of the [YaruDialogTitle]. 18 | final String title; 19 | 20 | /// The icon used inside the close button 21 | final IconData closeIconData; 22 | 23 | /// The content of the dialog, displayed underneath the title. 24 | final List children; 25 | 26 | /// The semantic label of the dialog used by accessibility frameworks to 27 | /// announce screen transitions when the dialog is opened and closed. 28 | /// 29 | /// If this label is not provided, a semantic label will be inferred from the 30 | /// [title] if it is not null. If there is no title, the label will be taken 31 | /// from [MaterialLocalizations.dialogLabel]. 32 | /// 33 | /// See also: 34 | /// 35 | /// * [SemanticsConfiguration.namesRoute], for a description of how this 36 | /// value is used. 37 | final String? semanticLabel; 38 | 39 | /// How to align the [Dialog] on the Screen. 40 | /// 41 | /// If null, then [DialogTheme.alignment] is used. If that is also null, the 42 | /// default is [Alignment.center]. 43 | final AlignmentGeometry? alignment; 44 | 45 | /// The width of the dialog which must be provided and constraints all children with the same width. 46 | /// 47 | final double width; 48 | 49 | /// Optional [TextAlign] used for the [YaruDialogTitle] 50 | final TextAlign? titleTextAlign; 51 | 52 | @override 53 | Widget build(BuildContext context) { 54 | return SizedBox( 55 | width: width, 56 | child: SimpleDialog( 57 | titlePadding: EdgeInsets.zero, 58 | title: YaruTitleBar( 59 | title: Text(title), 60 | ), 61 | contentPadding: const EdgeInsets.fromLTRB( 62 | kYaruPagePadding, 63 | kYaruPagePadding, 64 | kYaruPagePadding, 65 | kYaruPagePadding, 66 | ), 67 | semanticLabel: semanticLabel, 68 | alignment: alignment, 69 | children: [ 70 | for (final child in children) SizedBox(width: width, child: child), 71 | ], 72 | ), 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/view/pages/sound/sound_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:safe_change_notifier/safe_change_notifier.dart'; 2 | import 'package:settings/schemas/schemas.dart'; 3 | import 'package:yaru/settings.dart'; 4 | 5 | class SoundModel extends SafeChangeNotifier { 6 | SoundModel(GSettingsService service) 7 | : _soundSettings = service.lookup(schemaSound) { 8 | _soundSettings?.addListener(notifyListeners); 9 | } 10 | static const _allowAbove100Key = 'allow-volume-above-100-percent'; 11 | static const _eventSoundsKey = 'event-sounds'; 12 | static const _inputFeedbackSounds = 'input-feedback-sounds'; 13 | 14 | @override 15 | void dispose() { 16 | _soundSettings?.removeListener(notifyListeners); 17 | super.dispose(); 18 | } 19 | 20 | final GnomeSettings? _soundSettings; 21 | 22 | // System section 23 | 24 | bool? get allowAbove100 => _soundSettings?.boolValue(_allowAbove100Key); 25 | 26 | void setAllowAbove100(bool value) { 27 | _soundSettings?.setValue(_allowAbove100Key, value); 28 | notifyListeners(); 29 | } 30 | 31 | bool? get eventSounds => _soundSettings?.boolValue(_eventSoundsKey); 32 | 33 | void setEventSounds(bool value) { 34 | _soundSettings?.setValue(_eventSoundsKey, value); 35 | notifyListeners(); 36 | } 37 | 38 | bool? get inputFeedbackSounds => 39 | _soundSettings?.boolValue(_inputFeedbackSounds); 40 | 41 | void setInputFeedbackSounds(bool value) { 42 | _soundSettings?.setValue(_inputFeedbackSounds, value); 43 | notifyListeners(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/view/pages/sound/sound_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:settings/constants.dart'; 4 | import 'package:settings/l10n/l10n.dart'; 5 | import 'package:settings/view/common/settings_section.dart'; 6 | import 'package:settings/view/common/yaru_switch_row.dart'; 7 | import 'package:settings/view/pages/settings_page.dart'; 8 | import 'package:settings/view/pages/sound/sound_model.dart'; 9 | import 'package:watch_it/watch_it.dart'; 10 | import 'package:yaru/yaru.dart'; 11 | 12 | class SoundPage extends StatelessWidget { 13 | const SoundPage({super.key}); 14 | 15 | static Widget create(BuildContext context) { 16 | return ChangeNotifierProvider( 17 | create: (_) => SoundModel(di()), 18 | child: const SoundPage(), 19 | ); 20 | } 21 | 22 | static Widget createTitle(BuildContext context) => 23 | Text(context.l10n.soundPageTitle); 24 | 25 | static bool searchMatches(String value, BuildContext context) => value 26 | .isNotEmpty 27 | ? context.l10n.soundPageTitle.toLowerCase().contains(value.toLowerCase()) 28 | : false; 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | final model = context.watch(); 33 | 34 | return SettingsPage( 35 | children: [ 36 | SettingsSection( 37 | width: kDefaultWidth, 38 | headline: const Text('System'), 39 | children: [ 40 | YaruSwitchRow( 41 | trailingWidget: const Text('Allow Volume Above 100%'), 42 | value: model.allowAbove100, 43 | onChanged: model.setAllowAbove100, 44 | ), 45 | YaruSwitchRow( 46 | trailingWidget: const Text('Event Sounds'), 47 | actionDescription: 48 | 'Notify of a system action, notification or event', 49 | value: model.eventSounds, 50 | onChanged: model.setEventSounds, 51 | ), 52 | YaruSwitchRow( 53 | trailingWidget: const Text('Input Feedback Sounds'), 54 | actionDescription: 'Feedback for user input events, ' 55 | 'such as mouse clicks, or key presses', 56 | value: model.inputFeedbackSounds, 57 | onChanged: model.setInputFeedbackSounds, 58 | ), 59 | ], 60 | ), 61 | ], 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/view/pages/users/users.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:settings/l10n/l10n.dart'; 3 | import 'package:settings/view/pages/settings_page.dart'; 4 | 5 | class UsersPage extends StatelessWidget { 6 | const UsersPage({super.key}); 7 | 8 | static Widget create(BuildContext context) => const UsersPage(); 9 | 10 | static Widget createTitle(BuildContext context) => 11 | Text(context.l10n.usersPageTitle); 12 | 13 | static bool searchMatches(String value, BuildContext context) => value 14 | .isNotEmpty 15 | ? context.l10n.usersPageTitle.toLowerCase().contains(value.toLowerCase()) 16 | : false; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return SettingsPage( 21 | children: [ 22 | Center( 23 | child: Text(context.l10n.usersPageTitle), 24 | ), 25 | ], 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | void fl_register_plugins(FlPluginRegistry* registry) { 18 | g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = 19 | fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); 20 | file_selector_plugin_register_with_registrar(file_selector_linux_registrar); 21 | g_autoptr(FlPluginRegistrar) gtk_registrar = 22 | fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); 23 | gtk_plugin_register_with_registrar(gtk_registrar); 24 | g_autoptr(FlPluginRegistrar) handy_window_registrar = 25 | fl_plugin_registry_get_registrar_for_plugin(registry, "HandyWindowPlugin"); 26 | handy_window_plugin_register_with_registrar(handy_window_registrar); 27 | g_autoptr(FlPluginRegistrar) screen_retriever_registrar = 28 | fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); 29 | screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); 30 | g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = 31 | fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); 32 | url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 33 | g_autoptr(FlPluginRegistrar) window_manager_registrar = 34 | fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); 35 | window_manager_plugin_register_with_registrar(window_manager_registrar); 36 | g_autoptr(FlPluginRegistrar) yaru_window_linux_registrar = 37 | fl_plugin_registry_get_registrar_for_plugin(registry, "YaruWindowLinuxPlugin"); 38 | yaru_window_linux_plugin_register_with_registrar(yaru_window_linux_registrar); 39 | } 40 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | file_selector_linux 7 | gtk 8 | handy_window 9 | screen_retriever 10 | url_launcher_linux 11 | window_manager 12 | yaru_window_linux 13 | ) 14 | 15 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 16 | ) 17 | 18 | set(PLUGIN_BUNDLED_LIBRARIES) 19 | 20 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 21 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 22 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 23 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 24 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 25 | endforeach(plugin) 26 | 27 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 28 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 29 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 30 | endforeach(ffi_plugin) 31 | -------------------------------------------------------------------------------- /linux/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /linux/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: settings 2 | description: A new Flutter project. 3 | 4 | publish_to: "none" # Remove this line if you wish to publish to pub.dev 5 | 6 | version: 1.0.0+1 7 | 8 | environment: 9 | sdk: ">=3.0.0 <4.0.0" 10 | 11 | dependencies: 12 | bluez: ^0.8.0 13 | collection: ^1.17.1 14 | dbus: ^0.7.8 15 | duration: ^4.0.3 16 | equatable: ^2.0.3 17 | file_selector: ^1.0.0 18 | file_selector_linux: ^0.9.2 19 | filesize: ^2.0.1 20 | flex_color_picker: ^3.3.0 21 | flutter: 22 | sdk: flutter 23 | flutter_localizations: 24 | sdk: flutter 25 | flutter_localized_locales: ^2.0.3 26 | flutter_spinbox: ^0.13.1 27 | flutter_svg: ^2.0.7 28 | gsettings: ^0.2.8 29 | handy_window: ^0.4.0 30 | http: ^1.1.0 31 | intl: ^0.19.0 32 | linux_datetime_service: 33 | git: https://github.com/ubuntu-flutter-community/linux_datetime 34 | linux_system_info: 35 | git: 36 | url: https://github.com/Feichtmeier/linux_system_info.git 37 | ref: update_dbus_ffi_and_xml 38 | meta: ^1.9.1 39 | mime: ^2.0.0 40 | nm: ^0.5.0 41 | path_provider: ^2.0.9 42 | pdf: ^3.7.4 43 | provider: ^6.1.2 44 | safe_change_notifier: ^0.4.0 45 | udisks: ^0.4.0 46 | upower: ^0.7.0 47 | url_launcher: ^6.0.20 48 | watch_it: ^1.4.2 49 | xdg_accounts: 50 | git: https://github.com/ubuntu-flutter-community/xdg_accounts 51 | xml: ^6.2.2 52 | yaru: 53 | git: 54 | url: https://github.com/ubuntu/yaru.dart 55 | ref: ba067738fe0a3887bf788b94295fd70a0e1cf908 56 | 57 | dev_dependencies: 58 | build_runner: ^2.1.2 59 | 60 | flutter_lints: ^5.0.0 61 | flutter_test: 62 | sdk: flutter 63 | mockingjay: ^0.6.0 64 | mockito: ^5.0.16 65 | 66 | flutter: 67 | generate: true 68 | uses-material-design: true 69 | 70 | assets: 71 | - assets/images/multitasking/active-screen-edges-panel-mode/ 72 | - assets/images/multitasking/active-screen-edges-dock-mode/ 73 | - assets/images/multitasking/hot-corner-panel-mode/ 74 | - assets/images/multitasking/hot-corner-dock-mode/ 75 | - assets/images/multitasking/workspaces-panel-mode/ 76 | - assets/images/multitasking/workspaces-dock-mode/ 77 | - assets/images/cursor/ 78 | - assets/images/appearance/panel-mode/ 79 | - assets/images/appearance/dock-mode/ 80 | - assets/images/appearance/auto-hide-panel-mode/ 81 | - assets/images/appearance/auto-hide-dock-mode/ 82 | - assets/rive/ 83 | - assets/pdf_assets/ 84 | -------------------------------------------------------------------------------- /snap/gui/settings.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name=Ubuntu Settings 4 | Comment=System Settings 5 | Keywords=Settings; 6 | Exec=/home/frederik/Projects/settings/build/linux/x64/release/bundle/settings 7 | Icon=system-settings 8 | Terminal=false 9 | Categories=Settings; -------------------------------------------------------------------------------- /snap/gui/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubuntu-flutter-community/settings/6a92f728163fbbc8ba47c7e2066c4bc695825031/snap/gui/settings.png -------------------------------------------------------------------------------- /snap/snapcraft.yml: -------------------------------------------------------------------------------- 1 | # Starting Notes: 2 | # org.ufc.settings should be a working ID 3 | 4 | # Interfaces on the desktop that are being read from or written to: 5 | # gsettings (in the whole app) 6 | # bluez (bluetooth page) 7 | # nm (network page) 8 | # dbus: org.gnome.Mutter.DisplayConfig (Display Page) 9 | # dbus: org.freedesktop.locale1 (Local Page) 10 | # org.gnome.SettingsDaemon.Housekeeping (Privacy Page) 11 | # org.freedesktop.hostname1 (Info Page) 12 | # org.gnome.SettingsDaemon.Rfkill (Power Page) 13 | # upower (power page) 14 | # dbus: org.freedesktop.timedate1 (Time Page) 15 | # dbus: org.freedesktop.Accounts and org.freedesktop.Accounts.User (Accounts Page, WIP, not yet in master) 16 | # some day: printers, no idea which interface this is (cups?) 17 | # some day: org.gnome.Settings.SearchProvider and org.gnome.Shell.SearchProvider2 (search page) 18 | 19 | name: settings 20 | version: git 21 | summary: System Settings for the Ubuntu Desktop 22 | description: Control your system with settings-app 23 | website: https://github.com/ubuntu-flutter-community/settings 24 | contact: frederik.feichtmeier@gmail.com 25 | source-code: https://github.com/ubuntu-flutter-community/settings 26 | icon: snap/gui/settings.png 27 | 28 | confinement: strict 29 | base: core22 30 | grade: stable 31 | license: GPL-3.0+ 32 | 33 | architectures: 34 | - build-on: amd64 35 | - build-on: arm64 36 | 37 | apps: 38 | settings: 39 | command: settings 40 | extensions: [gnome] 41 | plugs: 42 | - network 43 | - bluez 44 | - audio-playback 45 | - network-manager-observe 46 | - home 47 | - removable-media 48 | 49 | parts: 50 | settings: 51 | source: . 52 | source-type: git 53 | plugin: flutter 54 | flutter-target: lib/main.dart -------------------------------------------------------------------------------- /test/test_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockingjay/mockingjay.dart'; 4 | import 'package:provider/provider.dart'; 5 | import 'package:safe_change_notifier/safe_change_notifier.dart'; 6 | 7 | typedef WidgetBuilderNoContext = Widget Function(); 8 | 9 | extension TesterExtension on WidgetTester { 10 | /// Test Helper to instantiate and pump widget/screen which depends on Repository [R] 11 | /// and optionally depends on Provider [P]. Add non null [args], if the widgets needs arguments from 12 | /// navigation arguments when it called using: 13 | /// ``` 14 | /// Navigator.of(context).pushNamed('/path', arguments: args); 15 | /// ``` 16 | 17 | Future pumpScreen({ 18 | required WidgetBuilderNoContext widgetBuilder, 19 | required R repository, 20 | P? provider, 21 | dynamic args, 22 | MockNavigator? navigator, 23 | }) async { 24 | var toTest = widgetBuilder.call(); 25 | if (args != null) { 26 | toTest = Navigator( 27 | onGenerateRoute: (_) { 28 | return MaterialPageRoute( 29 | builder: (_) => widgetBuilder.call(), 30 | settings: RouteSettings(arguments: args), 31 | ); 32 | }, 33 | ); 34 | } 35 | 36 | Widget parent = MaterialApp( 37 | home: MockNavigatorProvider( 38 | navigator: navigator ?? MockNavigator(), 39 | child: toTest, 40 | ), 41 | ); 42 | 43 | if (provider != null) { 44 | parent = ChangeNotifierProvider

.value( 45 | value: provider, 46 | child: parent, 47 | ); 48 | } 49 | 50 | await pumpWidget( 51 | Provider.value( 52 | value: repository, 53 | child: parent, 54 | ), 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/widgets/app_theme_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/annotations.dart'; 4 | import 'package:mockito/mockito.dart'; 5 | import 'package:settings/view/app_theme.dart'; 6 | import 'package:yaru/yaru.dart'; 7 | 8 | import 'app_theme_test.mocks.dart'; 9 | 10 | @GenerateMocks([GnomeSettings]) 11 | void main() { 12 | test( 13 | 'App Theme Dark Mode Test', 14 | () { 15 | final settings = MockGnomeSettings(); 16 | final theme = AppTheme(settings); 17 | 18 | when(settings.setValue('gtk-theme', 'Yaru-dark')).thenAnswer( 19 | (realInvocation) async {}, 20 | ); 21 | 22 | theme.apply(Brightness.dark, YaruVariant.orange); 23 | verify(settings.setValue('gtk-theme', 'Yaru-dark')).called(1); 24 | }, 25 | ); 26 | 27 | test( 28 | 'App Theme Light Mode Test', 29 | () { 30 | final settings = MockGnomeSettings(); 31 | final theme = AppTheme(settings); 32 | 33 | when(settings.setValue('gtk-theme', 'Yaru')).thenAnswer( 34 | (realInvocation) async {}, 35 | ); 36 | 37 | theme.apply(Brightness.light, YaruVariant.orange); 38 | verify(settings.setValue('gtk-theme', 'Yaru')).called(1); 39 | }, 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /test/widgets/yaru_check_box_row_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:settings/view/common/yaru_checkbox_row.dart'; 4 | import 'package:yaru/yaru.dart'; 5 | 6 | void main() { 7 | testWidgets('YaruCheckboxRow Test', (tester) async { 8 | await tester.pumpWidget( 9 | MaterialApp( 10 | home: Scaffold( 11 | body: YaruCheckboxRow( 12 | onChanged: (valeu) {}, 13 | enabled: true, 14 | text: 'Check Box', 15 | value: true, 16 | ), 17 | ), 18 | ), 19 | ); 20 | 21 | /// The [byWidgetPredicate] method of the [CommonFinders] class is to specify the 22 | /// type of any widget and so examine the state of that type. 23 | final finder = find.byWidgetPredicate( 24 | (widget) => widget is YaruCheckbox && widget.value == true, 25 | description: 'Check Box is checked', 26 | ); 27 | final sizedBoxFinder = find.byWidgetPredicate( 28 | (widget) => widget is SizedBox && widget.width == null, 29 | description: 'Sized box having width as null', 30 | ); 31 | final textFinder = find.text('Check Box'); 32 | 33 | expect(textFinder, findsOneWidget); 34 | expect(finder, findsOneWidget); 35 | expect(sizedBoxFinder, findsNothing); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /test/widgets/yaru_extra_options_row_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:settings/view/common/yaru_extra_option_row.dart'; 4 | import 'package:yaru/yaru.dart'; 5 | 6 | void main() { 7 | testWidgets( 8 | 'YaruExtraOptionRow widget build test', 9 | (tester) async { 10 | await tester.pumpWidget( 11 | MaterialApp( 12 | home: Scaffold( 13 | body: YaruExtraOptionRow( 14 | actionLabel: 'Repeat Keys', 15 | actionDescription: 'Key presses repeat when key is held down', 16 | value: true, 17 | onChanged: (_) {}, 18 | onPressed: () {}, 19 | iconData: const IconData(0), 20 | ), 21 | ), 22 | ), 23 | ); 24 | 25 | expect(find.text('Repeat Keys'), findsOneWidget); 26 | expect(find.byType(YaruOptionButton), findsOneWidget); 27 | 28 | /// The [byWidgetPredicate] method of the [CommonFinders] class is to specify the 29 | /// type of any widget and so examine the state of that type. 30 | final finder = find.byWidgetPredicate( 31 | (widget) => widget is YaruSwitch && widget.value == true, 32 | description: 'Switch is enabled', 33 | ); 34 | expect(finder, findsOneWidget); 35 | }, 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /test/widgets/yaru_single_info_row_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:settings/view/common/yaru_single_info_row.dart'; 4 | 5 | void main() { 6 | testWidgets('- YaruSingleInfoRow Test', (tester) async { 7 | await tester.pumpWidget( 8 | const MaterialApp( 9 | home: Scaffold( 10 | body: YaruSingleInfoRow( 11 | infoLabel: 'Foo Label', 12 | infoValue: 'Foo Value', 13 | ), 14 | ), 15 | ), 16 | ); 17 | 18 | // Use [widget] if you only expect to match one widget. 19 | // Throws a [StateError] if finder is empty or matches more than one widget. 20 | 21 | final findAlign = 22 | (tester.widget(find.byType(SelectableText)) as SelectableText) 23 | .textAlign; 24 | 25 | expect(find.text('Foo Label'), findsOneWidget); 26 | expect(find.text('Foo Value'), findsOneWidget); 27 | expect(findAlign, TextAlign.right); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /test/widgets/yaru_switch_row_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:settings/view/common/yaru_switch_row.dart'; 4 | import 'package:yaru/yaru.dart'; 5 | 6 | void main() { 7 | testWidgets('- YaruSwitchRow Test', (tester) async { 8 | await tester.pumpWidget( 9 | MaterialApp( 10 | home: Scaffold( 11 | body: YaruSwitchRow( 12 | onChanged: (v) {}, 13 | trailingWidget: const Text('Trailing Widget'), 14 | value: true, 15 | actionDescription: 'Description', 16 | ), 17 | ), 18 | ), 19 | ); 20 | 21 | // Use [widget] if you only expect to match one widget. 22 | // Throws a [StateError] if finder is empty or matches more than one widget. 23 | final findValue = 24 | (tester.widget(find.byType(YaruSwitch)) as YaruSwitch).value; 25 | 26 | expect(find.text('Description'), findsOneWidget); 27 | expect(find.text('Trailing Widget'), findsOneWidget); 28 | expect(find.byType(YaruTile), findsOneWidget); 29 | expect(findValue, true); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /test/widgets/yaru_togggle_button_row_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:settings/view/common/yaru_toggle_buttons_row.dart'; 4 | 5 | void main() { 6 | testWidgets('- YaruToggleButtonsRow Test', (tester) async { 7 | await tester.pumpWidget( 8 | MaterialApp( 9 | home: Scaffold( 10 | body: YaruToggleButtonsRow( 11 | actionLabel: 'Foo Label', 12 | onPressed: (x) {}, 13 | selectedValues: const [false], 14 | actionDescription: 'Foo Description', 15 | labels: const ['Label'], 16 | ), 17 | ), 18 | ), 19 | ); 20 | 21 | expect(find.text('Foo Label'), findsOneWidget); 22 | expect(find.text('Foo Description'), findsOneWidget); 23 | expect(find.byType(ToggleButtons), findsOneWidget); 24 | expect(find.text('Label'), findsOneWidget); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /tools/dbus/generate-remote-object.sh: -------------------------------------------------------------------------------- 1 | dart pub global activate dbus 2 | 3 | rm -rf ../../lib/generated/dbus/ 4 | mkdir ../../lib/generated/dbus/ 5 | dart-dbus generate-remote-object interfaces/org.gnome.Mutter.DisplayConfig.xml -o ../../lib/generated/dbus/display-config-remote-object.dart --------------------------------------------------------------------------------