diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 782ef780c7..4d80807552 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ - + + diff --git a/assets/i18n/en_US.json b/assets/i18n/en_US.json index 1f8002dbdc..eab9e144da 100644 --- a/assets/i18n/en_US.json +++ b/assets/i18n/en_US.json @@ -106,7 +106,7 @@ "storageButton": "Storage", "selectFromStorageButton": "Select from storage", - + "errorMessage": "Unable to use selected application", "downloadToast": "Download function is not available yet", @@ -175,7 +175,7 @@ "pressBackAgain": "Press back again to cancel", "openButton": "Open", "shareButton": "Share file", - + "notificationTitle": "ReVanced Manager is patching", "notificationText": "Tap to return to the installer", @@ -208,7 +208,7 @@ "englishOption": "English", "sourcesLabel": "Sources", - "sourcesLabelHint": "Configure your sources", + "sourcesLabelHint": "Configure the source of patches and integrations", "sourcesIntegrationsLabel": "Integrations source", "sourcesResetDialogTitle": "Reset", "sourcesResetDialogText": "Are you sure you want to reset your sources to their default values?", @@ -216,13 +216,13 @@ "sourcesUpdateNote": "Note: Patches will be updated to the latest version automatically.\n\nThis will reveal your IP address to the server.", "apiURLLabel": "API URL", - "apiURLHint": "Configure your API URL", + "apiURLHint": "Configure the URL of the API to use", "selectApiURL": "API URL", "hostRepositoryLabel": "Repository API", "orgPatchesLabel": "Patches organization", "sourcesPatchesLabel": "Patches source", "orgIntegrationsLabel": "Integrations organization", - + "contributorsLabel": "Contributors", "contributorsHint": "A list of contributors of ReVanced", @@ -230,7 +230,7 @@ "logsHint": "Share ReVanced Manager logs", "enablePatchesSelectionLabel": "Allow changing patch selection", - "enablePatchesSelectionHint": "Allow changing the selection of patches", + "enablePatchesSelectionHint": "Do not prevent selecting or deselecting patches", "enablePatchesSelectionWarningText": "Changing the selection of patches may cause unexpected issues.\n\nEnable anyways?", "disablePatchesSelectionWarningText": "You are about to disable changing the selection of patches.\nThe default selection of patches will be restored.\n\nDisable anyways?", @@ -240,9 +240,9 @@ "universalPatchesHint": "Display all apps and universal patches (may slow down the app list)", "versionCompatibilityCheckLabel": "Version compatibility check", - "versionCompatibilityCheckHint": "Restricts patches to supported app versions", + "versionCompatibilityCheckHint": "Prevent selecting patches that are not compatible with the selected app version", "requireSuggestedAppVersionLabel": "Require suggested app version", - "requireSuggestedAppVersionHint": "Enforce selection of suggested app version", + "requireSuggestedAppVersionHint": "Prevent selecting an app with a version that is not the suggested", "requireSuggestedAppVersionDialogText": "Selecting an app that is not the suggested version may cause unexpected issues.\n\nDo you want to proceed anyways?", "aboutLabel": "About", @@ -320,7 +320,7 @@ "patchedDateHint": "{date} at {time}", "appliedPatchesHint": "{quantity} applied patches", - + "updateNotImplemented": "This feature has not been implemented yet" }, "contributorsView": { diff --git a/lib/services/manager_api.dart b/lib/services/manager_api.dart index 06d87f4344..1e9b4606da 100644 --- a/lib/services/manager_api.dart +++ b/lib/services/manager_api.dart @@ -15,6 +15,8 @@ import 'package:revanced_manager/services/github_api.dart'; import 'package:revanced_manager/services/patcher_api.dart'; import 'package:revanced_manager/services/revanced_api.dart'; import 'package:revanced_manager/services/root_api.dart'; +import 'package:revanced_manager/services/toast.dart'; +import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_checkbox_list_tile.dart'; import 'package:revanced_manager/utils/check_for_supported_patch.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:timeago/timeago.dart'; @@ -23,6 +25,7 @@ import 'package:timeago/timeago.dart'; class ManagerAPI { final RevancedAPI _revancedAPI = locator(); final GithubAPI _githubAPI = locator(); + final Toast _toast = locator(); final RootAPI _rootAPI = RootAPI(); final String patcherRepo = 'revanced-patcher'; final String cliRepo = 'revanced-cli'; @@ -50,12 +53,12 @@ class ManagerAPI { String? integrationsVersion = ''; bool isDefaultPatchesRepo() { - return getPatchesRepo().toLowerCase() == 'revanced/revanced-patches'; + return getPatchesRepo().toLowerCase() == defaultPatchesRepo; } bool isDefaultIntegrationsRepo() { return getIntegrationsRepo().toLowerCase() == - 'revanced/revanced-integrations'; + defaultIntegrationsRepo; } Future initialize() async { @@ -65,6 +68,16 @@ class ManagerAPI { (await getSdkVersion()) >= 31; // ANDROID_12_SDK_VERSION = 31 storedPatchesFile = (await getApplicationDocumentsDirectory()).path + storedPatchesFile; + + // Migrate to new API URL if not done yet as the old one is sunset. + final bool hasMigrated = _prefs.getBool('migratedToNewApiUrl') ?? false; + if (!hasMigrated) { + final String apiUrl = getApiUrl().toLowerCase(); + if (apiUrl.contains('releases.revanced.app')) { + await setApiUrl(''); // Reset to default. + _prefs.setBool('migratedToNewApiUrl', true); + } + } } Future getSdkVersion() async { @@ -82,6 +95,7 @@ class ManagerAPI { } await _revancedAPI.clearAllCache(); await _prefs.setString('apiUrl', url); + _toast.showBottom('settingsView.restartAppForChanges'); } String getRepoUrl() { @@ -582,8 +596,8 @@ class ManagerAPI { return showDialog( barrierDismissible: false, context: context, - builder: (context) => WillPopScope( - onWillPop: () async => false, + builder: (context) => PopScope( + canPop: false, child: AlertDialog( title: I18nText('warning'), content: ValueListenableBuilder( @@ -604,7 +618,7 @@ class ManagerAPI { ), ), const SizedBox(height: 8), - CheckboxListTile( + HapticCheckboxListTile( value: value, contentPadding: EdgeInsets.zero, title: I18nText( @@ -632,7 +646,7 @@ class ManagerAPI { ); } - Future reAssessSavedApps() async { + Future rePatchedSavedApps() async { final List patchedApps = getPatchedApps(); // Remove apps that are not installed anymore. diff --git a/lib/services/revanced_api.dart b/lib/services/revanced_api.dart index 70d2064c98..2f5c210b70 100644 --- a/lib/services/revanced_api.dart +++ b/lib/services/revanced_api.dart @@ -32,7 +32,7 @@ class RevancedAPI { final response = await _dio.get('/contributors'); final List repositories = response.data['repositories']; for (final Map repo in repositories) { - final String name = repo['name']; + final String name = repo['name'].toLowerCase(); contributors[name] = repo['contributors']; } } on Exception catch (e) { @@ -54,7 +54,7 @@ class RevancedAPI { final List tools = response.data['tools']; return tools.firstWhereOrNull( (t) => - t['repository'] == repoName && + (t['repository'] as String).toLowerCase() == repoName.toLowerCase() && (t['name'] as String).endsWith(extension), ); } on Exception catch (e) { diff --git a/lib/services/root_api.dart b/lib/services/root_api.dart index dd2090abc9..39fe5086e4 100644 --- a/lib/services/root_api.dart +++ b/lib/services/root_api.dart @@ -144,8 +144,12 @@ class RootAPI { ); final String mountScript = ''' #!/system/bin/sh - MAGISKTMP="\$(magisk --path)" || MAGISKTMP=/sbin + # Mount using Magisk mirror, if available. + MAGISKTMP="\$( magisk --path )" || MAGISKTMP=/sbin MIRROR="\$MAGISKTMP/.magisk/mirror" + if [ ! -f \$MIRROR ]; then + MIRROR="" + fi until [ "\$(getprop sys.boot_completed)" = 1 ]; do sleep 3; done until [ -d "/sdcard/Android" ]; do sleep 1; done @@ -171,7 +175,7 @@ class RootAPI { } Future installPatchedApk( - String packageName, String patchedFilePath) async { + String packageName, String patchedFilePath,) async { final String newPatchedFilePath = '$_revancedDirPath/$packageName/base.apk'; await Root.exec( cmd: ''' diff --git a/lib/ui/views/app_selector/app_selector_view.dart b/lib/ui/views/app_selector/app_selector_view.dart index a4c6014970..ef679b0c91 100644 --- a/lib/ui/views/app_selector/app_selector_view.dart +++ b/lib/ui/views/app_selector/app_selector_view.dart @@ -4,6 +4,7 @@ import 'package:revanced_manager/ui/views/app_selector/app_selector_viewmodel.da import 'package:revanced_manager/ui/widgets/appSelectorView/app_skeleton_loader.dart'; import 'package:revanced_manager/ui/widgets/appSelectorView/installed_app_item.dart'; import 'package:revanced_manager/ui/widgets/appSelectorView/not_installed_app_item.dart'; +import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart'; import 'package:revanced_manager/ui/widgets/shared/search_bar.dart'; import 'package:stacked/stacked.dart' hide SkeletonLoader; @@ -23,7 +24,7 @@ class _AppSelectorViewState extends State { onViewModelReady: (model) => model.initialize(), viewModelBuilder: () => AppSelectorViewModel(), builder: (context, model, child) => Scaffold( - floatingActionButton: FloatingActionButton.extended( + floatingActionButton: HapticFloatingActionButtonExtended( label: I18nText('appSelectorView.storageButton'), icon: const Icon(Icons.sd_storage), onPressed: () { diff --git a/lib/ui/views/contributors/contributors_viewmodel.dart b/lib/ui/views/contributors/contributors_viewmodel.dart index c8c7975fd8..3085683c54 100644 --- a/lib/ui/views/contributors/contributors_viewmodel.dart +++ b/lib/ui/views/contributors/contributors_viewmodel.dart @@ -14,9 +14,9 @@ class ContributorsViewModel extends BaseViewModel { final Map> contributors = await _managerAPI.getContributors(); patcherContributors = contributors[_managerAPI.defaultPatcherRepo] ?? []; - patchesContributors = contributors[_managerAPI.getPatchesRepo()] ?? []; + patchesContributors = contributors[_managerAPI.getPatchesRepo().toLowerCase()] ?? []; integrationsContributors = - contributors[_managerAPI.getIntegrationsRepo()] ?? []; + contributors[_managerAPI.getIntegrationsRepo().toLowerCase()] ?? []; cliContributors = contributors[_managerAPI.defaultCliRepo] ?? []; managerContributors = contributors[_managerAPI.defaultManagerRepo] ?? []; notifyListeners(); diff --git a/lib/ui/views/home/home_viewmodel.dart b/lib/ui/views/home/home_viewmodel.dart index c9356a561b..242305af3c 100644 --- a/lib/ui/views/home/home_viewmodel.dart +++ b/lib/ui/views/home/home_viewmodel.dart @@ -20,6 +20,7 @@ import 'package:revanced_manager/services/toast.dart'; import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:revanced_manager/ui/widgets/homeView/update_confirmation_dialog.dart'; +import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_checkbox_list_tile.dart'; import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; @@ -38,11 +39,15 @@ class HomeViewModel extends BaseViewModel { File? downloadedApk; Future initialize(BuildContext context) async { + _managerAPI.rePatchedSavedApps().then((_) => _getPatchedApps()); + _latestManagerVersion = await _managerAPI.getLatestManagerVersion(); if (!_managerAPI.getPatchesConsent()) { await showPatchesConsent(context); } + await _patcherAPI.initialize(); + await flutterLocalNotificationsPlugin.initialize( const InitializationSettings( android: AndroidInitializationSettings('ic_notification'), @@ -63,11 +68,13 @@ class HomeViewModel extends BaseViewModel { .resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>() ?.requestNotificationsPermission(); + final bool isConnected = await Connectivity().checkConnectivity() != ConnectivityResult.none; if (!isConnected) { _toast.showBottom('homeView.noConnection'); } + final NotificationAppLaunchDetails? notificationAppLaunchDetails = await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) { @@ -79,8 +86,6 @@ class HomeViewModel extends BaseViewModel { _toast.showBottom('homeView.errorDownloadMessage'); } } - - _managerAPI.reAssessSavedApps().then((_) => _getPatchedApps()); } void navigateToAppInfo(PatchedApplication app) { @@ -203,7 +208,7 @@ class HomeViewModel extends BaseViewModel { ), ), ), - CheckboxListTile( + HapticCheckboxListTile( value: value, contentPadding: EdgeInsets.zero, title: I18nText( diff --git a/lib/ui/views/installer/installer_view.dart b/lib/ui/views/installer/installer_view.dart index a843f8eec6..8de65f3d0d 100644 --- a/lib/ui/views/installer/installer_view.dart +++ b/lib/ui/views/installer/installer_view.dart @@ -5,6 +5,7 @@ import 'package:revanced_manager/ui/views/installer/installer_viewmodel.dart'; import 'package:revanced_manager/ui/widgets/installerView/gradient_progress_indicator.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart'; +import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart'; import 'package:stacked/stacked.dart'; class InstallerView extends StatelessWidget { @@ -15,7 +16,7 @@ class InstallerView extends StatelessWidget { return ViewModelBuilder.reactive( onViewModelReady: (model) => model.initialize(context), viewModelBuilder: () => InstallerViewModel(), - builder: (context, model, child) => WillPopScope( + builder: (context, model, child) => PopScope( child: SafeArea( top: false, bottom: model.isPatching, @@ -23,7 +24,7 @@ class InstallerView extends StatelessWidget { floatingActionButton: Visibility( visible: !model.isPatching && !model.hasErrors, child: model.isInstalled - ? FloatingActionButton.extended( + ? HapticFloatingActionButtonExtended( label: I18nText('installerView.openButton'), icon: const Icon(Icons.open_in_new), onPressed: () { @@ -34,7 +35,7 @@ class InstallerView extends StatelessWidget { }, elevation: 0, ) - : FloatingActionButton.extended( + : HapticFloatingActionButtonExtended( label: I18nText('installerView.installButton'), icon: const Icon(Icons.file_download_outlined), onPressed: model.isInstalling @@ -113,7 +114,7 @@ class InstallerView extends StatelessWidget { ), ), ), - onWillPop: () => model.onWillPop(context), + onPopInvoked: (bool didPop) => model.onWillPop(context), ), ); } diff --git a/lib/ui/views/navigation/navigation_view.dart b/lib/ui/views/navigation/navigation_view.dart index 13a4457e22..5eb6926ae7 100644 --- a/lib/ui/views/navigation/navigation_view.dart +++ b/lib/ui/views/navigation/navigation_view.dart @@ -13,14 +13,10 @@ class NavigationView extends StatelessWidget { return ViewModelBuilder.reactive( onViewModelReady: (model) => model.initialize(context), viewModelBuilder: () => locator(), - builder: (context, model, child) => WillPopScope( - onWillPop: () async { - if (model.currentIndex == 0) { - return true; - } else { - model.setIndex(0); - return false; - } + builder: (context, model, child) => PopScope( + canPop: model.currentIndex == 0, + onPopInvoked: (bool didPop) => { + if (!didPop) model.setIndex(0), }, child: Scaffold( body: PageTransitionSwitcher( diff --git a/lib/ui/views/patch_options/patch_options_view.dart b/lib/ui/views/patch_options/patch_options_view.dart index e6ac1bb280..74f56657de 100644 --- a/lib/ui/views/patch_options/patch_options_view.dart +++ b/lib/ui/views/patch_options/patch_options_view.dart @@ -4,6 +4,7 @@ import 'package:google_fonts/google_fonts.dart'; import 'package:revanced_manager/models/patch.dart'; import 'package:revanced_manager/ui/views/patch_options/patch_options_viewmodel.dart'; import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_options_fields.dart'; +import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart'; import 'package:stacked/stacked.dart'; class PatchOptionsView extends StatelessWidget { @@ -17,7 +18,7 @@ class PatchOptionsView extends StatelessWidget { builder: (context, model, child) => GestureDetector( onTap: () => FocusScope.of(context).unfocus(), child: Scaffold( - floatingActionButton: FloatingActionButton.extended( + floatingActionButton: HapticFloatingActionButtonExtended( onPressed: () async { final bool saved = model.saveOptions(context); if (saved && context.mounted) { diff --git a/lib/ui/views/patcher/patcher_view.dart b/lib/ui/views/patcher/patcher_view.dart index 9def6c05a0..fb0592c008 100644 --- a/lib/ui/views/patcher/patcher_view.dart +++ b/lib/ui/views/patcher/patcher_view.dart @@ -6,6 +6,7 @@ import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:revanced_manager/ui/widgets/patcherView/app_selector_card.dart'; import 'package:revanced_manager/ui/widgets/patcherView/patch_selector_card.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart'; +import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart'; import 'package:stacked/stacked.dart'; class PatcherView extends StatelessWidget { @@ -19,7 +20,7 @@ class PatcherView extends StatelessWidget { builder: (context, model, child) => Scaffold( floatingActionButton: Visibility( visible: model.showPatchButton(), - child: FloatingActionButton.extended( + child: HapticFloatingActionButtonExtended( label: I18nText('patcherView.patchButton'), icon: const Icon(Icons.build), onPressed: () async { diff --git a/lib/ui/views/patches_selector/patches_selector_view.dart b/lib/ui/views/patches_selector/patches_selector_view.dart index 71e90d7962..3aa1da241f 100644 --- a/lib/ui/views/patches_selector/patches_selector_view.dart +++ b/lib/ui/views/patches_selector/patches_selector_view.dart @@ -3,6 +3,7 @@ import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart'; +import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart'; import 'package:revanced_manager/ui/widgets/shared/search_bar.dart'; import 'package:stacked/stacked.dart'; @@ -36,7 +37,7 @@ class _PatchesSelectorViewState extends State { builder: (context, model, child) => Scaffold( floatingActionButton: Visibility( visible: model.patches.isNotEmpty, - child: FloatingActionButton.extended( + child: HapticFloatingActionButtonExtended( label: Row( children: [ I18nText('patchesSelectorView.doneButton'), diff --git a/lib/ui/views/settings/settingsFragment/settings_manage_api_url.dart b/lib/ui/views/settings/settingsFragment/settings_manage_api_url.dart index 5669a07303..d0c44c4288 100644 --- a/lib/ui/views/settings/settingsFragment/settings_manage_api_url.dart +++ b/lib/ui/views/settings/settingsFragment/settings_manage_api_url.dart @@ -4,14 +4,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/services/manager_api.dart'; -import 'package:revanced_manager/services/toast.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart'; import 'package:stacked/stacked.dart'; class SManageApiUrl extends BaseViewModel { final ManagerAPI _managerAPI = locator(); - final Toast _toast = locator(); final TextEditingController _apiUrlController = TextEditingController(); @@ -35,15 +32,22 @@ class SManageApiUrl extends BaseViewModel { content: SingleChildScrollView( child: Column( children: [ - CustomTextField( - leadingIcon: Icon( - Icons.api_outlined, - color: Theme.of(context).colorScheme.secondary, - ), - inputController: _apiUrlController, - label: I18nText('settingsView.selectApiURL'), - hint: apiUrl, + TextField( + controller: _apiUrlController, + autocorrect: false, onChanged: (value) => notifyListeners(), + decoration: InputDecoration( + icon: Icon( + Icons.api_outlined, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + border: const OutlineInputBorder(), + labelText: FlutterI18n.translate( + context, + 'settingsView.selectApiURL', + ), + hintText: apiUrl, + ), ), ], ), @@ -63,7 +67,6 @@ class SManageApiUrl extends BaseViewModel { apiUrl = 'https://$apiUrl'; } _managerAPI.setApiUrl(apiUrl); - _toast.showBottom('settingsView.restartAppForChanges'); Navigator.of(context).pop(); }, child: I18nText('okButton'), @@ -87,7 +90,6 @@ class SManageApiUrl extends BaseViewModel { FilledButton( onPressed: () { _managerAPI.setApiUrl(''); - _toast.showBottom('settingsView.restartAppForChanges'); Navigator.of(context) ..pop() ..pop(); diff --git a/lib/ui/views/settings/settingsFragment/settings_manage_keystore_password.dart b/lib/ui/views/settings/settingsFragment/settings_manage_keystore_password.dart index fb717f64bb..6a704238fd 100644 --- a/lib/ui/views/settings/settingsFragment/settings_manage_keystore_password.dart +++ b/lib/ui/views/settings/settingsFragment/settings_manage_keystore_password.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/services/manager_api.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart'; import 'package:stacked/stacked.dart'; @@ -35,11 +34,17 @@ class SManageKeystorePassword extends BaseViewModel { content: SingleChildScrollView( child: Column( children: [ - CustomTextField( - inputController: _keystorePasswordController, - label: I18nText('settingsView.selectKeystorePassword'), - hint: '', + TextField( + controller: _keystorePasswordController, + autocorrect: false, + obscureText: true, onChanged: (value) => notifyListeners(), + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: I18nText( + 'settingsView.selectKeystorePassword', + ).toString(), + ), ), ], ), diff --git a/lib/ui/views/settings/settingsFragment/settings_manage_sources.dart b/lib/ui/views/settings/settingsFragment/settings_manage_sources.dart index 52c30ff043..363dab8af4 100644 --- a/lib/ui/views/settings/settingsFragment/settings_manage_sources.dart +++ b/lib/ui/views/settings/settingsFragment/settings_manage_sources.dart @@ -5,7 +5,6 @@ import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/toast.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart'; import 'package:stacked/stacked.dart'; @@ -45,59 +44,102 @@ class SManageSources extends BaseViewModel { content: SingleChildScrollView( child: Column( children: [ - CustomTextField( - leadingIcon: const Icon( - Icons.extension_outlined, - color: Colors.transparent, - ), - inputController: _hostSourceController, - label: I18nText('settingsView.hostRepositoryLabel'), - hint: hostRepository, + /* + API for accessing the specified repositories + If default is used, will use the ReVanced API + */ + TextField( + controller: _hostSourceController, + autocorrect: false, onChanged: (value) => notifyListeners(), - ), - const SizedBox(height: 20), - CustomTextField( - leadingIcon: Icon( - Icons.extension_outlined, - color: Theme.of(context).colorScheme.secondary, + decoration: InputDecoration( + icon: Icon( + Icons.rocket_launch_outlined, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + border: const OutlineInputBorder(), + labelText: FlutterI18n.translate( + context, + 'settingsView.hostRepositoryLabel', + ), + hintText: hostRepository, ), - inputController: _orgPatSourceController, - label: I18nText('settingsView.orgPatchesLabel'), - hint: patchesRepo.split('/')[0], - onChanged: (value) => notifyListeners(), ), const SizedBox(height: 8), - CustomTextField( - leadingIcon: const Icon( - Icons.extension_outlined, - color: Colors.transparent, - ), - inputController: _patSourceController, - label: I18nText('settingsView.sourcesPatchesLabel'), - hint: patchesRepo.split('/')[1], + // Patches owner's name + TextField( + controller: _orgPatSourceController, + autocorrect: false, onChanged: (value) => notifyListeners(), - ), - const SizedBox(height: 20), - CustomTextField( - leadingIcon: Icon( - Icons.merge_outlined, - color: Theme.of(context).colorScheme.secondary, + decoration: InputDecoration( + icon: Icon( + Icons.extension_outlined, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + border: const OutlineInputBorder(), + labelText: FlutterI18n.translate( + context, + 'settingsView.orgPatchesLabel', + ), + hintText: patchesRepo.split('/')[0], ), - inputController: _orgIntSourceController, - label: I18nText('settingsView.orgIntegrationsLabel'), - hint: integrationsRepo.split('/')[0], + ), + const SizedBox(height: 8), + // Patches repository's name + TextField( + controller: _patSourceController, + autocorrect: false, onChanged: (value) => notifyListeners(), + decoration: InputDecoration( + icon: const Icon( + Icons.extension_outlined, + color: Colors.transparent, + ), + border: const OutlineInputBorder(), + labelText: FlutterI18n.translate( + context, + 'settingsView.sourcesPatchesLabel', + ), + hintText: patchesRepo.split('/')[1], + ), ), const SizedBox(height: 8), - CustomTextField( - leadingIcon: const Icon( - Icons.merge_outlined, - color: Colors.transparent, + // Integrations owner's name + TextField( + controller: _orgIntSourceController, + autocorrect: false, + onChanged: (value) => notifyListeners(), + decoration: InputDecoration( + icon: Icon( + Icons.merge_outlined, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + border: const OutlineInputBorder(), + labelText: FlutterI18n.translate( + context, + 'settingsView.orgIntegrationsLabel', + ), + hintText: integrationsRepo.split('/')[0], ), - inputController: _intSourceController, - label: I18nText('settingsView.sourcesIntegrationsLabel'), - hint: integrationsRepo.split('/')[1], + ), + const SizedBox(height: 8), + // Integrations repository's name + TextField( + controller: _intSourceController, + autocorrect: false, onChanged: (value) => notifyListeners(), + decoration: InputDecoration( + icon: const Icon( + Icons.merge_outlined, + color: Colors.transparent, + ), + border: const OutlineInputBorder(), + labelText: FlutterI18n.translate( + context, + 'settingsView.sourcesIntegrationsLabel', + ), + hintText: integrationsRepo.split('/')[1], + ), ), const SizedBox(height: 20), I18nText('settingsView.sourcesUpdateNote'), diff --git a/lib/ui/views/settings/settingsFragment/settings_update_theme.dart b/lib/ui/views/settings/settingsFragment/settings_update_theme.dart index 09c1b28b19..cdd6c540bb 100644 --- a/lib/ui/views/settings/settingsFragment/settings_update_theme.dart +++ b/lib/ui/views/settings/settingsFragment/settings_update_theme.dart @@ -7,6 +7,8 @@ import 'package:flutter_i18n/widgets/I18nText.dart'; import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart'; +import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_radio_list_tile.dart'; +import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart'; class SUpdateThemeUI extends StatefulWidget { const SUpdateThemeUI({super.key}); @@ -42,7 +44,7 @@ class _SUpdateThemeUIState extends State { onTap: () => {showThemeDialog(context)}, ), if (managerAPI.isDynamicThemeAvailable) - SwitchListTile( + HapticSwitchListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), title: I18nText( 'settingsView.dynamicThemeLabel', @@ -129,7 +131,7 @@ class _SUpdateThemeUIState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - RadioListTile( + HapticRadioListTile( title: I18nText('settingsView.systemThemeLabel'), contentPadding: const EdgeInsets.symmetric(horizontal: 16), value: 0, @@ -138,7 +140,7 @@ class _SUpdateThemeUIState extends State { newTheme.value = value!; }, ), - RadioListTile( + HapticRadioListTile( title: I18nText('settingsView.lightThemeLabel'), contentPadding: const EdgeInsets.symmetric(horizontal: 16), value: 1, @@ -147,7 +149,7 @@ class _SUpdateThemeUIState extends State { newTheme.value = value!; }, ), - RadioListTile( + HapticRadioListTile( title: I18nText('settingsView.darkThemeLabel'), contentPadding: const EdgeInsets.symmetric(horizontal: 16), value: 2, diff --git a/lib/ui/widgets/patchesSelectorView/patch_item.dart b/lib/ui/widgets/patchesSelectorView/patch_item.dart index 70692ab2e8..233455118c 100644 --- a/lib/ui/widgets/patchesSelectorView/patch_item.dart +++ b/lib/ui/widgets/patchesSelectorView/patch_item.dart @@ -5,6 +5,8 @@ import 'package:revanced_manager/models/patch.dart'; import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/toast.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; +import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_checkbox.dart'; +import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_custom_card.dart'; // ignore: must_be_immutable class PatchItem extends StatefulWidget { @@ -56,7 +58,7 @@ class _PatchItemState extends State { widget._managerAPI.isVersionCompatibilityCheckEnabled() == true ? 0.5 : 1, - child: CustomCard( + child: HapticCustomCard( padding: EdgeInsets.only( top: 12, bottom: 16, @@ -88,7 +90,7 @@ class _PatchItemState extends State { children: [ Transform.scale( scale: 1.2, - child: Checkbox( + child: HapticCheckbox( value: widget.isSelected, activeColor: Theme.of(context).colorScheme.primary, checkColor: Theme.of(context).colorScheme.secondaryContainer, diff --git a/lib/ui/widgets/settingsView/custom_switch.dart b/lib/ui/widgets/settingsView/custom_switch.dart deleted file mode 100644 index 75229bb03e..0000000000 --- a/lib/ui/widgets/settingsView/custom_switch.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:flutter/material.dart'; - -class CustomSwitch extends StatelessWidget { - const CustomSwitch({ - super.key, - required this.onChanged, - required this.value, - }); - final ValueChanged onChanged; - final bool value; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () => onChanged(!value), - child: SizedBox( - height: 25, - width: 50, - child: Stack( - children: [ - AnimatedContainer( - height: 25, - width: 50, - curve: Curves.ease, - duration: const Duration(milliseconds: 400), - decoration: BoxDecoration( - borderRadius: const BorderRadius.all( - Radius.circular(25.0), - ), - color: value - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.secondary, - ), - ), - AnimatedAlign( - curve: Curves.ease, - duration: const Duration(milliseconds: 400), - alignment: !value ? Alignment.centerLeft : Alignment.centerRight, - child: Container( - height: 20, - width: 20, - margin: const EdgeInsets.symmetric(horizontal: 3), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: value - ? Theme.of(context).colorScheme.primaryContainer - : Theme.of(context).colorScheme.surface, - boxShadow: [ - BoxShadow( - color: Colors.black12.withOpacity(0.1), - spreadRadius: 0.5, - blurRadius: 1, - ), - ], - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/ui/widgets/settingsView/custom_switch_tile.dart b/lib/ui/widgets/settingsView/custom_switch_tile.dart deleted file mode 100644 index 3bf563a5a7..0000000000 --- a/lib/ui/widgets/settingsView/custom_switch_tile.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/custom_switch.dart'; - -class CustomSwitchTile extends StatelessWidget { - const CustomSwitchTile({ - super.key, - required this.title, - required this.subtitle, - required this.value, - required this.onTap, - this.padding, - }); - final Widget title; - final Widget subtitle; - final bool value; - final Function(bool) onTap; - final EdgeInsetsGeometry? padding; - - @override - Widget build(BuildContext context) { - return ListTile( - contentPadding: padding ?? EdgeInsets.zero, - title: title, - subtitle: subtitle, - onTap: () => onTap(!value), - trailing: CustomSwitch( - value: value, - onChanged: onTap, - ), - ); - } -} diff --git a/lib/ui/widgets/settingsView/custom_text_field.dart b/lib/ui/widgets/settingsView/custom_text_field.dart deleted file mode 100644 index 986b2ac685..0000000000 --- a/lib/ui/widgets/settingsView/custom_text_field.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:flutter/material.dart'; - -class CustomTextField extends StatelessWidget { - const CustomTextField({ - super.key, - required this.inputController, - required this.label, - required this.hint, - this.leadingIcon, - required this.onChanged, - }); - final TextEditingController inputController; - final Widget label; - final String hint; - final Widget? leadingIcon; - final Function(String)? onChanged; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(top: 4.0), - child: TextField( - controller: inputController, - onChanged: onChanged, - keyboardType: TextInputType.text, - decoration: InputDecoration( - icon: leadingIcon, - label: label, - filled: true, - fillColor: Theme.of(context).colorScheme.secondaryContainer, - hintText: hint, - hintStyle: TextStyle( - color: Theme.of(context).colorScheme.secondary, - ), - floatingLabelStyle: MaterialStateTextStyle.resolveWith( - (states) => states.contains(MaterialState.focused) - ? TextStyle(color: Theme.of(context).colorScheme.primary) - : TextStyle(color: Theme.of(context).colorScheme.secondary), - ), - contentPadding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 16.0, - ), - border: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.primary, - ), - borderRadius: BorderRadius.circular(10), - ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.primary, - width: 2.0, - ), - borderRadius: BorderRadius.circular(10), - ), - errorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.error, - ), - borderRadius: BorderRadius.circular(10), - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.primary, - ), - borderRadius: BorderRadius.circular(10), - ), - ), - ), - ); - } -} diff --git a/lib/ui/widgets/settingsView/settings_auto_update_patches.dart b/lib/ui/widgets/settingsView/settings_auto_update_patches.dart index 2063d658e6..14c5126cfe 100644 --- a/lib/ui/widgets/settingsView/settings_auto_update_patches.dart +++ b/lib/ui/widgets/settingsView/settings_auto_update_patches.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_i18n/widgets/I18nText.dart'; import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; +import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart'; class SAutoUpdatePatches extends StatefulWidget { const SAutoUpdatePatches({super.key}); @@ -14,7 +15,7 @@ final _settingsViewModel = SettingsViewModel(); class _SAutoUpdatePatchesState extends State { @override Widget build(BuildContext context) { - return SwitchListTile( + return HapticSwitchListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), title: I18nText( 'settingsView.autoUpdatePatchesLabel', diff --git a/lib/ui/widgets/settingsView/settings_enable_patches_selection.dart b/lib/ui/widgets/settingsView/settings_enable_patches_selection.dart index a2fcc86b88..c8384044f2 100644 --- a/lib/ui/widgets/settingsView/settings_enable_patches_selection.dart +++ b/lib/ui/widgets/settingsView/settings_enable_patches_selection.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_i18n/widgets/I18nText.dart'; import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; +import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart'; class SEnablePatchesSelection extends StatefulWidget { const SEnablePatchesSelection({super.key}); @@ -15,7 +16,7 @@ final _settingsViewModel = SettingsViewModel(); class _SEnablePatchesSelectionState extends State { @override Widget build(BuildContext context) { - return SwitchListTile( + return HapticSwitchListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), title: I18nText( 'settingsView.enablePatchesSelectionLabel', diff --git a/lib/ui/widgets/settingsView/settings_require_suggested_app_version.dart b/lib/ui/widgets/settingsView/settings_require_suggested_app_version.dart index da583a9715..9e91b9cb83 100644 --- a/lib/ui/widgets/settingsView/settings_require_suggested_app_version.dart +++ b/lib/ui/widgets/settingsView/settings_require_suggested_app_version.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_i18n/widgets/I18nText.dart'; import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; +import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart'; class SRequireSuggestedAppVersion extends StatefulWidget { const SRequireSuggestedAppVersion({super.key}); @@ -16,7 +17,7 @@ class _SRequireSuggestedAppVersionState extends State { @override Widget build(BuildContext context) { - return SwitchListTile( + return HapticSwitchListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), title: I18nText( 'settingsView.requireSuggestedAppVersionLabel', diff --git a/lib/ui/widgets/settingsView/settings_universal_patches.dart b/lib/ui/widgets/settingsView/settings_universal_patches.dart index 1add0ddf33..98aefa9393 100644 --- a/lib/ui/widgets/settingsView/settings_universal_patches.dart +++ b/lib/ui/widgets/settingsView/settings_universal_patches.dart @@ -3,6 +3,7 @@ import 'package:flutter_i18n/widgets/I18nText.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart'; import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; +import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart'; class SUniversalPatches extends StatefulWidget { const SUniversalPatches({super.key}); @@ -18,7 +19,7 @@ final _patcherViewModel = PatcherViewModel(); class _SUniversalPatchesState extends State { @override Widget build(BuildContext context) { - return SwitchListTile( + return HapticSwitchListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), title: I18nText( 'settingsView.universalPatchesLabel', diff --git a/lib/ui/widgets/settingsView/settings_version_compatibility_check.dart b/lib/ui/widgets/settingsView/settings_version_compatibility_check.dart index a17fcb3b40..7f90bac256 100644 --- a/lib/ui/widgets/settingsView/settings_version_compatibility_check.dart +++ b/lib/ui/widgets/settingsView/settings_version_compatibility_check.dart @@ -3,6 +3,7 @@ import 'package:flutter_i18n/widgets/I18nText.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart'; import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; +import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart'; import 'package:revanced_manager/utils/check_for_supported_patch.dart'; class SVersionCompatibilityCheck extends StatefulWidget { @@ -21,7 +22,7 @@ class _SVersionCompatibilityCheckState extends State { @override Widget build(BuildContext context) { - return SwitchListTile( + return HapticSwitchListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), title: I18nText( 'settingsView.versionCompatibilityCheckLabel', diff --git a/lib/ui/widgets/shared/haptics/haptic_checkbox.dart b/lib/ui/widgets/shared/haptics/haptic_checkbox.dart new file mode 100644 index 0000000000..5a16a8801b --- /dev/null +++ b/lib/ui/widgets/shared/haptics/haptic_checkbox.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class HapticCheckbox extends StatelessWidget { + const HapticCheckbox({ + super.key, + required this.value, + required this.onChanged, + this.activeColor, + this.checkColor, + this.side, + }); + final bool value; + final Function(bool?)? onChanged; + final Color? activeColor; + final Color? checkColor; + final BorderSide? side; + + @override + Widget build(BuildContext context) { + return Checkbox( + value: value, + onChanged: (value) => { + HapticFeedback.selectionClick(), + if (onChanged != null) onChanged!(value), + }, + activeColor: activeColor, + checkColor: checkColor, + side: side, + ); + } +} diff --git a/lib/ui/widgets/shared/haptics/haptic_checkbox_list_tile.dart b/lib/ui/widgets/shared/haptics/haptic_checkbox_list_tile.dart new file mode 100644 index 0000000000..11b8e878ad --- /dev/null +++ b/lib/ui/widgets/shared/haptics/haptic_checkbox_list_tile.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class HapticCheckboxListTile extends StatelessWidget { + const HapticCheckboxListTile({ + super.key, + required this.value, + required this.onChanged, + this.title, + this.subtitle, + this.contentPadding, + }); + final bool value; + final Function(bool?)? onChanged; + final Widget? title; + final Widget? subtitle; + final EdgeInsetsGeometry? contentPadding; + + @override + Widget build(BuildContext context) { + return CheckboxListTile( + contentPadding: contentPadding ?? EdgeInsets.zero, + title: title, + subtitle: subtitle, + value: value, + onChanged: (value) => { + HapticFeedback.lightImpact(), + if (onChanged != null) onChanged!(value), + }, + ); + } +} diff --git a/lib/ui/widgets/shared/haptics/haptic_custom_card.dart b/lib/ui/widgets/shared/haptics/haptic_custom_card.dart new file mode 100644 index 0000000000..fa889329ea --- /dev/null +++ b/lib/ui/widgets/shared/haptics/haptic_custom_card.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; + +class HapticCustomCard extends StatelessWidget { + const HapticCustomCard({ + super.key, + this.isFilled = true, + required this.child, + this.onTap, + this.padding, + this.backgroundColor, + }); + final bool isFilled; + final Widget child; + final Function()? onTap; + final EdgeInsetsGeometry? padding; + final Color? backgroundColor; + + @override + Widget build(BuildContext context) { + return CustomCard( + isFilled: isFilled, + onTap: () => { + HapticFeedback.selectionClick(), + if (onTap != null) onTap!(), + }, + padding: padding, + backgroundColor: backgroundColor, + child: child, + ); + } +} diff --git a/lib/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart b/lib/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart new file mode 100644 index 0000000000..c930ace629 --- /dev/null +++ b/lib/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class HapticFloatingActionButtonExtended extends StatelessWidget { + const HapticFloatingActionButtonExtended({ + super.key, + required this.onPressed, + required this.label, + this.icon, + this.elevation, + }); + final Function()? onPressed; + final Widget label; + final Widget? icon; + final double? elevation; + + @override + Widget build(BuildContext context) { + return FloatingActionButton.extended( + onPressed: () => { + HapticFeedback.lightImpact(), + if (onPressed != null) onPressed!(), + }, + label: label, + icon: icon, + elevation: elevation, + ); + } +} diff --git a/lib/ui/widgets/shared/haptics/haptic_radio_list_tile.dart b/lib/ui/widgets/shared/haptics/haptic_radio_list_tile.dart new file mode 100644 index 0000000000..6c5a799cba --- /dev/null +++ b/lib/ui/widgets/shared/haptics/haptic_radio_list_tile.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class HapticRadioListTile extends StatelessWidget { + const HapticRadioListTile({ + super.key, + required this.title, + required this.value, + required this.groupValue, + this.subtitle, + this.onChanged, + this.contentPadding, + }); + final Widget title; + final Widget? subtitle; + final int value; + final Function(int?)? onChanged; + final int groupValue; + final EdgeInsetsGeometry? contentPadding; + + @override + Widget build(BuildContext context) { + return RadioListTile( + contentPadding: contentPadding ?? EdgeInsets.zero, + title: title, + subtitle: subtitle, + value: value, + groupValue: groupValue, + onChanged: (val) => { + if (val == value) { + HapticFeedback.lightImpact(), + }, + + if (onChanged != null) onChanged!(val), + }, + ); + } +} diff --git a/lib/ui/widgets/shared/haptics/haptic_switch_list_tile.dart b/lib/ui/widgets/shared/haptics/haptic_switch_list_tile.dart new file mode 100644 index 0000000000..8c8cb8d5d5 --- /dev/null +++ b/lib/ui/widgets/shared/haptics/haptic_switch_list_tile.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class HapticSwitchListTile extends StatelessWidget { + const HapticSwitchListTile({ + super.key, + required this.value, + required this.onChanged, + this.title, + this.subtitle, + this.contentPadding, + }); + final bool value; + final Function(bool)? onChanged; + final Widget? title; + final Widget? subtitle; + final EdgeInsetsGeometry? contentPadding; + + @override + Widget build(BuildContext context) { + return SwitchListTile( + contentPadding: contentPadding ?? EdgeInsets.zero, + title: title, + subtitle: subtitle, + value: value, + onChanged: (value) => { + if (value) { + HapticFeedback.mediumImpact(), + } else { + HapticFeedback.lightImpact(), + }, + if (onChanged != null) onChanged!(value), + }, + ); + } +}