diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index c659723..dd9907e 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -476,6 +476,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -510,17 +511,28 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = B73F2CBGJ7; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Chrono Sheet"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = tech.harmonysoft.oss.chronoSheet; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; @@ -599,6 +611,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -656,6 +669,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -692,18 +706,29 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = B73F2CBGJ7; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Chrono Sheet"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = tech.harmonysoft.oss.chronoSheet; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -714,17 +739,28 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = B73F2CBGJ7; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Chrono Sheet"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = tech.harmonysoft.oss.chronoSheet; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; diff --git a/lib/category/widget/category_widget.dart b/lib/category/widget/category_widget.dart index 47699b6..fc149ea 100644 --- a/lib/category/widget/category_widget.dart +++ b/lib/category/widget/category_widget.dart @@ -1,5 +1,6 @@ import 'package:chrono_sheet/category/model/category.dart'; import 'package:chrono_sheet/category/state/categories_state.dart'; +import 'package:chrono_sheet/file/state/files_state.dart'; import 'package:chrono_sheet/generated/app_localizations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -7,109 +8,150 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; class CategoryWidget extends ConsumerWidget { const CategoryWidget({super.key}); - void _addCategory(BuildContext context, FileCategories categoriesNotifier) { - final controller = TextEditingController(); - final hasCategoryNameNotifier = ValueNotifier(false); - controller.addListener(() => hasCategoryNameNotifier.value = controller.text.trim().isNotEmpty); - final l10n = AppLocalizations.of(context); - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(l10n.titleAddNewCategory), - content: TextField( - controller: controller, - decoration: InputDecoration( - labelText: l10n.labelCategoryName, - border: OutlineInputBorder(), - ), + @override + Widget build(BuildContext context, WidgetRef ref) { + final asyncFiles = ref.watch(filesInfoHolderProvider); + return Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(), ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(l10n.textCancel), - ), - ValueListenableBuilder( - valueListenable: hasCategoryNameNotifier, - builder: (context, enabled, child) => ElevatedButton( - onPressed: enabled - ? () { - final categoryName = controller.text; - categoriesNotifier.select(Category(categoryName)); - Navigator.of(context).pop(); - } - : null, - child: Text(l10n.textAdd), + ), + child: asyncFiles.maybeWhen( + data: (files) => + files.operationInProgress == FileOperation.creation ? FileCreationWidget() : NoFileCreationWidget(), + orElse: () => NoFileCreationWidget(), + ), + ); + } +} + +class FileCreationWidget extends StatelessWidget { + const FileCreationWidget({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final localization = AppLocalizations.of(context); + return Row( + children: [ + IconButton( + onPressed: null, + icon: Icon(Icons.add), + ), + Expanded( + child: Center( + child: Text( + localization.progressFileCreationInProgress, + style: TextStyle(color: theme.disabledColor), ), ), - ], - ), + ), + DisabledPopupMenuButtonWidget(), + ], ); } +} + +class NoFileCreationWidget extends ConsumerWidget { + const NoFileCreationWidget({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final asyncInfo = ref.watch(fileCategoriesProvider); + final asyncCategories = ref.watch(fileCategoriesProvider); final theme = Theme.of(context); final localization = AppLocalizations.of(context); - return Container( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(), + return Row( + children: [ + IconButton( + onPressed: () => _addCategory(context, ref.read(fileCategoriesProvider.notifier)), + icon: Icon(Icons.add), ), - ), - child: Row( - children: [ - IconButton( - onPressed: () => _addCategory(context, ref.read(fileCategoriesProvider.notifier)), - icon: Icon(Icons.add), - ), - Expanded( - child: asyncInfo.when( - data: (data) => Container( - color: Colors.transparent, - child: Center( - child: Text( - data.selected?.name ?? localization.hintTapToCreateCategory, - style: data.selected == null ? TextStyle(color: theme.disabledColor) : null, - ), - ), + Expanded( + child: Center( + child: asyncCategories.when( + data: (data) => Text( + data.selected?.name ?? localization.hintCreateCategory, + style: data.selected == null ? TextStyle(color: theme.disabledColor) : null, ), - error: (_, __) => Center( - child: Text( - localization.errorCanNotParseCategories, - style: TextStyle(color: theme.disabledColor), - ), + error: (_, __) => Text( + localization.errorCanNotParseCategories, + style: TextStyle(color: theme.disabledColor), ), - loading: () => Center( - child: Text( - localization.progressParsingCategories, - style: TextStyle(color: theme.disabledColor), - ), + loading: () => Text( + localization.progressParsingCategories, + style: TextStyle(color: theme.disabledColor), ), ), ), - asyncInfo.when( - data: (data) => PopupMenuButton( - icon: Icon(Icons.arrow_drop_down), - onSelected: (category) => ref.read(fileCategoriesProvider.notifier).select(category), - itemBuilder: (context) => data.categories.map((c) { - return PopupMenuItem( - value: c, - child: Text(c.name), - ); - }).toList(), - ), - error: (_, __) => PopupMenuButton( - icon: Icon(Icons.arrow_drop_down), - itemBuilder: (context) => [], - ), - loading: () => PopupMenuButton( - icon: Icon(Icons.arrow_drop_down), - itemBuilder: (context) => [], - ), - ), - ], - ), + ), + asyncCategories.maybeWhen( + data: (data) => data.categories.isEmpty + ? DisabledPopupMenuButtonWidget() + : PopupMenuButton( + icon: Icon(Icons.arrow_drop_down), + onSelected: (category) => ref.read(fileCategoriesProvider.notifier).select(category), + itemBuilder: (context) => data.categories.map((c) { + return PopupMenuItem( + value: c, + child: Text(c.name), + ); + }).toList(), + ), + orElse: () => DisabledPopupMenuButtonWidget(), + ), + ], ); } } + +class DisabledPopupMenuButtonWidget extends StatelessWidget { + const DisabledPopupMenuButtonWidget({super.key}); + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + icon: Icon(Icons.arrow_drop_down), + itemBuilder: (context) => [], + ); + } +} + +void _addCategory(BuildContext context, FileCategories categoriesNotifier) { + final controller = TextEditingController(); + final hasCategoryNameNotifier = ValueNotifier(false); + controller.addListener(() => hasCategoryNameNotifier.value = controller.text.trim().isNotEmpty); + final l10n = AppLocalizations.of(context); + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(l10n.titleAddNewCategory), + content: TextField( + controller: controller, + decoration: InputDecoration( + labelText: l10n.labelCategoryName, + border: OutlineInputBorder(), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(l10n.textCancel), + ), + ValueListenableBuilder( + valueListenable: hasCategoryNameNotifier, + builder: (context, enabled, child) => ElevatedButton( + onPressed: enabled + ? () { + final categoryName = controller.text; + categoriesNotifier.select(Category(categoryName)); + Navigator.of(context).pop(); + } + : null, + child: Text(l10n.textAdd), + ), + ), + ], + ), + ); +} diff --git a/lib/file/state/files_state.dart b/lib/file/state/files_state.dart index 05419d4..602887f 100644 --- a/lib/file/state/files_state.dart +++ b/lib/file/state/files_state.dart @@ -1,4 +1,5 @@ import 'package:chrono_sheet/file/model/google_file.dart'; +import 'package:chrono_sheet/google/state/google_login_state.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../logging/logging.dart'; @@ -48,6 +49,16 @@ class FilesInfoHolder extends _$FilesInfoHolder { @override Future build() async { + final loginState = ref.watch(loginStateProvider); + switch (loginState) { + case AsyncData(value:final loggedIn): + if (!loggedIn) { + return FilesInfo(); + } + default: + return FilesInfo(); + } + var selected = _deserialize(await _prefs.getString(_Key.selected)); if (selected == null) { return FilesInfo(); diff --git a/lib/file/widget/selected_file_widget.dart b/lib/file/widget/selected_file_widget.dart index cd5ce67..6b05630 100644 --- a/lib/file/widget/selected_file_widget.dart +++ b/lib/file/widget/selected_file_widget.dart @@ -78,72 +78,84 @@ class SelectedFileWidget extends ConsumerWidget { final localization = AppLocalizations.of(context); final theme = Theme.of(context); return Container( - decoration: BoxDecoration( - border: Border(bottom: BorderSide()), - ), - child: Row( - // mainAxisAlignment: MainAxisAlignment.center, - children: [ - asyncData.maybeWhen( - data: (data) => data.operationInProgress == FileOperation.creation - ? CircularProgressIndicator() - : IconButton( - onPressed: () => _createFile(context, ref), - icon: Icon(Icons.add), - ), - orElse: () => IconButton( - onPressed: () => _createFile(context, ref), - icon: Icon(Icons.add), - ), + decoration: BoxDecoration( + border: Border(bottom: BorderSide()), + ), + child: Row( + // mainAxisAlignment: MainAxisAlignment.center, + children: [ + asyncData.maybeWhen( + data: (data) => data.operationInProgress == FileOperation.creation + ? CircularProgressIndicator() + : IconButton( + onPressed: () => _createFile(context, ref), + icon: Icon(Icons.add), + ), + orElse: () => IconButton( + onPressed: () => _createFile(context, ref), + icon: Icon(Icons.add), ), - Expanded( - child: GestureDetector( - onTap: () => _selectFile(context), - // we use Container here in order for it to fill all the - // available space occupied by Expanded. Otherwise - // GestureDetector reacts only on taps on the nested Text. - child: Container( - color: Colors.transparent, - child: Center( - child: asyncData.when( - data: (data) => Text( - data.selected?.name ?? localization.hintSelectFile, - style: data.selected == null ? TextStyle(color: theme.disabledColor) : null, - ), - error: (_, __) => Text( - localization.hintSelectFile, - style: TextStyle(color: theme.disabledColor), - ), - loading: () => Text( - localization.hintSelectFile, - style: TextStyle(color: theme.disabledColor), - ), + ), + Expanded( + child: GestureDetector( + onTap: () => _selectFile(context), + // we use Container here in order for it to fill all the + // available space occupied by Expanded. Otherwise + // GestureDetector reacts only on taps on the nested Text. + child: Container( + color: Colors.transparent, + child: Center( + child: asyncData.when( + data: (data) => Text( + data.operationInProgress == FileOperation.none + ? data.selected?.name ?? localization.hintSelectFile + : "", + style: (data.selected == null || data.operationInProgress != FileOperation.none) + ? TextStyle(color: theme.disabledColor) + : null, + ), + error: (_, __) => Text( + localization.hintSelectFile, + style: TextStyle(color: theme.disabledColor), + ), + loading: () => Text( + localization.hintSelectFile, + style: TextStyle(color: theme.disabledColor), ), ), ), ), ), - asyncData.when( - data: (data) => PopupMenuButton( - icon: Icon(Icons.arrow_drop_down), - onSelected: (file) => _onFileSelected(file, ref), - itemBuilder: (context) => data.recent.map((file) { - return PopupMenuItem( - value: file, - child: Text(file.name), - ); - }).toList(), - ), - error: (_, __) => PopupMenuButton( - icon: Icon(Icons.arrow_drop_down), - itemBuilder: (context) => [], - ), - loading: () => PopupMenuButton( - icon: Icon(Icons.arrow_drop_down), - itemBuilder: (context) => [], - ), - ), - ], - )); + ), + asyncData.maybeWhen( + data: (data) => data.operationInProgress == FileOperation.none + ? PopupMenuButton( + icon: Icon(Icons.arrow_drop_down), + onSelected: (file) => _onFileSelected(file, ref), + itemBuilder: (context) => data.recent.map((file) { + return PopupMenuItem( + value: file, + child: Text(file.name), + ); + }).toList(), + ) + : DisabledPopupMenuItem(), + orElse: () => DisabledPopupMenuItem(), + ), + ], + ), + ); + } +} + +class DisabledPopupMenuItem extends StatelessWidget { + const DisabledPopupMenuItem({super.key}); + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + icon: Icon(Icons.arrow_drop_down), + itemBuilder: (context) => [], + ); } } diff --git a/lib/google/google_helper.dart b/lib/google/google_helper.dart index 91256a1..e3b815c 100644 --- a/lib/google/google_helper.dart +++ b/lib/google/google_helper.dart @@ -1,16 +1,16 @@ +import 'package:chrono_sheet/generated/app_localizations.dart'; import 'package:chrono_sheet/logging/logging.dart'; +import 'package:chrono_sheet/main.dart'; +import 'package:flutter/material.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:googleapis/sheets/v4.dart' as sheets; import 'package:http/src/client.dart' as http; - import '../http/AuthenticatedHttpClient.dart'; +import '../util/rich_text_util.dart'; final _logger = getNamedLogger(); -final signIn = GoogleSignIn(scopes: [ - sheets.SheetsApi.spreadsheetsScope, - sheets.SheetsApi.driveFileScope -]); +final signIn = GoogleSignIn(scopes: [sheets.SheetsApi.spreadsheetsScope, sheets.SheetsApi.driveFileScope]); http.Client? _clientOverride; void setClientOverride(http.Client client) { @@ -26,6 +26,21 @@ Future getAuthenticatedGoogleApiHttpClient() async { var googleAccount = await signIn.signInSilently(); if (googleAccount == null) { _logger.fine("failed to sign in silently, signing in normally"); + await showDialog( + context: navigatorKey.currentContext!, + builder: (context) { + final theme = Theme.of(context); + return AlertDialog( + content: buildRichText(AppLocalizations.of(context).textPermissionsRationale, theme.textTheme), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(AppLocalizations.of(context).textOk), + ), + ], + ); + }, + ); googleAccount = await signIn.signIn(); } else { _logger.fine("successfully signed in silently"); @@ -35,4 +50,4 @@ Future getAuthenticatedGoogleApiHttpClient() async { } final headers = await googleAccount.authHeaders; return AuthenticatedHttpClient(headers); -} \ No newline at end of file +} diff --git a/lib/google/state/google_login_state.dart b/lib/google/state/google_login_state.dart index b15bae5..1c2e1ac 100644 --- a/lib/google/state/google_login_state.dart +++ b/lib/google/state/google_login_state.dart @@ -20,8 +20,12 @@ class LoginState extends _$LoginState { Future login() async { _logger.fine("got a request to login"); state = AsyncValue.loading(); - final account = await signIn.signIn(); - state = AsyncValue.data(account != null); + try { + await getAuthenticatedGoogleApiHttpClient(); + state = AsyncValue.data(true); + } catch (e) { + state = AsyncValue.data(false); + } } Future logout() async { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d464e1f..679e55f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -9,8 +9,11 @@ "textSave": "Save", "textCancel": "Cancel", "textAdd": "Add", + "textOk": "Ok", + "textClose": "Close", + "textPermissionsRationale": "**Why Does This App Need Permissions?**\n\nOur app requires access to your Google Drive and Google Sheets to function properly:\n\n 1. **Google Drive:** To show you a list of your existing Google Sheets and let you create a new one if needed.\n\n 2. **Google Sheets:** To store your stopwatch measurements directly in your chosen Google Sheet.\n\nWe only access the files you select or create through the app. Your data remains private and secure.", - "hintTapToCreateCategory": "tap to create a category", + "hintCreateCategory": "create a category", "hintSelectFile": "tap to select a file", "labelCategoryName": "category name", @@ -20,5 +23,6 @@ "errorNoFileIsSelected": "no file is selected", "errorNoCategoryIsSelected": "no category is selected", + "progressFileCreationInProgress": "file creation is in progress...", "progressParsingCategories": "parsing categories from file..." } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 20edcee..323c848 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,6 +9,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'firebase_options.dart'; import 'generated/app_localizations.dart'; +final GlobalKey navigatorKey = GlobalKey(); + void main() async { setupLogging(); final logger = getNamedLogger(); @@ -39,6 +41,7 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( + navigatorKey: navigatorKey, onGenerateTitle: (context) => AppLocalizations.of(context).appName, theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.lightBlue), diff --git a/lib/util/rich_text_util.dart b/lib/util/rich_text_util.dart new file mode 100644 index 0000000..9f06615 --- /dev/null +++ b/lib/util/rich_text_util.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +class _Marker { + static const bold = "**"; +} + +RichText buildRichText(String text, TextTheme theme) { + List spans = []; + int offset = 0; + while (offset < text.length) { + int markerStart = text.indexOf(_Marker.bold, offset); + if (markerStart >= 0) { + int markerEnd = text.indexOf(_Marker.bold, markerStart + 1); + if (markerEnd > markerStart) { + if (offset < markerStart) { + spans.add(TextSpan( + text: text.substring(offset, markerStart), + )); + } + spans.add(TextSpan( + text: text.substring(markerStart + _Marker.bold.length, markerEnd), + style: TextStyle(fontWeight: FontWeight.bold), + )); + offset = markerEnd + _Marker.bold.length; + } else { + spans.add(TextSpan( + text: text.substring(offset, markerStart + _Marker.bold.length), + )); + offset = markerStart + _Marker.bold.length; + } + } else { + spans.add(TextSpan( + text: text.substring(offset), + )); + break; + } + } + return RichText( + text: TextSpan( + style: TextStyle(color: theme.bodyMedium?.color ?? Colors.black, fontSize: 18), + children: spans, + ), + ); +}